@nexub/react 5.1.0 → 5.3.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/DataTable/DataTable.d.ts +24 -2
- package/dist/components/DataTable/DataTable.js +146 -14
- 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.3.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
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { type ColumnDef, type RowData } from '@tanstack/react-table';
|
|
2
|
+
import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState } from '@tanstack/react-table';
|
|
3
3
|
declare module '@tanstack/react-table' {
|
|
4
4
|
interface ColumnMeta<TData extends RowData, TValue> {
|
|
5
5
|
/** Horizontal alignment of the header and cells. Use 'end' for numbers. @default 'start' */
|
|
@@ -8,6 +8,7 @@ declare module '@tanstack/react-table' {
|
|
|
8
8
|
}
|
|
9
9
|
/** A DataTable column: TanStack's `ColumnDef`, with `meta.align` for numeric columns. */
|
|
10
10
|
export type DataTableColumn<TData> = ColumnDef<TData, unknown>;
|
|
11
|
+
export type DataTableDensity = 'comfortable' | 'compact';
|
|
11
12
|
export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
|
12
13
|
columns: DataTableColumn<TData>[];
|
|
13
14
|
/** Names the table for assistive tech (and is shown below it unless `captionHidden`). */
|
|
@@ -17,8 +18,29 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
|
|
|
17
18
|
data: TData[];
|
|
18
19
|
enableSorting?: boolean;
|
|
19
20
|
enableRowSelection?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* With `enableRowSelection`: a checkbox column with select-page in its
|
|
23
|
+
* header, and "Select all N" across pages. On by default from 6.0.
|
|
24
|
+
*/
|
|
25
|
+
selectionColumn?: boolean;
|
|
26
|
+
/** Controlled selection, keyed by row id (see `getRowId`). */
|
|
27
|
+
rowSelection?: RowSelectionState;
|
|
28
|
+
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
|
29
|
+
/** Stable row ids for selection; defaults to the row index. */
|
|
30
|
+
getRowId?: (row: TData, index: number) => string;
|
|
31
|
+
/** A floating bar while rows are selected: render your actions for the selected rows. */
|
|
32
|
+
bulkActions?: (rows: TData[], clear: () => void) => React.ReactNode;
|
|
33
|
+
density?: DataTableDensity;
|
|
34
|
+
defaultDensity?: DataTableDensity;
|
|
35
|
+
onDensityChange?: (density: DataTableDensity) => void;
|
|
36
|
+
columnVisibility?: VisibilityState;
|
|
37
|
+
onColumnVisibilityChange?: (visibility: VisibilityState) => void;
|
|
38
|
+
/** A "View" menu above the table: density and which columns show. */
|
|
39
|
+
viewOptions?: boolean;
|
|
40
|
+
/** Content at the start of the row above the table (filters, a title). */
|
|
41
|
+
toolbar?: React.ReactNode;
|
|
20
42
|
pagination?: {
|
|
21
43
|
pageSize?: number;
|
|
22
44
|
};
|
|
23
45
|
}
|
|
24
|
-
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
46
|
+
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import {
|
|
5
5
|
useReactTable,
|
|
@@ -9,11 +9,16 @@ import {
|
|
|
9
9
|
getPaginationRowModel,
|
|
10
10
|
flexRender
|
|
11
11
|
} from "@tanstack/react-table";
|
|
12
|
-
import { ArrowUp, ArrowDown, ChevronsUpDown } from "@nexub/icons";
|
|
12
|
+
import { ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
|
|
13
13
|
import { cn } from "../../utils/cn.js";
|
|
14
14
|
import { disabledContainer, pressable } from "../../utils/control.js";
|
|
15
|
-
import {
|
|
15
|
+
import { floatingSurface } from "../../utils/surface.js";
|
|
16
|
+
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
17
|
+
import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
16
18
|
import { Table } from "../Table/Table.js";
|
|
19
|
+
import { Checkbox } from "../Checkbox/Checkbox.js";
|
|
20
|
+
import { Button } from "../Button/Button.js";
|
|
21
|
+
import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
|
|
17
22
|
const alignText = {
|
|
18
23
|
start: "text-start",
|
|
19
24
|
end: "text-end",
|
|
@@ -47,7 +52,7 @@ const TableRow = React.forwardRef(
|
|
|
47
52
|
{
|
|
48
53
|
ref,
|
|
49
54
|
className: cn(
|
|
50
|
-
"border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[var(--color-
|
|
55
|
+
"border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[color-mix(in_srgb,var(--color-primary)_10%,transparent)]",
|
|
51
56
|
className
|
|
52
57
|
),
|
|
53
58
|
...props
|
|
@@ -79,11 +84,25 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
|
|
|
79
84
|
}
|
|
80
85
|
));
|
|
81
86
|
TableCell.displayName = "DataTable.TableCell";
|
|
87
|
+
const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
|
|
88
|
+
const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
|
|
82
89
|
function DataTable({
|
|
83
90
|
columns,
|
|
84
91
|
data,
|
|
85
92
|
enableSorting = false,
|
|
86
93
|
enableRowSelection = false,
|
|
94
|
+
selectionColumn = false,
|
|
95
|
+
rowSelection: rowSelectionProp,
|
|
96
|
+
onRowSelectionChange,
|
|
97
|
+
getRowId,
|
|
98
|
+
bulkActions,
|
|
99
|
+
density: densityProp,
|
|
100
|
+
defaultDensity = "comfortable",
|
|
101
|
+
onDensityChange,
|
|
102
|
+
columnVisibility: columnVisibilityProp,
|
|
103
|
+
onColumnVisibilityChange,
|
|
104
|
+
viewOptions = false,
|
|
105
|
+
toolbar,
|
|
87
106
|
pagination,
|
|
88
107
|
caption,
|
|
89
108
|
captionHidden = false,
|
|
@@ -93,26 +112,73 @@ function DataTable({
|
|
|
93
112
|
...props
|
|
94
113
|
}) {
|
|
95
114
|
const messages = useNexubMessages();
|
|
115
|
+
const { locale } = useLocale();
|
|
96
116
|
const [sorting, setSorting] = React.useState([]);
|
|
97
|
-
const [rowSelection, setRowSelection] =
|
|
117
|
+
const [rowSelection = {}, setRowSelection] = useControllableState({
|
|
118
|
+
value: rowSelectionProp,
|
|
119
|
+
defaultValue: {},
|
|
120
|
+
onChange: onRowSelectionChange
|
|
121
|
+
});
|
|
122
|
+
const [density = "comfortable", setDensity] = useControllableState({
|
|
123
|
+
value: densityProp,
|
|
124
|
+
defaultValue: defaultDensity,
|
|
125
|
+
onChange: onDensityChange
|
|
126
|
+
});
|
|
127
|
+
const [columnVisibility = {}, setColumnVisibility] = useControllableState({
|
|
128
|
+
value: columnVisibilityProp,
|
|
129
|
+
defaultValue: {},
|
|
130
|
+
onChange: onColumnVisibilityChange
|
|
131
|
+
});
|
|
98
132
|
const [paginationState, setPaginationState] = React.useState({
|
|
99
133
|
pageIndex: 0,
|
|
100
134
|
pageSize: pagination?.pageSize ?? 20
|
|
101
135
|
});
|
|
136
|
+
const withSelectColumn = enableRowSelection && selectionColumn;
|
|
137
|
+
const allColumns = React.useMemo(
|
|
138
|
+
() => withSelectColumn ? [
|
|
139
|
+
{
|
|
140
|
+
id: "__select",
|
|
141
|
+
enableSorting: false,
|
|
142
|
+
enableHiding: false,
|
|
143
|
+
header: ({ table: table2 }) => /* @__PURE__ */ jsx(
|
|
144
|
+
Checkbox,
|
|
145
|
+
{
|
|
146
|
+
"aria-label": messages.selectAllOnPage,
|
|
147
|
+
checked: table2.getIsAllPageRowsSelected() ? true : table2.getIsSomePageRowsSelected() ? "indeterminate" : false,
|
|
148
|
+
onCheckedChange: (v) => table2.toggleAllPageRowsSelected(v === true)
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(
|
|
152
|
+
Checkbox,
|
|
153
|
+
{
|
|
154
|
+
"aria-label": messages.selectRow,
|
|
155
|
+
checked: row.getIsSelected(),
|
|
156
|
+
disabled: !row.getCanSelect(),
|
|
157
|
+
onCheckedChange: (v) => row.toggleSelected(v === true)
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
...columns
|
|
162
|
+
] : columns,
|
|
163
|
+
[withSelectColumn, columns, messages.selectAllOnPage, messages.selectRow]
|
|
164
|
+
);
|
|
102
165
|
const tableState = React.useMemo(
|
|
103
166
|
() => ({
|
|
104
167
|
sorting: enableSorting ? sorting : void 0,
|
|
105
168
|
rowSelection: enableRowSelection ? rowSelection : void 0,
|
|
106
|
-
pagination: pagination ? paginationState : void 0
|
|
169
|
+
pagination: pagination ? paginationState : void 0,
|
|
170
|
+
columnVisibility
|
|
107
171
|
}),
|
|
108
|
-
[enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState]
|
|
172
|
+
[enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState, columnVisibility]
|
|
109
173
|
);
|
|
110
174
|
const table = useReactTable({
|
|
111
175
|
data,
|
|
112
|
-
columns,
|
|
176
|
+
columns: allColumns,
|
|
113
177
|
state: tableState,
|
|
178
|
+
getRowId,
|
|
114
179
|
onSortingChange: enableSorting ? setSorting : void 0,
|
|
115
|
-
onRowSelectionChange: enableRowSelection ? setRowSelection : void 0,
|
|
180
|
+
onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
|
|
181
|
+
onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
|
|
116
182
|
onPaginationChange: pagination ? setPaginationState : void 0,
|
|
117
183
|
getCoreRowModel: getCoreRowModel(),
|
|
118
184
|
getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
|
|
@@ -121,7 +187,49 @@ function DataTable({
|
|
|
121
187
|
enableRowSelection,
|
|
122
188
|
manualPagination: false
|
|
123
189
|
});
|
|
124
|
-
|
|
190
|
+
const compact = density === "compact";
|
|
191
|
+
const selectedRows = enableRowSelection ? table.getSelectedRowModel().rows : [];
|
|
192
|
+
const totalRows = table.getCoreRowModel().rows.length;
|
|
193
|
+
const fmt = (n) => new Intl.NumberFormat(locale).format(n);
|
|
194
|
+
const multiSort = sorting.length > 1;
|
|
195
|
+
const offerAll = withSelectColumn && !!pagination && table.getIsAllPageRowsSelected() && totalRows > table.getRowModel().rows.length;
|
|
196
|
+
const clear = () => table.resetRowSelection(true);
|
|
197
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
|
|
198
|
+
(toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
199
|
+
toolbar,
|
|
200
|
+
viewOptions && /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
|
|
201
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
|
|
202
|
+
/* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
|
|
203
|
+
messages.view
|
|
204
|
+
] }) }),
|
|
205
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
|
|
206
|
+
/* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
|
|
207
|
+
/* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => setDensity(v), children: [
|
|
208
|
+
/* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
|
|
209
|
+
/* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
|
|
210
|
+
] }),
|
|
211
|
+
/* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
212
|
+
/* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
|
|
213
|
+
table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => /* @__PURE__ */ jsx(
|
|
214
|
+
DropdownMenu.CheckboxItem,
|
|
215
|
+
{
|
|
216
|
+
checked: c.getIsVisible(),
|
|
217
|
+
onCheckedChange: (v) => c.toggleVisibility(v === true),
|
|
218
|
+
onSelect: (e) => e.preventDefault(),
|
|
219
|
+
children: columnLabel(c)
|
|
220
|
+
},
|
|
221
|
+
c.id
|
|
222
|
+
))
|
|
223
|
+
] })
|
|
224
|
+
] })
|
|
225
|
+
] }),
|
|
226
|
+
offerAll && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 rounded-shape-container bg-[var(--color-surface-container-low)] px-4 py-2 text-body-sm", children: table.getIsAllRowsSelected() ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
227
|
+
/* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(totalRows)) }),
|
|
228
|
+
/* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: clear, children: messages.clearSelection })
|
|
229
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
230
|
+
/* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(selectedRows.length)) }),
|
|
231
|
+
/* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: () => table.toggleAllRowsSelected(true), children: messages.selectAllRows(fmt(totalRows)) })
|
|
232
|
+
] }) }),
|
|
125
233
|
/* @__PURE__ */ jsxs(TableRoot, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [
|
|
126
234
|
caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
|
|
127
235
|
/* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
|
|
@@ -129,7 +237,7 @@ function DataTable({
|
|
|
129
237
|
return /* @__PURE__ */ jsx(
|
|
130
238
|
TableHead,
|
|
131
239
|
{
|
|
132
|
-
className: alignText[align],
|
|
240
|
+
className: cn(alignText[align], compact && "h-8", header.column.id === "__select" && "w-10"),
|
|
133
241
|
"aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
|
|
134
242
|
children: header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
|
|
135
243
|
"button",
|
|
@@ -142,6 +250,10 @@ function DataTable({
|
|
|
142
250
|
),
|
|
143
251
|
children: [
|
|
144
252
|
flexRender(header.column.columnDef.header, header.getContext()),
|
|
253
|
+
multiSort && header.column.getIsSorted() && /* @__PURE__ */ jsxs("span", { className: "text-[10px] tabular-nums leading-none", children: [
|
|
254
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: header.column.getSortIndex() + 1 }),
|
|
255
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.sortOrder(header.column.getSortIndex() + 1) })
|
|
256
|
+
] }),
|
|
145
257
|
header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
|
|
146
258
|
]
|
|
147
259
|
}
|
|
@@ -157,7 +269,7 @@ function DataTable({
|
|
|
157
269
|
children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
|
|
158
270
|
TableCell,
|
|
159
271
|
{
|
|
160
|
-
className: alignText[cell.column.columnDef.meta?.align ?? "start"],
|
|
272
|
+
className: cn(alignText[cell.column.columnDef.meta?.align ?? "start"], compact && "py-1.5"),
|
|
161
273
|
children: flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
162
274
|
},
|
|
163
275
|
cell.id
|
|
@@ -167,7 +279,7 @@ function DataTable({
|
|
|
167
279
|
)) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
168
280
|
TableCell,
|
|
169
281
|
{
|
|
170
|
-
colSpan:
|
|
282
|
+
colSpan: table.getVisibleLeafColumns().length,
|
|
171
283
|
className: "h-24 text-center text-[var(--color-on-surface-muted)]",
|
|
172
284
|
children: messages.noResults
|
|
173
285
|
}
|
|
@@ -197,7 +309,27 @@ function DataTable({
|
|
|
197
309
|
}
|
|
198
310
|
)
|
|
199
311
|
] })
|
|
200
|
-
] })
|
|
312
|
+
] }),
|
|
313
|
+
bulkActions && selectedRows.length > 0 && /* @__PURE__ */ jsxs(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
role: "region",
|
|
317
|
+
"aria-label": messages.selectedCount(fmt(selectedRows.length)),
|
|
318
|
+
className: cn(
|
|
319
|
+
floatingSurface,
|
|
320
|
+
"nx-motion sticky bottom-4 z-10 mx-auto flex w-fit max-w-full flex-wrap items-center gap-3 px-4 py-2"
|
|
321
|
+
),
|
|
322
|
+
"data-state": "open",
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "text-label-lg tabular-nums", children: messages.selectedCount(fmt(selectedRows.length)) }),
|
|
325
|
+
bulkActions(
|
|
326
|
+
selectedRows.map((r) => r.original),
|
|
327
|
+
clear
|
|
328
|
+
),
|
|
329
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
)
|
|
201
333
|
] });
|
|
202
334
|
}
|
|
203
335
|
export {
|
|
@@ -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
|
+
}
|