@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
JavaScript
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