UNPKG

@flanksource/clicky-ui

Version:

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

144 lines (143 loc) 5.06 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { useRef, useState, useEffect, useMemo } from "react"; import { Button } from "./button.js"; import { cn } from "../lib/utils.js"; import { Icon } from "../data/Icon.js"; function MultiSelect({ options, value, onChange, placeholder = "Select options", disabled, className, triggerClassName, menuClassName }) { const rootRef = useRef(null); const triggerRef = useRef(null); const [open, setOpen] = useState(false); useEffect(() => { if (!open) return; const onPointerDown = (event) => { var _a; if (!((_a = rootRef.current) == null ? void 0 : _a.contains(event.target))) { setOpen(false); } }; const onKeyDown = (event) => { var _a; if (event.key === "Escape") { setOpen(false); (_a = triggerRef.current) == null ? void 0 : _a.focus(); } }; document.addEventListener("mousedown", onPointerDown); document.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("mousedown", onPointerDown); document.removeEventListener("keydown", onKeyDown); }; }, [open]); const selected = useMemo(() => { const selectedOptions = options.filter((option) => value.includes(option.value)); const labels = selectedOptions.map((option) => option.label).filter((label) => typeof label === "string"); if (selectedOptions.length === 0) return placeholder; if (labels.length === 0) return `${selectedOptions.length} selected`; if (labels.length <= 2) return labels.join(", "); return `${labels.length} selected`; }, [options, placeholder, value]); function toggleOption(nextValue) { if (value.includes(nextValue)) { onChange(value.filter((current) => current !== nextValue)); return; } onChange([...value, nextValue]); } return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative", className), children: [ /* @__PURE__ */ jsxs( Button, { ref: triggerRef, type: "button", variant: "outline", size: "sm", disabled, "aria-label": `${placeholder} filter`, "aria-haspopup": "menu", "aria-expanded": open, onClick: () => setOpen((current) => !current), className: cn( "w-fit max-w-[15rem] min-w-0 shrink-0 justify-between gap-3 text-left font-normal", triggerClassName, value.length === 0 && "text-muted-foreground" ), children: [ /* @__PURE__ */ jsx("span", { className: "truncate", children: selected }), /* @__PURE__ */ jsx( Icon, { name: open ? "codicon:chevron-up" : "codicon:chevron-down", className: "text-muted-foreground" } ) ] } ), open && /* @__PURE__ */ jsxs( "div", { role: "menu", className: cn( "absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[14rem] max-w-[20rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5", menuClassName ), children: [ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: [ /* @__PURE__ */ jsx("span", { children: placeholder }), /* @__PURE__ */ jsx( "button", { type: "button", className: "text-[10px] text-primary disabled:text-muted-foreground", onClick: () => onChange([]), disabled: value.length === 0, children: "Clear all" } ) ] }), /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-auto", children: options.map((option) => { const checked = value.includes(option.value); return /* @__PURE__ */ jsxs( "label", { className: cn( "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent", option.disabled && "cursor-not-allowed opacity-50" ), children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", role: "menuitemcheckbox", className: "size-4 rounded border border-input", checked, disabled: option.disabled, onChange: () => toggleOption(option.value) } ), /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: option.label }) ] }, option.value ); }) }) ] } ) ] }); } export { MultiSelect }; //# sourceMappingURL=MultiSelect.js.map