Ensure only one dropdown can be open at a time

This commit is contained in:
Gregory Schier
2025-01-11 14:16:37 -08:00
parent ba330047ca
commit d2936cb022
+17 -5
View File
@@ -26,6 +26,7 @@ import { useClickOutside } from '../../hooks/useClickOutside';
import type { HotkeyAction } from '../../hooks/useHotKey'; import type { HotkeyAction } from '../../hooks/useHotKey';
import { useHotKey } from '../../hooks/useHotKey'; import { useHotKey } from '../../hooks/useHotKey';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import {generateId} from "../../lib/generateId";
import { getNodeText } from '../../lib/getNodeText'; import { getNodeText } from '../../lib/getNodeText';
import { Overlay } from '../Overlay'; import { Overlay } from '../Overlay';
import { Button } from './Button'; import { Button } from './Button';
@@ -33,6 +34,7 @@ import { HotKey } from './HotKey';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
import { atom, useAtom } from 'jotai';
export type DropdownItemSeparator = { export type DropdownItemSeparator = {
type: 'separator'; type: 'separator';
@@ -76,19 +78,28 @@ export interface DropdownRef {
select?: () => void; select?: () => void;
} }
// Every dropdown gets a unique ID and we use this global atom to ensure
// only one dropdown can be open at a time.
const openAtom = atom<string | null>(null);
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown( export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps, { children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
ref, ref,
) { ) {
const [isOpen, _setIsOpen] = useState<boolean>(false); const id = useRef(generateId()).current;
const [openId, setOpenId] = useAtom(openAtom);
const isOpen = openId === id;
// const [isOpen, _setIsOpen] = useState<boolean>(false);
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number | null>(null); const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number | null>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null); const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
const setIsOpen = useCallback( const setIsOpen = useCallback(
(o: SetStateAction<boolean>) => { (o: SetStateAction<boolean>) => {
_setIsOpen((prev) => { setOpenId((prevId) => {
const newIsOpen = typeof o === 'function' ? o(prev) : o; const prevIsOpen = prevId === id;
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
if (newIsOpen) onOpen?.(); if (newIsOpen) onOpen?.();
else onClose?.(); else onClose?.();
@@ -96,10 +107,11 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
// Set to different value when opened and closed to force it to update. This is to force // Set to different value when opened and closed to force it to update. This is to force
// <Menu/> to reset its selected-index state, which it does when this prop changes // <Menu/> to reset its selected-index state, which it does when this prop changes
setDefaultSelectedIndex(newIsOpen ? -1 : null); setDefaultSelectedIndex(newIsOpen ? -1 : null);
return newIsOpen;
return newIsOpen ? id : null; // Set global atom to current ID to signify open state
}); });
}, },
[onClose, onOpen], [id, onClose, onOpen, setOpenId],
); );
const handleClose = useCallback(() => { const handleClose = useCallback(() => {