import { offset, Placement, useFloating, flip, arrow, shift, autoUpdate } from '@floating-ui/react'; import clsx from 'clsx'; import { supportsPassiveEvents } from 'detect-passive-events'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactSwipeableViews from 'react-swipeable-views'; import { userTouching } from 'pl-fe/is-mobile'; import { useUiStore, useModalsStore } from 'pl-fe/stores'; import { HStack, IconButton, Portal } from 'pl-fe/components/ui'; import DropdownMenuItem, { MenuItem } from './dropdown-menu-item'; type Menu = Array; interface IDropdownMenuContent { handleClose: () => any; items?: Menu; component?: React.FC<{ handleClose: () => any }>; touchscreen?: boolean; } interface IDropdownMenu { children?: React.ReactElement; disabled?: boolean; items?: Menu; component?: React.FC<{ handleClose: () => any }>; onClose?: () => void; onOpen?: () => void; onShiftClick?: React.EventHandler; placement?: Placement; src?: string; title?: string; } const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const DropdownMenuContent: React.FC = ({ handleClose, items, component: Component, touchscreen }) => { const [tab, setTab] = useState(); const ref = useRef(null); const autoFocus = items && !items.some((item) => item?.active); const handleKeyDown = useMemo(() => (e: KeyboardEvent) => { if (!ref.current) return; const elements = Array.from(ref.current.querySelectorAll('a, button')); const index = elements.indexOf(document.activeElement as any); let element = null; switch (e.key) { case 'ArrowLeft': setTab(tab => { if (tab !== undefined) { (elements[tab] as HTMLElement)?.focus(); return undefined; } return tab; }); break; case 'ArrowRight': // eslint-disable-next-line no-case-declarations const itemIndex = +(elements[index]?.getAttribute('data-index') || ''); if (items?.[itemIndex]?.items) setTab(itemIndex); break; case 'ArrowDown': element = elements[index + 1] || elements[0]; break; case 'ArrowUp': element = elements[index - 1] || elements[elements.length - 1]; break; case 'Tab': if (e.shiftKey) { element = elements[index - 1] || elements[elements.length - 1]; } else { element = elements[index + 1] || elements[0]; } break; case 'Home': element = elements[0]; break; case 'End': element = elements[elements.length - 1]; break; case 'Escape': handleClose(); break; } if (element) { (element as HTMLAnchorElement).focus(); e.preventDefault(); e.stopPropagation(); } }, [ref.current]); const handleDocumentClick = useMemo(() => (event: Event) => { if (ref.current && !ref.current.contains(event.target as Node)) { handleClose(); event.stopPropagation(); } }, [ref.current]); useEffect(() => { if (!touchscreen) { document.addEventListener('click', handleDocumentClick, false); document.addEventListener('touchend', handleDocumentClick, listenerOptions); } document.addEventListener('keydown', handleKeyDown, false); return () => { document.removeEventListener('click', handleDocumentClick); document.removeEventListener('touchend', handleDocumentClick); document.removeEventListener('keydown', handleKeyDown); }; }, [ref.current]); const handleExitSubmenu: React.EventHandler = (event) => { event.stopPropagation(); setTab(undefined); }; const renderItems = (items: Menu | undefined) => (
    {items?.map((item, idx) => ( ))}
); return (
{items?.some(item => item?.items?.length) ? (
{Component && } {(items?.length || touchscreen) && renderItems(items)}
{tab !== undefined && ( <> {items![tab]?.text} {renderItems(items![tab]?.items)} )}
) : ( <> {Component && } {(items?.length || touchscreen) && renderItems(items)} )}
); }; const DropdownMenu = (props: IDropdownMenu) => { const { children, disabled, items, component, onClose, onOpen, onShiftClick, placement: initialPlacement = 'top', src = require('@tabler/icons/outline/dots.svg'), title = 'Menu', } = props; const { openDropdownMenu, closeDropdownMenu } = useUiStore(); const { openModal, closeModal } = useModalsStore(); const [isOpen, setIsOpen] = useState(false); const [isDisplayed, setIsDisplayed] = useState(false); const arrowRef = useRef(null); const { x, y, strategy, refs, middlewareData, placement } = useFloating({ placement: initialPlacement, middleware: [ offset(12), flip(), shift({ padding: 8, }), arrow({ element: arrowRef, }), ], whileElementsMounted: autoUpdate, }); const handleClick: React.EventHandler< React.MouseEvent | React.KeyboardEvent > = (event) => { event.stopPropagation(); if (onShiftClick && event.shiftKey) { event.preventDefault(); onShiftClick(event); return; } if (isOpen) { handleClose(); } else { handleOpen(); } }; const handleOpen = () => { if (userTouching.matches) { const handleClose = () => { closeModal('DROPDOWN_MENU'); }; openModal('DROPDOWN_MENU', { content: , }); } else { openDropdownMenu(); setIsOpen(true); } if (onOpen) { onOpen(); } }; const handleClose = (goBack: any = true) => { (refs.reference.current as HTMLButtonElement)?.focus(); closeDropdownMenu(); setIsOpen(false); if (onClose) { onClose(); } }; const handleKeyPress: React.EventHandler> = (event) => { switch (event.key) { case ' ': case 'Enter': event.stopPropagation(); event.preventDefault(); handleClick(event); break; } }; const arrowProps: React.CSSProperties = useMemo(() => { if (middlewareData.arrow) { const { x, y } = middlewareData.arrow; const staticPlacement = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[placement.split('-')[0]]; return { left: x !== null ? `${x}px` : '', top: y !== null ? `${y}px` : '', // Ensure the static side gets unset when // flipping to other placements' axes. right: '', bottom: '', [staticPlacement as string]: `${(-(arrowRef.current?.offsetWidth || 0)) / 2}px`, transform: 'rotate(45deg)', }; } return {}; }, [middlewareData.arrow, placement]); useEffect(() => () => { closeDropdownMenu(); }, []); useEffect(() => { setTimeout(() => setIsDisplayed(isOpen), isOpen ? 0 : 150); }, [isOpen]); if (items?.length === 0 && !component) { return null; } const getClassName = () => { const className = clsx('z-[1001] bg-white py-1 shadow-lg ease-in-out focus:outline-none black:bg-black no-reduce-motion:transition-all dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', { 'rounded-md min-w-56 max-w-sm duration-100': true, 'no-reduce-motion:scale-0': !(isDisplayed && isOpen), 'scale-100': isDisplayed && isOpen, 'origin-bottom': placement === 'top', 'origin-left': placement === 'right', 'origin-top': placement === 'bottom', 'origin-right': placement === 'left', 'origin-bottom-left': placement === 'top-start', 'origin-bottom-right': placement === 'top-end', 'origin-top-left': placement === 'bottom-start', 'origin-top-right': placement === 'bottom-end', }); return className; }; return ( <> {children ? ( React.cloneElement(children, { disabled, onClick: handleClick, onKeyPress: handleKeyPress, ref: refs.setReference, }) ) : ( )} {isOpen || isDisplayed ? (
{/* Arrow */}
) : null} ); }; export { type Menu, DropdownMenu as default };