import { useRef, useState, useEffect, useMemo, type ReactNode, type ReactElement, type Dispatch, type SetStateAction, type HTMLAttributes, useCallback, Fragment, } from 'react'; import cn from 'classnames'; import Button, { type ButtonProps } from './button'; import '../styles/components/dropdown.scss'; export type DropdownButtonProps = { /** * Content revealed on click. */ children: | ReactNode | ((showMenu: Dispatch>) => ReactNode); /** * Label to be display by the button */ label: ReactNode; /** * Open on pointer over (useful for dropdowns in header) */ openOnHover?: boolean; } & Omit; // Keep it around for now as it's still used in TreeSelect /** @deprecated */ const DropdownButton = ({ children, label, className, openOnHover = false, ...props }: DropdownButtonProps) => { const [showMenu, setShowMenu] = useState(false); const [size, setSize] = useState(); const ref = useRef(null); const dropdownRef = useRef(null); const childType = typeof children; // effect to handle a click on anything closing the dropdown useEffect(() => { if (!showMenu) { return; } const listener = (event: MouseEvent | TouchEvent) => { if ( !ref.current || ref.current?.parentElement?.contains(event.target as Node) || (childType === 'function' && dropdownRef.current?.contains(event.target as Node)) ) { return; } setShowMenu(false); }; window.document.addEventListener('mouseup', listener); window.document.addEventListener('touchend', listener); // eslint-disable-next-line consistent-return return () => { window.document.removeEventListener('mouseup', listener); window.document.removeEventListener('touchend', listener); }; }, [showMenu, childType]); useEffect(() => { if (ref.current && showMenu) { setSize(ref.current.getBoundingClientRect()); } }, [showMenu]); const style = useMemo(() => { if (!size) { return undefined; } const availableHeight = window.innerHeight - size.bottom; return { top: size.height, maxHeight: availableHeight }; }, [size]); return (
// setShowMenu(e.currentTarget.contains(e.relatedTarget as Node)) // } onPointerEnter={openOnHover ? () => setShowMenu(true) : undefined} onPointerLeave={openOnHover ? () => setShowMenu(false) : undefined} >
{showMenu && (typeof children === 'function' ? children(setShowMenu) : children)}
); }; type ControlledDropdownProps = { /** * Element always visible used to open and close the dropdown */ visibleElement: ReactElement; /** * Whether the dropdown is open or closed */ expanded: boolean; }; export const ControlledDropdown = ({ visibleElement, expanded, children, className, 'aria-haspopup': ariaHaspopup, ...props }: ControlledDropdownProps & HTMLAttributes) => (
{/* Not sure why fragments and keys are needed, but otherwise gets the React key warnings messages and children are rendered as array... */} {visibleElement} {expanded &&
{children}
}
); type DropdownProps = { /** * Prop that, when it changes, will cause the dropdown to close */ propChangeToClose?: unknown; /** * Render for element always visible used to open and close the dropdown */ visibleElement: (onClick: () => unknown) => ReactElement; /** * Close if a clickable element within is clicked */ children: ReactNode | ((closeDropdown: () => unknown) => ReactNode); }; export const Dropdown = ({ visibleElement, propChangeToClose, className, 'aria-haspopup': ariaHaspopup, children, ...props }: Omit & DropdownProps & Omit, 'children'>) => { const [expanded, setExpanded] = useState(false); const ref = useRef(null); const close = useCallback(() => setExpanded(false), []); // Effect in order to close the dropdown when the corresponding prop changes useEffect(() => { close(); }, [close, propChangeToClose]); // effect to handle a click on anything closing the dropdown useEffect(() => { if (!expanded) { return; } const listener = (event: MouseEvent | TouchEvent) => { if ( !ref.current || (event.target && ref.current?.contains(event.target as Node)) ) { return; } close(); }; window.document.addEventListener('mouseup', listener, { passive: true }); window.document.addEventListener('touchend', listener, { passive: true }); // eslint-disable-next-line consistent-return return () => { window.document.removeEventListener('mouseup', listener); window.document.removeEventListener('touchend', listener); }; }, [close, expanded]); const handleClick = useCallback( () => setExpanded((expanded) => !expanded), [] ); return (
{/* Not sure why fragments and keys are needed, but otherwise gets the React key warnings messages and children are rendered as array... */} {visibleElement(handleClick)} {expanded && (
{typeof children === 'function' ? children(close) : children}
)}
); }; export default DropdownButton;