@trail-ui/react
Version:
269 lines (266 loc) • 9.3 kB
JavaScript
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
};