UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

182 lines (181 loc) 6.19 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { forwardRef, useRef, useState, useImperativeHandle, useMemo, useCallback, useEffect } from "react"; import { cn } from "../lib/utils.js"; import { Icon } from "../data/Icon.js"; import { Button } from "./button.js"; function IconMenuPickerInner({ value, onChange, options, ariaLabel, triggerTitle, footer, className, triggerClassName, menuClassName }, ref) { const rootRef = useRef(null); const triggerRef = useRef(null); const itemRefs = useRef([]); const [open, setOpen] = useState(false); useImperativeHandle(ref, () => rootRef.current); const activeIndex = useMemo( () => Math.max( 0, options.findIndex((option) => option.value === value) ), [options, value] ); const close = useCallback((returnFocus) => { var _a; setOpen(false); if (returnFocus) (_a = triggerRef.current) == null ? void 0 : _a.focus(); }, []); const select = useCallback( (next) => { onChange(next); close(true); }, [close, onChange] ); useEffect(() => { if (!open) return; const onPointerDown = (event) => { var _a; if (event.button !== 0) return; if (!((_a = rootRef.current) == null ? void 0 : _a.contains(event.target))) { close(false); } }; const onKeyDown = (event) => { if (event.key === "Escape") { event.preventDefault(); close(true); } }; document.addEventListener("pointerdown", onPointerDown); document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("pointerdown", onPointerDown); document.removeEventListener("keydown", onKeyDown); }; }, [close, open]); useEffect(() => { if (!open) return; const node = itemRefs.current[activeIndex]; node == null ? void 0 : node.focus(); }, [activeIndex, open]); const focusItem = (index) => { var _a; const wrapped = (index + options.length) % options.length; (_a = itemRefs.current[wrapped]) == null ? void 0 : _a.focus(); }; const onMenuKeyDown = (event) => { const focusedIndex = itemRefs.current.findIndex((node) => node === document.activeElement); if (event.key === "ArrowDown") { event.preventDefault(); focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) + 1); return; } if (event.key === "ArrowUp") { event.preventDefault(); focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) - 1); return; } if (event.key === "Home") { event.preventDefault(); focusItem(0); return; } if (event.key === "End") { event.preventDefault(); focusItem(options.length - 1); return; } if (event.key === "Tab") { close(false); } }; const onTriggerKeyDown = (event) => { if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") { event.preventDefault(); setOpen(true); } }; const selected = options[activeIndex]; if (!selected) { throw new Error(`IconMenuPicker: value "${value}" not found in options for ${ariaLabel}`); } return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative inline-flex", className), children: [ /* @__PURE__ */ jsx( Button, { ref: triggerRef, type: "button", variant: "ghost", size: "icon", "aria-label": ariaLabel, "aria-haspopup": "menu", "aria-expanded": open, title: triggerTitle ?? `${ariaLabel}: ${selected.label}`, onClick: () => setOpen((current) => !current), onKeyDown: onTriggerKeyDown, className: cn("text-muted-foreground hover:text-foreground", triggerClassName), children: /* @__PURE__ */ jsx(Icon, { name: selected.icon }) } ), open && /* @__PURE__ */ jsxs( "div", { role: "menu", "aria-label": ariaLabel, onKeyDown: onMenuKeyDown, className: cn( "absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5", menuClassName ), children: [ options.map((option, index) => { const active = option.value === value; return /* @__PURE__ */ jsxs( "button", { ref: (node) => { itemRefs.current[index] = node; }, type: "button", role: "menuitemradio", "aria-checked": active, tabIndex: active ? 0 : -1, onClick: () => select(option.value), onKeyDown: (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); select(option.value); } }, className: cn( "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors", "hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none", active && "text-foreground" ), children: [ /* @__PURE__ */ jsx(Icon, { name: option.icon, className: "shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate capitalize", children: option.label }), active ? /* @__PURE__ */ jsx(Icon, { name: "ph:check", className: "shrink-0 text-foreground" }) : /* @__PURE__ */ jsx("span", { className: "inline-block size-4 shrink-0", "aria-hidden": true }) ] }, option.value ); }), footer ? /* @__PURE__ */ jsx("div", { className: "mt-1 border-t border-border/60 px-2 py-1.5 text-[11px] text-muted-foreground", children: footer }) : null ] } ) ] }); } const IconMenuPicker = forwardRef(IconMenuPickerInner); export { IconMenuPicker }; //# sourceMappingURL=icon-menu-picker.js.map