import { autoUpdate, flip, FloatingPortal, offset, Placement, size, useFloating, } from '@floating-ui/react'; import { MouseEvent, useEffect, useRef } from 'react'; import { Keys } from 'react-hotkeys-hook'; import { Key } from 'ts-key-enum'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { isDefined } from '~/utils/isDefined'; import { useDropdown } from '../hooks/useDropdown'; import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement'; import { DropdownMenu } from './DropdownMenu'; import { DropdownOnToggleEffect } from './DropdownOnToggleEffect'; type DropdownProps = { className?: string; clickableComponent?: JSX.Element | JSX.Element[]; dropdownComponents: JSX.Element | JSX.Element[]; hotkey?: { key: Keys; scope: string; }; dropdownHotkeyScope: HotkeyScope; dropdownId: string; dropdownPlacement?: Placement; dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; dropdownOffset?: { x?: number; y?: number }; dropdownStrategy?: 'fixed' | 'absolute'; disableBlur?: boolean; onClickOutside?: () => void; usePortal?: boolean; onClose?: () => void; onOpen?: () => void; }; export const Dropdown = ({ className, clickableComponent, dropdownComponents, dropdownMenuWidth, hotkey, dropdownId, dropdownHotkeyScope, dropdownPlacement = 'bottom-end', dropdownStrategy = 'absolute', dropdownOffset = { x: 0, y: 0 }, disableBlur = false, usePortal = false, onClickOutside, onClose, onOpen, }: DropdownProps) => { const containerRef = useRef(null); const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth, setDropdownPlacement, resetDropdown, } = useDropdown(dropdownId); const offsetMiddlewares = []; if (isDefined(dropdownOffset.x)) { offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x })); } if (isDefined(dropdownOffset.y)) { offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y })); } const { refs, floatingStyles, placement } = useFloating({ placement: dropdownPlacement, middleware: [ flip(), size({ padding: 12 + 20, // 12px for padding bottom, 20px for dropdown bottom margin target apply: ({ availableHeight, elements }) => { elements.floating.style.maxHeight = availableHeight >= elements.floating.scrollHeight ? '' : `${availableHeight}px`; }, }), ...offsetMiddlewares, ], whileElementsMounted: autoUpdate, strategy: dropdownStrategy, }); useEffect(() => { setDropdownPlacement(placement); }, [placement, setDropdownPlacement]); const handleHotkeyTriggered = () => { toggleDropdown(); }; const handleClickableComponentClick = (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); toggleDropdown(); onClickOutside?.(); }; useListenClickOutside({ refs: [refs.floating], callback: () => { onClickOutside?.(); if (isDropdownOpen) { closeDropdown(); } }, }); useInternalHotkeyScopeManagement({ dropdownScopeId: getScopeIdFromComponentId(dropdownId), dropdownHotkeyScopeFromParent: dropdownHotkeyScope, }); useScopedHotkeys( [Key.Escape], () => { closeDropdown(); }, dropdownHotkeyScope.scope, [closeDropdown], ); useEffect(() => { return () => { resetDropdown(); }; }, [resetDropdown]); return (
{clickableComponent && (
{clickableComponent}
)} {hotkey && ( )} {isDropdownOpen && usePortal && ( {dropdownComponents} )} {isDropdownOpen && !usePortal && ( {dropdownComponents} )}
); };