/home/vianto5/heredit.mx/wp-content/plugins/code-snippets/js/components/common
Edit: /home/vianto5/heredit.mx/wp-content/plugins/code-snippets/js/components/common/KebabMenu.tsx (6194B)
import classnames from 'classnames'
import React, {
createContext,
useCallback,
useContext,
useEffect,
useId,
useLayoutEffect,
useRef,
useState
} from 'react'
import { KebabIcon } from './icons/KebabIcon'
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: VoidFunction
}
const KebabMenuContext = createContext
({ closeMenu: () => undefined })
export const useKebabMenu = (): KebabMenuContextValue => useContext(KebabMenuContext)
export interface KebabMenuItemProps {
onSelect?: VoidFunction
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 = () =>
export interface KebabMenuRowProps {
className?: string
}
export const KebabMenuRow: React.FC> = ({
className,
children
}) =>
{children}
interface PopoverBehaviourOptions {
isOpen: boolean
setIsOpen: Dispatch>
closeMenu: VoidFunction
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 active = document.activeElement
const isFormInput = active instanceof HTMLElement &&
(active.isContentEditable || active.matches('input, select, textarea'))
if (isFormInput) {
return
}
const focusable = Array.from(popoverRef.current.querySelectorAll(FOCUSABLE_SELECTOR))
const currentIndex = active instanceof HTMLElement ? focusable.indexOf(active) : -1
if (0 < focusable.length && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
event.preventDefault()
const getTarget = () => {
switch (event.key) {
case 'Home':
return 0
case 'End':
return focusable.length - 1
default: {
const offset = currentIndex + ('ArrowDown' === event.key ? 1 : -1)
return (offset + focusable.length) % focusable.length
}
}
}
focusable[getTarget()].focus()
}
}
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
?
: null}
)
}