Fix dropdown not closing when clicking trigger

This commit is contained in:
Gregory Schier
2025-01-11 06:46:47 -08:00
parent 8b5b66acf0
commit bcf5b3db84
2 changed files with 47 additions and 13 deletions
+14 -13
View File
@@ -7,6 +7,7 @@ import type {
MouseEvent, MouseEvent,
ReactElement, ReactElement,
ReactNode, ReactNode,
RefObject,
SetStateAction, SetStateAction,
} from 'react'; } from 'react';
import React, { import React, {
@@ -20,7 +21,8 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { useClickAway, useKey, useWindowSize } from 'react-use'; import { useKey, useWindowSize } from 'react-use';
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';
@@ -99,10 +101,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
setDefaultSelectedIndex(undefined); setDefaultSelectedIndex(undefined);
}, [setIsOpen]); }, [setIsOpen]);
const openDropdown = useCallback(() => {
setIsOpen((o) => !o);
}, [setIsOpen]);
useImperativeHandle( useImperativeHandle(
ref, ref,
() => ({ () => ({
@@ -113,18 +111,18 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
else this.close(); else this.close();
}, },
open() { open() {
openDropdown(); setIsOpen(true);
}, },
close() { close() {
handleClose(); handleClose();
}, },
}), }),
[handleClose, isOpen, openDropdown], [handleClose, isOpen, setIsOpen],
); );
useHotKey(hotKeyAction ?? null, () => { useHotKey(hotKeyAction ?? null, () => {
setDefaultSelectedIndex(0); setDefaultSelectedIndex(0);
openDropdown(); setIsOpen(true);
}); });
const child = useMemo(() => { const child = useMemo(() => {
@@ -134,17 +132,17 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
...existingChild.props, ...existingChild.props,
ref: buttonRef, ref: buttonRef,
'aria-haspopup': 'true', 'aria-haspopup': 'true',
onClick: onMouseDown:
existingChild.props?.onClick ?? existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => { ((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setDefaultSelectedIndex(undefined); setDefaultSelectedIndex(undefined);
openDropdown(); setIsOpen((o) => !o); // Toggle dropdown
}), }),
}; };
return cloneElement(existingChild, props); return cloneElement(existingChild, props);
}, [children, openDropdown]); }, [children, setIsOpen]);
useEffect(() => { useEffect(() => {
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString()); buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
@@ -163,6 +161,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
<Menu <Menu
ref={menuRef} ref={menuRef}
showTriangle showTriangle
triggerRef={buttonRef}
fullWidth={fullWidth} fullWidth={fullWidth}
defaultSelectedIndex={defaultSelectedIndex} defaultSelectedIndex={defaultSelectedIndex}
items={items} items={items}
@@ -218,6 +217,7 @@ interface MenuProps {
fullWidth?: boolean; fullWidth?: boolean;
isOpen: boolean; isOpen: boolean;
items: DropdownItem[]; items: DropdownItem[];
triggerRef?: RefObject<HTMLButtonElement>;
} }
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'>, MenuProps>( const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'>, MenuProps>(
@@ -231,6 +231,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
triggerShape, triggerShape,
defaultSelectedIndex, defaultSelectedIndex,
showTriangle, showTriangle,
triggerRef,
}: MenuProps, }: MenuProps,
ref, ref,
) { ) {
@@ -415,7 +416,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
); );
const menuRef = useRef<HTMLDivElement | null>(null); const menuRef = useRef<HTMLDivElement | null>(null);
useClickAway(menuRef, handleClose); useClickOutside(menuRef, handleClose, triggerRef);
return ( return (
<> <>
@@ -437,7 +438,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
<motion.div <motion.div
tabIndex={0} tabIndex={0}
onKeyDown={handleMenuKeyDown} onKeyDown={handleMenuKeyDown}
onContextMenu={e => { onContextMenu={(e) => {
// Prevent showing any ancestor context menus // Prevent showing any ancestor context menus
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
+33
View File
@@ -0,0 +1,33 @@
import type { RefObject } from 'react';
import { useEffect, useRef } from 'react';
/**
* Get notified when a mouse click happens outside the target ref
* @param ref The element to be notified when a mouse click happens outside it
* @param onClickAway
* @param ignored Optional outside element to ignore (useful for dropdown triggers)
*/
export function useClickOutside(
ref: RefObject<HTMLElement | null>,
onClickAway: (event: MouseEvent) => void,
ignored?: RefObject<HTMLElement | null>,
) {
const savedCallback = useRef(onClickAway);
useEffect(() => {
savedCallback.current = onClickAway;
}, [onClickAway]);
useEffect(() => {
const handler = (event: MouseEvent) => {
if (ref.current == null || !(event.target instanceof HTMLElement)) return;
const isIgnored = ignored?.current?.contains(event.target);
const clickedOutside = !ref.current.contains(event.target);
if (!isIgnored && clickedOutside) {
savedCallback.current(event);
}
};
document.addEventListener('click', handler);
return () => {
document.removeEventListener('click', handler);
};
}, [ignored, ref]);
}