mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-08 05:45:10 +02:00
Fix dropdown not closing when clicking trigger
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user