Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 1x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 22x 6x 5x 5x 6x 6x 6x 6x 6x 6x 6x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 6x 6x 3x 6x 6x 6x 23x 23x | 'use client'
import {ReactNode, useEffect, useRef, useState} from 'react'
/**
* A button that opens a small menu. It closes on a tap or click anywhere
* outside it, on Escape, and after choosing any link or button inside it.
*/
export default function Dropdown({
trigger,
label,
align = 'right',
triggerClassName = '',
panelClassName = 'w-56',
children,
}: {
trigger: ReactNode
/** Accessible name when the trigger is only a symbol */
label?: string
/** Which edge of the trigger the menu lines up with */
align?: 'left' | 'right'
triggerClassName?: string
panelClassName?: string
children: ReactNode
}) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
// pointerdown, not click: iOS doesn't fire click on non-interactive
// elements, so a tap on plain page background would otherwise be missed
const onDown = (e: PointerEvent) => {
if (!ref.current?.contains(e.target as Node)) setOpen(false)
}
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setOpen(false)
document.addEventListener('pointerdown', onDown)
document.addEventListener('keydown', onKey)
return () => {
document.removeEventListener('pointerdown', onDown)
document.removeEventListener('keydown', onKey)
}
}, [open])
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
aria-haspopup="menu"
aria-label={label}
className={triggerClassName}
>
{trigger}
</button>
{open && (
<div
onClick={(e) =>
(e.target as Element).closest('a,button') && setOpen(false)
}
className={`absolute z-40 mt-1 rounded-xl border border-line-2 bg-panel p-1.5 shadow-xl ${align === 'right' ? 'right-0' : 'left-0'} ${panelClassName}`}
>
{children}
</div>
)}
</div>
)
}
|