import classnames from 'classnames' import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react' import type { Dispatch, PropsWithChildren, ReactNode, RefObject, SetStateAction } from 'react' const FOCUSABLE_SELECTOR = [ 'button:not(:disabled)', '[href]', 'input:not(:disabled)', 'select:not(:disabled)', 'textarea:not(:disabled)', '[tabindex]:not([tabindex="-1"])' ].join(', ') interface KebabMenuContextValue { closeMenu: () => void } const KebabMenuContext = createContext({ closeMenu: () => undefined }) export const useKebabMenu = (): KebabMenuContextValue => useContext(KebabMenuContext) export interface KebabMenuItemProps { onSelect?: () => void destructive?: boolean disabled?: boolean className?: string } export const KebabMenuItem: React.FC> = ({ onSelect, destructive, disabled, className, children }) => { const { closeMenu } = useKebabMenu() return (
  • ) } export const KebabMenuDivider: React.FC = () =>
  • {children}
  • const KebabIcon: React.FC = () => interface PopoverBehaviourOptions { isOpen: boolean setIsOpen: Dispatch> closeMenu: () => void containerRef: RefObject triggerRef: RefObject popoverRef: RefObject } const usePopoverPlacement = ({ isOpen, triggerRef, popoverRef }: PopoverBehaviourOptions): boolean => { const [isFlipped, setIsFlipped] = useState(false) useLayoutEffect(() => { if (isOpen && popoverRef.current) { const popover = popoverRef.current.getBoundingClientRect() const trigger = triggerRef.current?.getBoundingClientRect() setIsFlipped(popover.bottom > window.innerHeight && (trigger?.top ?? 0) > popover.height) const firstItem = popoverRef.current.querySelector('button:not(:disabled)') ;(firstItem ?? popoverRef.current).focus() } else { setIsFlipped(false) } }, [isOpen, triggerRef, popoverRef]) return isFlipped } const handlePopoverKeyDown = ( event: KeyboardEvent, { closeMenu, popoverRef }: Pick ) => { if ('Escape' === event.key) { event.preventDefault() closeMenu() return } if (!popoverRef.current) { return } const focusable = Array.from(popoverRef.current.querySelectorAll(FOCUSABLE_SELECTOR)) const active = document.activeElement const currentIndex = active instanceof HTMLElement ? focusable.indexOf(active) : -1 if ( 'ArrowDown' === event.key || 'ArrowUp' === event.key || 'Home' === event.key || 'End' === event.key ) { if (0 < focusable.length) { event.preventDefault() const offset = 'ArrowDown' === event.key ? currentIndex + 1 : currentIndex - 1 const target = 'Home' === event.key ? 0 : 'End' === event.key ? focusable.length - 1 : (offset + focusable.length) % focusable.length focusable[target].focus() } return } } const usePopoverDismissal = ({ isOpen, setIsOpen, closeMenu, containerRef, popoverRef }: PopoverBehaviourOptions) => { useEffect(() => { if (!isOpen) { return } const handlePointerDown = (event: MouseEvent) => { if (event.target instanceof Node && !containerRef.current?.contains(event.target)) { setIsOpen(false) } } const handleFocusIn = (event: FocusEvent) => { if (event.target instanceof Node && !containerRef.current?.contains(event.target)) { setIsOpen(false) } } const handleKeyDown = (event: KeyboardEvent) => handlePopoverKeyDown(event, { closeMenu, popoverRef }) document.addEventListener('mousedown', handlePointerDown) document.addEventListener('focusin', handleFocusIn) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('mousedown', handlePointerDown) document.removeEventListener('focusin', handleFocusIn) document.removeEventListener('keydown', handleKeyDown) } }, [isOpen, setIsOpen, closeMenu, containerRef, popoverRef]) } export interface KebabMenuProps { label: string className?: string children: ReactNode } export const KebabMenu: React.FC = ({ label, className, children }) => { const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) const triggerRef = useRef(null) const popoverRef = useRef(null) const menuId = useId() const closeMenu = useCallback(() => { setIsOpen(false) triggerRef.current?.focus() }, []) const behaviourOptions = { isOpen, setIsOpen, closeMenu, containerRef, triggerRef, popoverRef } const isFlipped = usePopoverPlacement(behaviourOptions) usePopoverDismissal(behaviourOptions) return (
    {isOpen ?
      {children}
    : null}
    ) }