mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 04:35:40 +00:00
feat: enhance ThemeDropdown with keyboard navigation and improved layout (#6554)
* feat: enhance ThemeDropdown with keyboard navigation and improved layout - Added keyboard navigation support for theme selection. - Refactored theme selection UI to improve accessibility and user experience. - Updated styles for better responsiveness and visual clarity. - Consolidated theme rendering logic for light and dark themes. * style: update ThemeDropdown styles for focus and active states - Removed outline styles for focused states to enhance visual clarity. - Adjusted background and border colors for active state to improve accessibility and user experience.
This commit is contained in:
@@ -2,79 +2,149 @@ import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
/* Main container */
|
||||
.theme-menu {
|
||||
min-width: 160px;
|
||||
padding: 4px 0;
|
||||
min-width: 200px;
|
||||
height: 325px;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.dropdown.bg};
|
||||
border: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 1px 4px 0px #0000000D;
|
||||
outline: none;
|
||||
|
||||
&.two-columns {
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
padding: 6px 12px 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
/* Mode section */
|
||||
.mode-section {
|
||||
padding: 0 8px 12px 8px;
|
||||
margin: 0 -8px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)};
|
||||
border-color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
|
||||
&.focused {
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)};
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme lists container */
|
||||
.theme-lists {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
&.two-columns {
|
||||
gap: 0;
|
||||
|
||||
.theme-list {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 12px;
|
||||
border-right: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Individual theme list */
|
||||
.theme-list {
|
||||
min-width: 180px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.theme-list-label {
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Theme item */
|
||||
.theme-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
height: 26px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.focused {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.07)};
|
||||
}
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)};
|
||||
|
||||
&.has-submenu {
|
||||
padding-right: 8px;
|
||||
&.focused {
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
.theme-item-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
opacity: 0.6;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
min-width: 180px;
|
||||
padding: 4px 0;
|
||||
background: ${(props) => props.theme.dropdown.bg};
|
||||
border: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import { IconChevronRight, IconCheck, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons';
|
||||
import { IconCheck, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { getLightThemes, getDarkThemes } from 'themes/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// Constants
|
||||
const MODES = ['light', 'dark', 'system'];
|
||||
const MODE_BUTTONS = [
|
||||
{ mode: 'light', icon: IconSun, title: 'Light' },
|
||||
{ mode: 'dark', icon: IconMoon, title: 'Dark' },
|
||||
{ mode: 'system', icon: IconDeviceDesktop, title: 'System' }
|
||||
];
|
||||
|
||||
const ThemeDropdown = ({ children }) => {
|
||||
// Dropdown state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [lightSubmenuOpen, setLightSubmenuOpen] = useState(false);
|
||||
const [darkSubmenuOpen, setDarkSubmenuOpen] = useState(false);
|
||||
const [tooltipEnabled, setTooltipEnabled] = useState(true);
|
||||
|
||||
// Keyboard navigation state
|
||||
const [focusedSection, setFocusedSection] = useState('mode');
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [isKeyboardNav, setIsKeyboardNav] = useState(false);
|
||||
|
||||
// Refs for focus management
|
||||
const menuRef = useRef(null);
|
||||
const modeButtonRefs = useRef([]);
|
||||
const lightItemRefs = useRef([]);
|
||||
const darkItemRefs = useRef([]);
|
||||
|
||||
// Theme context
|
||||
const {
|
||||
storedTheme,
|
||||
setStoredTheme,
|
||||
@@ -21,156 +40,263 @@ const ThemeDropdown = ({ children }) => {
|
||||
setThemeVariantDark
|
||||
} = useTheme();
|
||||
|
||||
// Theme data
|
||||
const lightThemes = getLightThemes();
|
||||
const darkThemes = getDarkThemes();
|
||||
const isSystemMode = storedTheme === 'system';
|
||||
|
||||
const handleModeSelect = (mode) => {
|
||||
setStoredTheme(mode);
|
||||
// Helper to get class names for focusable items
|
||||
const getFocusedClass = (section, index) => {
|
||||
return isKeyboardNav && focusedSection === section && focusedIndex === index ? 'focused' : '';
|
||||
};
|
||||
|
||||
// Handlers
|
||||
const handleModeSelect = (mode) => setStoredTheme(mode);
|
||||
|
||||
const handleThemeSelect = (themeId, isLight) => {
|
||||
if (isLight) {
|
||||
setThemeVariantLight(themeId);
|
||||
} else {
|
||||
setThemeVariantDark(themeId);
|
||||
}
|
||||
setIsOpen(false);
|
||||
setLightSubmenuOpen(false);
|
||||
setDarkSubmenuOpen(false);
|
||||
};
|
||||
|
||||
const renderSubmenu = (themes, isLight, currentVariant) => (
|
||||
<div className="submenu">
|
||||
{themes.map((theme) => (
|
||||
<div
|
||||
key={theme.id}
|
||||
className={`menu-item ${currentVariant === theme.id ? 'active' : ''}`}
|
||||
onClick={() => handleThemeSelect(theme.id, isLight)}
|
||||
>
|
||||
<span className="menu-item-label">{theme.name}</span>
|
||||
{currentVariant === theme.id && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const menuContent = (
|
||||
<StyledWrapper>
|
||||
<div className="theme-menu">
|
||||
{/* Mode Section */}
|
||||
<div className="menu-label">Mode</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'light' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('light')}
|
||||
>
|
||||
<IconSun size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">Light</span>
|
||||
{storedTheme === 'light' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'dark' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('dark')}
|
||||
>
|
||||
<IconMoon size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">Dark</span>
|
||||
{storedTheme === 'dark' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'system' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('system')}
|
||||
>
|
||||
<IconDeviceDesktop size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">System</span>
|
||||
{storedTheme === 'system' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="menu-divider" />
|
||||
|
||||
{/* Light Themes with Submenu */}
|
||||
<Tippy
|
||||
content={renderSubmenu(lightThemes, true, themeVariantLight)}
|
||||
placement="right-start"
|
||||
interactive={true}
|
||||
arrow={false}
|
||||
offset={[0, 2]}
|
||||
animation={false}
|
||||
visible={lightSubmenuOpen}
|
||||
onClickOutside={() => setLightSubmenuOpen(false)}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div
|
||||
className="menu-item has-submenu"
|
||||
onMouseEnter={() => {
|
||||
setLightSubmenuOpen(true);
|
||||
setDarkSubmenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="menu-item-label">Light Themes</span>
|
||||
<IconChevronRight size={14} strokeWidth={2} className="chevron-icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
|
||||
{/* Dark Themes with Submenu */}
|
||||
<Tippy
|
||||
content={renderSubmenu(darkThemes, false, themeVariantDark)}
|
||||
placement="right-start"
|
||||
interactive={true}
|
||||
arrow={false}
|
||||
offset={[0, 2]}
|
||||
animation={false}
|
||||
visible={darkSubmenuOpen}
|
||||
onClickOutside={() => setDarkSubmenuOpen(false)}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div
|
||||
className="menu-item has-submenu"
|
||||
onMouseEnter={() => {
|
||||
setDarkSubmenuOpen(true);
|
||||
setLightSubmenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="menu-item-label">Dark Themes</span>
|
||||
<IconChevronRight size={14} strokeWidth={2} className="chevron-icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
const handleOpen = () => {
|
||||
setTooltipEnabled(false);
|
||||
setIsOpen(true);
|
||||
setFocusedSection('mode');
|
||||
setFocusedIndex(0);
|
||||
setIsKeyboardNav(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setLightSubmenuOpen(false);
|
||||
setDarkSubmenuOpen(false);
|
||||
// Small delay before re-enabling tooltip to prevent flash
|
||||
setTimeout(() => setTooltipEnabled(true), 100);
|
||||
};
|
||||
|
||||
const handleMouseEnter = (section, index) => {
|
||||
setIsKeyboardNav(false);
|
||||
setFocusedSection(section);
|
||||
setFocusedIndex(index);
|
||||
};
|
||||
|
||||
// Get available sections based on current mode
|
||||
const getAvailableSections = useCallback(() => {
|
||||
if (isSystemMode) return ['mode', 'light', 'dark'];
|
||||
return storedTheme === 'light' ? ['mode', 'light'] : ['mode', 'dark'];
|
||||
}, [isSystemMode, storedTheme]);
|
||||
|
||||
// Get max index for a section
|
||||
const getMaxIndex = useCallback((section) => {
|
||||
switch (section) {
|
||||
case 'mode': return 2;
|
||||
case 'light': return lightThemes.length - 1;
|
||||
case 'dark': return darkThemes.length - 1;
|
||||
default: return 0;
|
||||
}
|
||||
}, [lightThemes.length, darkThemes.length]);
|
||||
|
||||
// Get mode index for returning to mode section
|
||||
const getModeIndex = useCallback(() => {
|
||||
return MODES.indexOf(storedTheme);
|
||||
}, [storedTheme]);
|
||||
|
||||
// Focus element based on current section and index
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const refs = {
|
||||
mode: modeButtonRefs,
|
||||
light: lightItemRefs,
|
||||
dark: darkItemRefs
|
||||
};
|
||||
refs[focusedSection]?.current[focusedIndex]?.focus();
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isOpen, focusedSection, focusedIndex]);
|
||||
|
||||
// Keyboard navigation handler
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const sections = getAvailableSections();
|
||||
const maxIndex = getMaxIndex(focusedSection);
|
||||
|
||||
const navigationHandlers = {
|
||||
'Escape': () => {
|
||||
e.preventDefault();
|
||||
handleClose();
|
||||
},
|
||||
|
||||
'ArrowDown': () => {
|
||||
e.preventDefault();
|
||||
setIsKeyboardNav(true);
|
||||
if (focusedSection === 'mode') {
|
||||
setFocusedSection(sections[1]);
|
||||
setFocusedIndex(0);
|
||||
} else if (focusedIndex < maxIndex) {
|
||||
setFocusedIndex(focusedIndex + 1);
|
||||
}
|
||||
},
|
||||
|
||||
'ArrowUp': () => {
|
||||
e.preventDefault();
|
||||
setIsKeyboardNav(true);
|
||||
if (focusedSection !== 'mode') {
|
||||
if (focusedIndex > 0) {
|
||||
setFocusedIndex(focusedIndex - 1);
|
||||
} else {
|
||||
setFocusedSection('mode');
|
||||
setFocusedIndex(getModeIndex());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'ArrowLeft': () => {
|
||||
e.preventDefault();
|
||||
setIsKeyboardNav(true);
|
||||
if (focusedSection === 'mode') {
|
||||
if (focusedIndex > 0) setFocusedIndex(focusedIndex - 1);
|
||||
} else if (isSystemMode && focusedSection === 'dark') {
|
||||
setFocusedSection('light');
|
||||
setFocusedIndex(Math.min(focusedIndex, lightThemes.length - 1));
|
||||
}
|
||||
},
|
||||
|
||||
'ArrowRight': () => {
|
||||
e.preventDefault();
|
||||
setIsKeyboardNav(true);
|
||||
if (focusedSection === 'mode') {
|
||||
if (focusedIndex < 2) setFocusedIndex(focusedIndex + 1);
|
||||
} else if (isSystemMode && focusedSection === 'light') {
|
||||
setFocusedSection('dark');
|
||||
setFocusedIndex(Math.min(focusedIndex, darkThemes.length - 1));
|
||||
}
|
||||
},
|
||||
|
||||
'Enter': () => {
|
||||
e.preventDefault();
|
||||
if (focusedSection === 'mode') {
|
||||
handleModeSelect(MODES[focusedIndex]);
|
||||
} else if (focusedSection === 'light') {
|
||||
handleThemeSelect(lightThemes[focusedIndex].id, true);
|
||||
} else if (focusedSection === 'dark') {
|
||||
handleThemeSelect(darkThemes[focusedIndex].id, false);
|
||||
}
|
||||
},
|
||||
|
||||
' ': () => navigationHandlers.Enter(),
|
||||
|
||||
'Tab': () => handleClose()
|
||||
};
|
||||
|
||||
navigationHandlers[e.key]?.();
|
||||
}, [
|
||||
isOpen, focusedSection, focusedIndex, getAvailableSections,
|
||||
getMaxIndex, getModeIndex, isSystemMode, lightThemes, darkThemes
|
||||
]);
|
||||
|
||||
// Set up keyboard listener
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, handleKeyDown]);
|
||||
|
||||
// Render theme list
|
||||
const renderThemeList = (themes, isLight, currentVariant, label) => {
|
||||
const refs = isLight ? lightItemRefs : darkItemRefs;
|
||||
const section = isLight ? 'light' : 'dark';
|
||||
|
||||
return (
|
||||
<div className="theme-list" role="listbox" aria-label={label}>
|
||||
<div className="theme-list-label">{label}</div>
|
||||
{themes.map((theme, index) => {
|
||||
const isActive = currentVariant === theme.id;
|
||||
return (
|
||||
<div
|
||||
key={theme.id}
|
||||
ref={(el) => (refs.current[index] = el)}
|
||||
className={`theme-item ${isActive ? 'active' : ''} ${getFocusedClass(section, index)}`}
|
||||
role="option"
|
||||
aria-selected={isActive}
|
||||
tabIndex={-1}
|
||||
onClick={() => handleThemeSelect(theme.id, isLight)}
|
||||
onMouseEnter={() => handleMouseEnter(section, index)}
|
||||
>
|
||||
<span className="theme-item-label">{theme.name}</span>
|
||||
{isActive && <IconCheck size={14} strokeWidth={2} className="check-icon" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render mode buttons
|
||||
const renderModeButtons = () => (
|
||||
<div className="mode-buttons" role="radiogroup" aria-labelledby="mode-label">
|
||||
{MODE_BUTTONS.map((btn, index) => {
|
||||
const Icon = btn.icon;
|
||||
const isActive = storedTheme === btn.mode;
|
||||
return (
|
||||
<button
|
||||
key={btn.mode}
|
||||
ref={(el) => (modeButtonRefs.current[index] = el)}
|
||||
className={`mode-button ${isActive ? 'active' : ''} ${getFocusedClass('mode', index)}`}
|
||||
role="radio"
|
||||
aria-checked={isActive}
|
||||
tabIndex={-1}
|
||||
onClick={() => handleModeSelect(btn.mode)}
|
||||
onMouseEnter={() => handleMouseEnter('mode', index)}
|
||||
title={btn.title}
|
||||
>
|
||||
<Icon size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Menu content
|
||||
const menuContent = (
|
||||
<StyledWrapper>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`theme-menu ${isSystemMode ? 'two-columns' : ''}`}
|
||||
role="dialog"
|
||||
aria-label="Theme selector"
|
||||
>
|
||||
<div className="mode-section">
|
||||
<div className="mode-label" id="mode-label">Mode</div>
|
||||
{renderModeButtons()}
|
||||
</div>
|
||||
|
||||
<div className={`theme-lists ${isSystemMode ? 'two-columns' : ''}`}>
|
||||
{(storedTheme === 'light' || isSystemMode)
|
||||
&& renderThemeList(lightThemes, true, themeVariantLight, 'Light theme')}
|
||||
{(storedTheme === 'dark' || isSystemMode)
|
||||
&& renderThemeList(darkThemes, false, themeVariantDark, 'Dark theme')}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolHint text="Theme" toolhintId="ThemeDropdown" place="top" offset={10} hidden={!tooltipEnabled}>
|
||||
<Tippy
|
||||
content={menuContent}
|
||||
placement="top-start"
|
||||
interactive={true}
|
||||
interactive
|
||||
arrow={false}
|
||||
animation={false}
|
||||
visible={isOpen}
|
||||
onClickOutside={handleClose}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div onClick={() => isOpen ? handleClose() : handleOpen()}>
|
||||
<div onClick={() => (isOpen ? handleClose() : handleOpen())}>
|
||||
{children}
|
||||
</div>
|
||||
</Tippy>
|
||||
|
||||
Reference in New Issue
Block a user