@nexub/react 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accessibility.json +18 -1
- package/dist/components/Autocomplete/Autocomplete.d.ts +10 -1
- package/dist/components/Autocomplete/Autocomplete.js +69 -21
- package/dist/components/Chip/Chip.d.ts +2 -0
- package/dist/components/Chip/Chip.js +2 -2
- package/dist/components/Combobox/ComboboxPopover.d.ts +19 -0
- package/dist/components/Combobox/ComboboxPopover.js +91 -0
- package/dist/components/MultiSelect/MultiSelect.d.ts +35 -0
- package/dist/components/MultiSelect/MultiSelect.js +142 -0
- package/dist/components/MultiSelect/index.d.ts +2 -0
- package/dist/components/MultiSelect/index.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/styles.css +1 -1
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.2.0",
|
|
3
3
|
"standard": "WCAG 2.2 Level AA",
|
|
4
4
|
"method": {
|
|
5
5
|
"automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark on every pull request; a new violation fails the build.",
|
|
@@ -693,6 +693,23 @@
|
|
|
693
693
|
"keyboard": [],
|
|
694
694
|
"screenReader": []
|
|
695
695
|
},
|
|
696
|
+
{
|
|
697
|
+
"slug": "multi-select",
|
|
698
|
+
"name": "Multi Select",
|
|
699
|
+
"package": "@nexub/react",
|
|
700
|
+
"automated": "pass",
|
|
701
|
+
"exceptions": [],
|
|
702
|
+
"keyboard": [
|
|
703
|
+
"Type: Filters the options and opens the list.",
|
|
704
|
+
"ArrowDown / ArrowUp: Moves through the options.",
|
|
705
|
+
"Enter: Toggles the highlighted option.",
|
|
706
|
+
"Backspace: In an empty field, removes the last chip.",
|
|
707
|
+
"Escape: Closes the list."
|
|
708
|
+
],
|
|
709
|
+
"screenReader": [
|
|
710
|
+
"Options carry `aria-checked`; each chip has a \"Remove <label>\" button."
|
|
711
|
+
]
|
|
712
|
+
},
|
|
696
713
|
{
|
|
697
714
|
"slug": "navigation-rail",
|
|
698
715
|
"name": "Navigation Rail",
|
|
@@ -17,9 +17,18 @@ export interface AutocompleteProps {
|
|
|
17
17
|
* filtered (fetched for this text), so the built-in filtering is off.
|
|
18
18
|
*/
|
|
19
19
|
onInputChange?: (text: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* `'inline'` (default): the list always shows under the input.
|
|
22
|
+
* `'popover'`: a field whose list opens below it while typing.
|
|
23
|
+
*/
|
|
24
|
+
mode?: 'inline' | 'popover';
|
|
25
|
+
/** Adds a "Create “text”" option when the text matches no option exactly. */
|
|
26
|
+
onCreate?: (text: string) => void;
|
|
27
|
+
disabled?: boolean;
|
|
20
28
|
className?: string;
|
|
29
|
+
'aria-label'?: string;
|
|
21
30
|
}
|
|
22
|
-
export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, className, }: AutocompleteProps): React.JSX.Element;
|
|
31
|
+
export declare function Autocomplete({ options, value, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, mode, onCreate, disabled, className, 'aria-label': ariaLabel, }: AutocompleteProps): React.JSX.Element;
|
|
23
32
|
export declare namespace Autocomplete {
|
|
24
33
|
var displayName: string;
|
|
25
34
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { Plus } from "@nexub/icons";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
4
6
|
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
5
7
|
import { Combobox } from "../Combobox/Combobox.js";
|
|
8
|
+
import { ComboboxLoading, ComboboxPopover } from "../Combobox/ComboboxPopover.js";
|
|
6
9
|
function Autocomplete({
|
|
7
10
|
options,
|
|
8
11
|
value,
|
|
@@ -12,39 +15,84 @@ function Autocomplete({
|
|
|
12
15
|
loading = false,
|
|
13
16
|
inputValue,
|
|
14
17
|
onInputChange,
|
|
15
|
-
|
|
18
|
+
mode = "inline",
|
|
19
|
+
onCreate,
|
|
20
|
+
disabled,
|
|
21
|
+
className,
|
|
22
|
+
"aria-label": ariaLabel
|
|
16
23
|
}) {
|
|
17
24
|
const messages = useNexubMessages();
|
|
18
25
|
const selectedLabel = options.find((o) => o.value === value)?.label ?? "";
|
|
19
26
|
const [innerSearch, setInnerSearch] = React.useState(selectedLabel);
|
|
20
27
|
const [prevValue, setPrevValue] = React.useState(value);
|
|
28
|
+
const [open, setOpen] = React.useState(false);
|
|
21
29
|
const search = inputValue ?? innerSearch;
|
|
22
|
-
const setSearch = (
|
|
23
|
-
setInnerSearch(
|
|
24
|
-
onInputChange?.(
|
|
30
|
+
const setSearch = (text2) => {
|
|
31
|
+
setInnerSearch(text2);
|
|
32
|
+
onInputChange?.(text2);
|
|
25
33
|
};
|
|
26
34
|
if (value !== prevValue) {
|
|
27
35
|
setPrevValue(value);
|
|
28
36
|
setInnerSearch(selectedLabel);
|
|
29
37
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
children: opt.label
|
|
38
|
+
const text = search.trim();
|
|
39
|
+
const canCreate = !!onCreate && !loading && text !== "" && !options.some((o) => o.label.toLowerCase() === text.toLowerCase());
|
|
40
|
+
const list = /* @__PURE__ */ jsx(Combobox.List, { children: loading ? /* @__PURE__ */ jsx(ComboboxLoading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
41
|
+
!canCreate && /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
|
|
42
|
+
options.map((opt) => /* @__PURE__ */ jsx(
|
|
43
|
+
Combobox.Item,
|
|
44
|
+
{
|
|
45
|
+
value: opt.label,
|
|
46
|
+
keywords: [opt.value],
|
|
47
|
+
onSelect: () => {
|
|
48
|
+
onValueChange?.(opt.value);
|
|
49
|
+
setSearch(opt.label);
|
|
50
|
+
setOpen(false);
|
|
44
51
|
},
|
|
45
|
-
opt.
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
children: opt.label
|
|
53
|
+
},
|
|
54
|
+
opt.value
|
|
55
|
+
)),
|
|
56
|
+
canCreate && /* @__PURE__ */ jsxs(
|
|
57
|
+
Combobox.Item,
|
|
58
|
+
{
|
|
59
|
+
forceMount: true,
|
|
60
|
+
value: "\0",
|
|
61
|
+
onSelect: () => {
|
|
62
|
+
onCreate(text);
|
|
63
|
+
setOpen(false);
|
|
64
|
+
},
|
|
65
|
+
className: "gap-2",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
|
|
68
|
+
messages.createOption(text)
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] }) });
|
|
73
|
+
const input = {
|
|
74
|
+
placeholder: placeholder ?? messages.searchPlaceholder,
|
|
75
|
+
value: search,
|
|
76
|
+
"aria-label": ariaLabel,
|
|
77
|
+
onValueChange: (t) => {
|
|
78
|
+
setSearch(t);
|
|
79
|
+
setOpen(true);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (mode === "popover") {
|
|
83
|
+
return /* @__PURE__ */ jsx(
|
|
84
|
+
Combobox.Root,
|
|
85
|
+
{
|
|
86
|
+
label: ariaLabel,
|
|
87
|
+
className: cn("overflow-visible rounded-none bg-transparent", className),
|
|
88
|
+
shouldFilter: !onInputChange,
|
|
89
|
+
children: /* @__PURE__ */ jsx(ComboboxPopover, { open: open && !disabled, onOpenChange: setOpen, input, disabled, children: list })
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return /* @__PURE__ */ jsxs(Combobox.Root, { label: ariaLabel, className, shouldFilter: !onInputChange, children: [
|
|
94
|
+
/* @__PURE__ */ jsx(Combobox.Input, { ...input, disabled }),
|
|
95
|
+
list
|
|
48
96
|
] });
|
|
49
97
|
}
|
|
50
98
|
Autocomplete.displayName = "Autocomplete";
|
|
@@ -8,5 +8,7 @@ export declare const chipVariants: (props?: ({
|
|
|
8
8
|
export interface ChipProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof chipVariants> {
|
|
9
9
|
tone?: ToneValue;
|
|
10
10
|
onRemove?: () => void;
|
|
11
|
+
/** Accessible name of the remove button. Defaults to the localised "Remove". */
|
|
12
|
+
removeLabel?: string;
|
|
11
13
|
}
|
|
12
14
|
export declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -32,7 +32,7 @@ const chipVariants = cva(
|
|
|
32
32
|
}
|
|
33
33
|
);
|
|
34
34
|
const Chip = React.forwardRef(
|
|
35
|
-
({ className, size, tone = "neutral", onRemove, children, ...props }, ref) => {
|
|
35
|
+
({ className, size, tone = "neutral", onRemove, removeLabel, children, ...props }, ref) => {
|
|
36
36
|
const messages = useNexubMessages();
|
|
37
37
|
return /* @__PURE__ */ jsxs(
|
|
38
38
|
"span",
|
|
@@ -46,7 +46,7 @@ const Chip = React.forwardRef(
|
|
|
46
46
|
"button",
|
|
47
47
|
{
|
|
48
48
|
type: "button",
|
|
49
|
-
"aria-label": messages.remove,
|
|
49
|
+
"aria-label": removeLabel ?? messages.remove,
|
|
50
50
|
onClick: (e) => {
|
|
51
51
|
e.stopPropagation();
|
|
52
52
|
onRemove();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Command } from 'cmdk';
|
|
3
|
+
export interface ComboboxPopoverProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
/** Rendered inside the field, before the input (MultiSelect chips). */
|
|
7
|
+
before?: React.ReactNode;
|
|
8
|
+
input: React.ComponentProps<typeof Command.Input>;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** The list (a Combobox.List). */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A field that keeps focus in its input while a list opens below it.
|
|
15
|
+
* The list is not portalled: cmdk finds its items inside the Command root.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ComboboxPopover({ open, onOpenChange, before, input, disabled, children }: ComboboxPopoverProps): React.JSX.Element;
|
|
18
|
+
/** Three placeholder rows while options load; the word "Loading" is read out. */
|
|
19
|
+
export declare function ComboboxLoading(): React.JSX.Element;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RadixPopover from "@radix-ui/react-popover";
|
|
5
|
+
import { Command } from "cmdk";
|
|
6
|
+
import { cn } from "../../utils/cn.js";
|
|
7
|
+
import { floatingSurface } from "../../utils/surface.js";
|
|
8
|
+
import { field, fieldDisabled, fieldFocusWithin } from "../../utils/control.js";
|
|
9
|
+
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
|
|
10
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
11
|
+
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
12
|
+
function ComboboxPopover({ open, onOpenChange, before, input, disabled, children }) {
|
|
13
|
+
const anchorRef = React.useRef(null);
|
|
14
|
+
const inputRef = React.useRef(null);
|
|
15
|
+
const labelledBy = input["aria-labelledby"];
|
|
16
|
+
useIsomorphicLayoutEffect(() => {
|
|
17
|
+
inputRef.current?.setAttribute("aria-expanded", String(open));
|
|
18
|
+
if (labelledBy) inputRef.current?.setAttribute("aria-labelledby", labelledBy);
|
|
19
|
+
});
|
|
20
|
+
return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange, children: [
|
|
21
|
+
/* @__PURE__ */ jsx(RadixPopover.Anchor, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
22
|
+
"div",
|
|
23
|
+
{
|
|
24
|
+
ref: anchorRef,
|
|
25
|
+
className: cn(
|
|
26
|
+
field,
|
|
27
|
+
fieldFocusWithin,
|
|
28
|
+
"flex min-h-control-md w-full flex-wrap items-center gap-1.5 px-3 py-1",
|
|
29
|
+
disabled && fieldDisabled
|
|
30
|
+
),
|
|
31
|
+
onPointerDown: (e) => {
|
|
32
|
+
if (disabled) return;
|
|
33
|
+
if (e.target.closest("button")) return;
|
|
34
|
+
if (e.target !== inputRef.current) e.preventDefault();
|
|
35
|
+
inputRef.current?.focus();
|
|
36
|
+
onOpenChange(true);
|
|
37
|
+
},
|
|
38
|
+
children: [
|
|
39
|
+
before,
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
Command.Input,
|
|
42
|
+
{
|
|
43
|
+
ref: inputRef,
|
|
44
|
+
disabled,
|
|
45
|
+
...input,
|
|
46
|
+
onKeyDown: (e) => {
|
|
47
|
+
if (e.key === "ArrowDown" && !open) onOpenChange(true);
|
|
48
|
+
input.onKeyDown?.(e);
|
|
49
|
+
},
|
|
50
|
+
className: cn(
|
|
51
|
+
"min-w-8 flex-1 bg-transparent text-body-md outline-none placeholder:text-[var(--color-on-surface-muted)]",
|
|
52
|
+
input.className
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
) }),
|
|
59
|
+
/* @__PURE__ */ jsx(
|
|
60
|
+
RadixPopover.Content,
|
|
61
|
+
{
|
|
62
|
+
align: "start",
|
|
63
|
+
sideOffset: 4,
|
|
64
|
+
onOpenAutoFocus: (e) => e.preventDefault(),
|
|
65
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
66
|
+
onInteractOutside: (e) => {
|
|
67
|
+
if (anchorRef.current?.contains(e.target)) e.preventDefault();
|
|
68
|
+
},
|
|
69
|
+
className: cn(
|
|
70
|
+
"z-[var(--z-popover)] w-[var(--radix-popover-trigger-width)] overflow-hidden p-0 outline-none",
|
|
71
|
+
floatingSurface,
|
|
72
|
+
"nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
|
|
73
|
+
),
|
|
74
|
+
children
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
] });
|
|
78
|
+
}
|
|
79
|
+
function ComboboxLoading() {
|
|
80
|
+
const messages = useNexubMessages();
|
|
81
|
+
return /* @__PURE__ */ jsxs("div", { role: "status", "aria-busy": "true", className: "flex flex-col gap-2 px-2.5 py-2", children: [
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.loading }),
|
|
83
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "w-3/4", "aria-hidden": "true" }),
|
|
84
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "w-1/2", "aria-hidden": "true" }),
|
|
85
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "w-2/3", "aria-hidden": "true" })
|
|
86
|
+
] });
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
ComboboxLoading,
|
|
90
|
+
ComboboxPopover
|
|
91
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface MultiSelectOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MultiSelectProps {
|
|
7
|
+
options: MultiSelectOption[];
|
|
8
|
+
value?: string[];
|
|
9
|
+
defaultValue?: string[];
|
|
10
|
+
onValueChange?: (value: string[]) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
emptyText?: string;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
/** Controlled search text. */
|
|
15
|
+
inputValue?: string;
|
|
16
|
+
/** Called as the user types; options are then shown as given (no built-in filtering). */
|
|
17
|
+
onInputChange?: (text: string) => void;
|
|
18
|
+
/** Adds a "Create “text”" option when the text matches no option exactly. */
|
|
19
|
+
onCreate?: (text: string) => void;
|
|
20
|
+
/** Chips shown before the rest collapse into "+N more". Default 3. */
|
|
21
|
+
maxVisibleChips?: number;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
'aria-label'?: string;
|
|
26
|
+
'aria-labelledby'?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pick several options: chips in the field, a filterable list below.
|
|
30
|
+
* Backspace in an empty field removes the last chip.
|
|
31
|
+
*/
|
|
32
|
+
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, placeholder, emptyText, loading, inputValue, onInputChange, onCreate, maxVisibleChips, disabled, className, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }: MultiSelectProps): React.JSX.Element;
|
|
33
|
+
export declare namespace MultiSelect {
|
|
34
|
+
var displayName: string;
|
|
35
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, Plus } from "@nexub/icons";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
6
|
+
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
7
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
8
|
+
import { Chip } from "../Chip/Chip.js";
|
|
9
|
+
import { Combobox } from "../Combobox/Combobox.js";
|
|
10
|
+
import { ComboboxLoading, ComboboxPopover } from "../Combobox/ComboboxPopover.js";
|
|
11
|
+
function MultiSelect({
|
|
12
|
+
options,
|
|
13
|
+
value: valueProp,
|
|
14
|
+
defaultValue,
|
|
15
|
+
onValueChange,
|
|
16
|
+
placeholder,
|
|
17
|
+
emptyText,
|
|
18
|
+
loading = false,
|
|
19
|
+
inputValue,
|
|
20
|
+
onInputChange,
|
|
21
|
+
onCreate,
|
|
22
|
+
maxVisibleChips = 3,
|
|
23
|
+
disabled,
|
|
24
|
+
className,
|
|
25
|
+
id,
|
|
26
|
+
"aria-label": ariaLabel,
|
|
27
|
+
"aria-labelledby": ariaLabelledBy
|
|
28
|
+
}) {
|
|
29
|
+
const messages = useNexubMessages();
|
|
30
|
+
const [value = [], setValue] = useControllableState({
|
|
31
|
+
value: valueProp,
|
|
32
|
+
defaultValue: defaultValue ?? [],
|
|
33
|
+
onChange: onValueChange
|
|
34
|
+
});
|
|
35
|
+
const [innerSearch, setInnerSearch] = React.useState("");
|
|
36
|
+
const [open, setOpen] = React.useState(false);
|
|
37
|
+
const search = inputValue ?? innerSearch;
|
|
38
|
+
const setSearch = (text2) => {
|
|
39
|
+
setInnerSearch(text2);
|
|
40
|
+
onInputChange?.(text2);
|
|
41
|
+
};
|
|
42
|
+
const [labels, setLabels] = React.useState(() => /* @__PURE__ */ new Map());
|
|
43
|
+
const missing = options.some((o) => labels.get(o.value) !== o.label);
|
|
44
|
+
if (missing) setLabels(new Map([...labels, ...options.map((o) => [o.value, o.label])]));
|
|
45
|
+
const labelOf = (v) => labels.get(v) ?? options.find((o) => o.value === v)?.label ?? v;
|
|
46
|
+
const toggle = (v) => setValue(value.includes(v) ? value.filter((x) => x !== v) : [...value, v]);
|
|
47
|
+
const remove = (v) => setValue(value.filter((x) => x !== v));
|
|
48
|
+
const text = search.trim();
|
|
49
|
+
const canCreate = !!onCreate && !loading && text !== "" && !options.some((o) => o.label.toLowerCase() === text.toLowerCase());
|
|
50
|
+
const hiddenCount = Math.max(0, value.length - maxVisibleChips);
|
|
51
|
+
const chips = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
52
|
+
value.slice(0, maxVisibleChips).map((v) => /* @__PURE__ */ jsx(
|
|
53
|
+
Chip,
|
|
54
|
+
{
|
|
55
|
+
size: "sm",
|
|
56
|
+
onRemove: disabled ? void 0 : () => remove(v),
|
|
57
|
+
removeLabel: messages.removeOption(labelOf(v)),
|
|
58
|
+
children: labelOf(v)
|
|
59
|
+
},
|
|
60
|
+
v
|
|
61
|
+
)),
|
|
62
|
+
hiddenCount > 0 && /* @__PURE__ */ jsxs(Chip, { size: "sm", children: [
|
|
63
|
+
"+",
|
|
64
|
+
messages.moreCount(hiddenCount)
|
|
65
|
+
] })
|
|
66
|
+
] });
|
|
67
|
+
return /* @__PURE__ */ jsx(
|
|
68
|
+
Combobox.Root,
|
|
69
|
+
{
|
|
70
|
+
label: ariaLabel,
|
|
71
|
+
className: cn("overflow-visible rounded-none bg-transparent", className),
|
|
72
|
+
shouldFilter: !onInputChange,
|
|
73
|
+
children: /* @__PURE__ */ jsx(
|
|
74
|
+
ComboboxPopover,
|
|
75
|
+
{
|
|
76
|
+
open: open && !disabled,
|
|
77
|
+
onOpenChange: setOpen,
|
|
78
|
+
before: chips,
|
|
79
|
+
disabled,
|
|
80
|
+
input: {
|
|
81
|
+
id,
|
|
82
|
+
"aria-label": ariaLabel,
|
|
83
|
+
"aria-labelledby": ariaLabelledBy,
|
|
84
|
+
placeholder: value.length ? void 0 : placeholder ?? messages.searchPlaceholder,
|
|
85
|
+
value: search,
|
|
86
|
+
onValueChange: (t) => {
|
|
87
|
+
setSearch(t);
|
|
88
|
+
setOpen(true);
|
|
89
|
+
},
|
|
90
|
+
onKeyDown: (e) => {
|
|
91
|
+
if (e.key === "Backspace" && search === "" && value.length) remove(value[value.length - 1]);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
children: /* @__PURE__ */ jsx(Combobox.List, { "aria-multiselectable": "true", children: loading ? /* @__PURE__ */ jsx(ComboboxLoading, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
95
|
+
!canCreate && /* @__PURE__ */ jsx(Combobox.Empty, { children: emptyText ?? messages.noResults }),
|
|
96
|
+
options.map((opt) => {
|
|
97
|
+
const checked = value.includes(opt.value);
|
|
98
|
+
return /* @__PURE__ */ jsxs(
|
|
99
|
+
Combobox.Item,
|
|
100
|
+
{
|
|
101
|
+
value: opt.label,
|
|
102
|
+
keywords: [opt.value],
|
|
103
|
+
"aria-checked": checked,
|
|
104
|
+
onSelect: () => {
|
|
105
|
+
toggle(opt.value);
|
|
106
|
+
if (search) setSearch("");
|
|
107
|
+
},
|
|
108
|
+
className: "gap-2",
|
|
109
|
+
children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Check, { size: "sm", "aria-hidden": "true", className: cn(!checked && "invisible") }),
|
|
111
|
+
opt.label
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
opt.value
|
|
115
|
+
);
|
|
116
|
+
}),
|
|
117
|
+
canCreate && /* @__PURE__ */ jsxs(
|
|
118
|
+
Combobox.Item,
|
|
119
|
+
{
|
|
120
|
+
forceMount: true,
|
|
121
|
+
value: "\0",
|
|
122
|
+
onSelect: () => {
|
|
123
|
+
onCreate(text);
|
|
124
|
+
setSearch("");
|
|
125
|
+
},
|
|
126
|
+
className: "gap-2",
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
|
|
129
|
+
messages.createOption(text)
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
] }) })
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
MultiSelect.displayName = "MultiSelect";
|
|
140
|
+
export {
|
|
141
|
+
MultiSelect
|
|
142
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,8 @@ export { Select } from './components/Select/index.js';
|
|
|
160
160
|
export { Combobox } from './components/Combobox/index.js';
|
|
161
161
|
export { Autocomplete } from './components/Autocomplete/index.js';
|
|
162
162
|
export type { AutocompleteOption, AutocompleteProps } from './components/Autocomplete/index.js';
|
|
163
|
+
export { MultiSelect } from './components/MultiSelect/index.js';
|
|
164
|
+
export type { MultiSelectOption, MultiSelectProps } from './components/MultiSelect/index.js';
|
|
163
165
|
export { Switch } from './components/Switch/index.js';
|
|
164
166
|
export type { SwitchProps } from './components/Switch/index.js';
|
|
165
167
|
export { Checkbox } from './components/Checkbox/index.js';
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,7 @@ import { Drawer, Sheet } from "./components/Drawer/index.js";
|
|
|
89
89
|
import { Select } from "./components/Select/index.js";
|
|
90
90
|
import { Combobox } from "./components/Combobox/index.js";
|
|
91
91
|
import { Autocomplete } from "./components/Autocomplete/index.js";
|
|
92
|
+
import { MultiSelect } from "./components/MultiSelect/index.js";
|
|
92
93
|
import { Switch } from "./components/Switch/index.js";
|
|
93
94
|
import { Checkbox } from "./components/Checkbox/index.js";
|
|
94
95
|
import { RadioGroup } from "./components/RadioGroup/index.js";
|
|
@@ -201,6 +202,7 @@ export {
|
|
|
201
202
|
MessageActions,
|
|
202
203
|
MessageBubble,
|
|
203
204
|
MoneyInput,
|
|
205
|
+
MultiSelect,
|
|
204
206
|
NavigationMenu,
|
|
205
207
|
NavigationRail,
|
|
206
208
|
NexubProvider,
|