UNPKG

@trail-ui/react

Version:
269 lines (266 loc) 9.3 kB
import { Tag, TagGroup, TagList } from "./chunk-3ASQMMZO.mjs"; import { DescriptionProvider, Input, LabeledGroup, WithDescriptionContext, WithLabelContext } from "./chunk-BRBCCMSN.mjs"; import { Button } from "./chunk-JSQOQEGD.mjs"; import { ListBox, ListBoxItem } from "./chunk-WX6MFSZC.mjs"; import { composeTailwindRenderProps, inputFieldStyle } from "./chunk-3GNMOIYS.mjs"; import { Popover } from "./chunk-KJVKK2SZ.mjs"; // src/multiselect/multiselect.tsx import { useCallback, useEffect, useId, useRef, useState } from "react"; import { ComboBox, composeRenderProps, Label } from "react-aria-components"; import { useListData } from "react-stately"; import { useFilter } from "react-aria"; import { twMerge } from "tailwind-merge"; import { ChevronDownIcon, CloseFilledIcon } from "@trail-ui/icons"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; function MultiSelectField({ children, className, ...props }) { return /* @__PURE__ */ jsx( LabeledGroup, { role: "presentation", ...props, className: composeTailwindRenderProps(className, inputFieldStyle), children: /* @__PURE__ */ jsx(DescriptionProvider, { children }) } ); } function MultiSelect({ children, items, selectedList, label, placeholder, onItemRemove, onItemAdd, className, ...props }) { const { contains } = useFilter({ sensitivity: "base" }); const selectedKeys = selectedList.items.map((i) => i.id); const filter = useCallback( (item, filterText) => { return !selectedKeys.includes(item.id) && contains(item.textValue, filterText); }, [contains, selectedKeys] ); const availableList = useListData({ initialItems: items, filter }); const [fieldState, setFieldState] = useState({ selectedKey: null, inputValue: "" }); const onRemove = useCallback( (keys) => { const key = keys.values().next().value; selectedList.remove(key); setFieldState({ inputValue: "", selectedKey: null }); onItemRemove == null ? void 0 : onItemRemove(key); }, [selectedList, onItemRemove] ); const onSelectionChange = (id) => { if (!id) { return; } const item = availableList.getItem(id); if (!item) { return; } if (!selectedKeys.includes(id)) { selectedList.append(item); setFieldState({ inputValue: "", selectedKey: id }); onItemAdd == null ? void 0 : onItemAdd(id); } availableList.setFilterText(""); }; const onInputChange = (value) => { setFieldState((prevState) => ({ inputValue: value, selectedKey: value === "" ? null : prevState.selectedKey })); availableList.setFilterText(value); }; const deleteAllTags = useCallback(() => { selectedList.items.forEach((key) => { selectedList.remove(key.id); onItemRemove == null ? void 0 : onItemRemove(key.id); }); }, [selectedList, onItemRemove]); const tagGroupId = useId(); const triggerRef = useRef(null); const triggerButtonRef = useRef(null); const [width, setWidth] = useState(0); useEffect(() => { const trigger = triggerRef.current; if (!trigger) { return; } const observer = new ResizeObserver((entries) => { for (const entry of entries) { setWidth(entry.target.clientWidth); } }); observer.observe(trigger); return () => { observer.unobserve(trigger); }; }, [triggerRef]); return /* @__PURE__ */ jsx(WithLabelContext, { children: (labelContext) => { return /* @__PURE__ */ jsx(WithDescriptionContext, { children: (descriptionContext) => { var _a; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Label, { className: "pb-1 text-sm font-medium", children: label }), /* @__PURE__ */ jsxs( "div", { "data-slot": "control", ref: triggerRef, className: "relative flex min-h-10 w-full flex-row flex-wrap items-center rounded border border-neutral-300 bg-neutral-50 py-1 pl-1 pr-[60px] has-[input[data-focused=true]]:border-2 has-[input[data-hovered=true]]:border-2 has-[input[data-focused=true]]:border-purple-600 data-[pressed=true]:border-purple-600 data-[hovered=true]:bg-neutral-100", children: [ /* @__PURE__ */ jsx( TagGroup, { id: tagGroupId, "aria-labelledby": labelContext == null ? void 0 : labelContext["aria-labelledBy"], className: "contents", onRemove, children: /* @__PURE__ */ jsx(TagList, { items: selectedList.items, children: (item) => /* @__PURE__ */ jsx(Tag, { textValue: item.textValue, children: item.textValue }) }) } ), /* @__PURE__ */ jsxs( ComboBox, { ...props, allowsEmptyCollection: true, className: twMerge("group flex flex-1", className), items: availableList.items, selectedKey: fieldState.selectedKey, inputValue: fieldState.inputValue, onSelectionChange, onInputChange, "aria-labelledby": labelContext == null ? void 0 : labelContext["aria-labelledBy"], children: [ /* @__PURE__ */ jsxs("div", { className: "inline-flex flex-1 flex-wrap items-center gap-1 py-1", children: [ /* @__PURE__ */ jsx( Input, { placeholder: selectedList.items.length <= 0 ? placeholder : "", className: "flex-1 border-0 py-0 pl-1 text-neutral-900 shadow-none ring-0", onBlur: () => { setFieldState({ inputValue: "", selectedKey: null }); availableList.setFilterText(""); }, "aria-describedby": [ tagGroupId, (_a = descriptionContext == null ? void 0 : descriptionContext["aria-describedby"]) != null ? _a : "" ].join(" ") } ), /* @__PURE__ */ jsx("div", { className: "sr-only", children: /* @__PURE__ */ jsx(Button, { plain: true, ref: triggerButtonRef, children: /* @__PURE__ */ jsx(ChevronDownIcon, { height: 16, width: 16 }) }) }) ] }), /* @__PURE__ */ jsx( Popover, { style: { width: `${width}px` }, triggerRef, className: "max-w-none bg-neutral-50 duration-0", children: /* @__PURE__ */ jsx( ListBox, { selectionMode: "multiple", className: "flex max-h-[inherit] flex-col overflow-auto py-2 outline-none", renderEmptyState: () => { return /* @__PURE__ */ jsx("p", { className: "py-2 text-center text-sm text-neutral-400", children: fieldState.inputValue ? "No results found" : "No more options" }); }, children } ) } ) ] } ), /* @__PURE__ */ jsxs("div", { className: "absolute right-0 mr-2 flex rounded", children: [ selectedList.items.length > 0 && /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center focus-visible:outline-purple-600", onClick: deleteAllTags, children: /* @__PURE__ */ jsx(CloseFilledIcon, { height: 20, width: 20, className: "text-neutral-600" }) } ), /* @__PURE__ */ jsx( "button", { className: "flex h-6 w-6 items-center justify-center focus-visible:outline-purple-600", onClick: (e) => { var _a2; e.preventDefault(); (_a2 = triggerButtonRef.current) == null ? void 0 : _a2.click(); }, children: /* @__PURE__ */ jsx(ChevronDownIcon, { height: 24, width: 24, className: "text-neutral-600" }) } ) ] }) ] } ) ] }); } }); } }); } function MultiSelectItem(props) { return /* @__PURE__ */ jsx( ListBoxItem, { ...props, className: composeRenderProps(props.className, (className) => { return twMerge(["px-2 py-1.5 text-sm", className]); }), children: props.children } ); } export { MultiSelectField, MultiSelect, MultiSelectItem };