@nexub/react 6.2.0 → 6.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 +19 -1
- package/dist/components/DataTable/DataTable.d.ts +11 -2
- package/dist/components/DataTable/DataTable.js +51 -4
- package/dist/components/TreeView/TreeView.d.ts +38 -0
- package/dist/components/TreeView/TreeView.js +253 -0
- package/dist/components/TreeView/index.d.ts +2 -0
- package/dist/components/TreeView/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": "6.
|
|
2
|
+
"version": "6.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, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
|
|
@@ -1204,6 +1204,24 @@
|
|
|
1204
1204
|
"keyboard": [],
|
|
1205
1205
|
"screenReader": []
|
|
1206
1206
|
},
|
|
1207
|
+
{
|
|
1208
|
+
"slug": "tree-view",
|
|
1209
|
+
"name": "Tree View",
|
|
1210
|
+
"package": "@nexub/react",
|
|
1211
|
+
"automated": "pass",
|
|
1212
|
+
"exceptions": [],
|
|
1213
|
+
"keyboard": [
|
|
1214
|
+
"ArrowDown / ArrowUp: Next or previous visible item.",
|
|
1215
|
+
"ArrowRight: Opens a closed item, or moves to its first child. Mirrored in RTL.",
|
|
1216
|
+
"ArrowLeft: Closes an open item, or moves to its parent. Mirrored in RTL.",
|
|
1217
|
+
"Home / End: First or last visible item.",
|
|
1218
|
+
"Enter / Space: Selects the item, or toggles its checkbox.",
|
|
1219
|
+
"Type a letter: Moves to the next item starting with the typed text."
|
|
1220
|
+
],
|
|
1221
|
+
"screenReader": [
|
|
1222
|
+
"The tree is one tab stop (roving tabindex); it returns to the last focused or the selected item."
|
|
1223
|
+
]
|
|
1224
|
+
},
|
|
1207
1225
|
{
|
|
1208
1226
|
"slug": "activity-feed",
|
|
1209
1227
|
"name": "ActivityFeed",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState } from '@tanstack/react-table';
|
|
2
|
+
import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState, type ExpandedState } 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' */
|
|
@@ -75,5 +75,14 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
|
|
|
75
75
|
pagination?: {
|
|
76
76
|
pageSize?: number;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Nested rows (a tree table): return a row's children. The first column gets
|
|
80
|
+
* an expand toggle and indents each level. Sorting sorts within each level.
|
|
81
|
+
*/
|
|
82
|
+
getSubRows?: (row: TData, index: number) => TData[] | undefined;
|
|
83
|
+
/** Expanded rows by id, or `true` for all. */
|
|
84
|
+
expanded?: ExpandedState;
|
|
85
|
+
defaultExpanded?: ExpandedState;
|
|
86
|
+
onExpandedChange?: (expanded: ExpandedState) => void;
|
|
78
87
|
}
|
|
79
|
-
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
88
|
+
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
getSortedRowModel,
|
|
8
8
|
getFilteredRowModel,
|
|
9
9
|
getPaginationRowModel,
|
|
10
|
+
getExpandedRowModel,
|
|
10
11
|
flexRender
|
|
11
12
|
} from "@tanstack/react-table";
|
|
12
|
-
import { ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
|
|
13
|
+
import { ArrowUp, ArrowDown, ChevronDown, ChevronRight, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
|
|
13
14
|
import { cn } from "../../utils/cn.js";
|
|
14
15
|
import { disabledContainer, pressable } from "../../utils/control.js";
|
|
15
16
|
import { floatingSurface } from "../../utils/surface.js";
|
|
@@ -102,6 +103,34 @@ const useAllRows = (count) => ({
|
|
|
102
103
|
before: 0,
|
|
103
104
|
after: 0
|
|
104
105
|
});
|
|
106
|
+
function TreeCell({
|
|
107
|
+
row,
|
|
108
|
+
expandLabel,
|
|
109
|
+
collapseLabel,
|
|
110
|
+
children
|
|
111
|
+
}) {
|
|
112
|
+
const open = row.getIsExpanded();
|
|
113
|
+
const ref = React.useCallback(
|
|
114
|
+
(el) => {
|
|
115
|
+
if (el) el.style.paddingInlineStart = `${row.depth * 1.5}rem`;
|
|
116
|
+
},
|
|
117
|
+
[row.depth]
|
|
118
|
+
);
|
|
119
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: "flex items-center gap-1", children: [
|
|
120
|
+
row.getCanExpand() ? /* @__PURE__ */ jsx(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
"aria-expanded": open,
|
|
125
|
+
"aria-label": open ? collapseLabel : expandLabel,
|
|
126
|
+
onClick: row.toggleExpanded,
|
|
127
|
+
className: "-ms-1 inline-flex size-6 shrink-0 items-center justify-center rounded-shape-min text-[var(--color-on-surface-variant)] hover:bg-[var(--color-surface-container-high)]",
|
|
128
|
+
children: open ? /* @__PURE__ */ jsx(ChevronDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { size: "xs", "aria-hidden": "true", className: "rtl:-scale-x-100" })
|
|
129
|
+
}
|
|
130
|
+
) : /* @__PURE__ */ jsx("span", { className: "-ms-1 size-6 shrink-0", "aria-hidden": "true" }),
|
|
131
|
+
children
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
105
134
|
const px = (n) => typeof n === "number" ? `${n}px` : n;
|
|
106
135
|
const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
|
|
107
136
|
const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
|
|
@@ -134,6 +163,10 @@ function DataTable({
|
|
|
134
163
|
grid = false,
|
|
135
164
|
rowWindow,
|
|
136
165
|
pagination,
|
|
166
|
+
getSubRows,
|
|
167
|
+
expanded: expandedProp,
|
|
168
|
+
defaultExpanded,
|
|
169
|
+
onExpandedChange,
|
|
137
170
|
caption,
|
|
138
171
|
captionHidden = false,
|
|
139
172
|
className,
|
|
@@ -174,6 +207,11 @@ function DataTable({
|
|
|
174
207
|
defaultValue: {},
|
|
175
208
|
onChange: onColumnPinningChange
|
|
176
209
|
});
|
|
210
|
+
const [expanded = {}, setExpanded] = useControllableState({
|
|
211
|
+
value: expandedProp,
|
|
212
|
+
defaultValue: defaultExpanded ?? {},
|
|
213
|
+
onChange: onExpandedChange
|
|
214
|
+
});
|
|
177
215
|
const [announcement, setAnnouncement] = React.useState("");
|
|
178
216
|
const tableRef = React.useRef(null);
|
|
179
217
|
const gridNav = React.useMemo(() => createGridNavigation(dir), [dir]);
|
|
@@ -223,9 +261,12 @@ function DataTable({
|
|
|
223
261
|
columnVisibility,
|
|
224
262
|
columnSizing,
|
|
225
263
|
columnOrder,
|
|
226
|
-
columnPinning
|
|
264
|
+
columnPinning,
|
|
265
|
+
expanded: getSubRows ? expanded : void 0
|
|
227
266
|
}),
|
|
228
267
|
[
|
|
268
|
+
getSubRows,
|
|
269
|
+
expanded,
|
|
229
270
|
enableSorting,
|
|
230
271
|
sorting,
|
|
231
272
|
enableRowSelection,
|
|
@@ -257,6 +298,12 @@ function DataTable({
|
|
|
257
298
|
getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
|
|
258
299
|
getFilteredRowModel: getFilteredRowModel(),
|
|
259
300
|
getPaginationRowModel: pagination ? getPaginationRowModel() : void 0,
|
|
301
|
+
getSubRows,
|
|
302
|
+
getExpandedRowModel: getSubRows ? getExpandedRowModel() : void 0,
|
|
303
|
+
onExpandedChange: getSubRows ? (u) => setExpanded(apply(u, expanded)) : void 0,
|
|
304
|
+
// With pages, count top-level rows so a parent and its children stay together
|
|
305
|
+
// (TanStack then expands inside the page model; without pages it must not).
|
|
306
|
+
paginateExpandedRows: !pagination,
|
|
260
307
|
enableRowSelection,
|
|
261
308
|
manualPagination: false
|
|
262
309
|
});
|
|
@@ -466,7 +513,7 @@ function DataTable({
|
|
|
466
513
|
"data-index": i,
|
|
467
514
|
"aria-rowindex": rowWin.virtual ? i + 2 : void 0,
|
|
468
515
|
"data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
|
|
469
|
-
children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
|
|
516
|
+
children: row.getVisibleCells().map((cell, _i, cells) => /* @__PURE__ */ jsx(
|
|
470
517
|
TableCell,
|
|
471
518
|
{
|
|
472
519
|
ref: pinned || enableColumnResizing ? placeCell(cell.column) : void 0,
|
|
@@ -476,7 +523,7 @@ function DataTable({
|
|
|
476
523
|
pinClass(cell.column),
|
|
477
524
|
enableColumnResizing && "truncate"
|
|
478
525
|
),
|
|
479
|
-
children: flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
526
|
+
children: getSubRows && cell === cells.find((c) => c.column.id !== "__select") ? /* @__PURE__ */ jsx(TreeCell, { row, expandLabel: messages.expandRow, collapseLabel: messages.collapseRow, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }) : flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
480
527
|
},
|
|
481
528
|
cell.id
|
|
482
529
|
))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: React.ReactNode;
|
|
5
|
+
/** Text for type-ahead when `label` isn't a string. */
|
|
6
|
+
textValue?: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
children?: TreeNode[];
|
|
9
|
+
/** Children exist but aren't loaded yet: expanding calls `onLoadChildren`. */
|
|
10
|
+
hasChildren?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface TreeViewProps {
|
|
14
|
+
nodes: TreeNode[];
|
|
15
|
+
'aria-label'?: string;
|
|
16
|
+
'aria-labelledby'?: string;
|
|
17
|
+
expanded?: string[];
|
|
18
|
+
defaultExpanded?: string[];
|
|
19
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
20
|
+
/** Single selection (ignored when `checkable`). */
|
|
21
|
+
selected?: string | null;
|
|
22
|
+
defaultSelected?: string | null;
|
|
23
|
+
onSelectedChange?: (id: string | null) => void;
|
|
24
|
+
/** Checkboxes with tri-state parents; `checked` lists every checked id, parents included. */
|
|
25
|
+
checkable?: boolean;
|
|
26
|
+
checked?: string[];
|
|
27
|
+
defaultChecked?: string[];
|
|
28
|
+
onCheckedChange?: (ids: string[]) => void;
|
|
29
|
+
/** Load a `hasChildren` node's children, then pass them in through `nodes`. */
|
|
30
|
+
onLoadChildren?: (node: TreeNode) => Promise<unknown>;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A hierarchy people can browse with the keyboard (ARIA tree): Arrow keys move
|
|
35
|
+
* and expand, Home/End jump, typing finds an item. Optional lazy children and
|
|
36
|
+
* tri-state checkboxes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function TreeView({ nodes, expanded: expandedProp, defaultExpanded, onExpandedChange, selected: selectedProp, defaultSelected, onSelectedChange, checkable, checked: checkedProp, defaultChecked, onCheckedChange, onLoadChildren, className, ...aria }: TreeViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, ChevronRight, Minus } from "@nexub/icons";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
6
|
+
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
7
|
+
import { Spinner } from "../Spinner/Spinner.js";
|
|
8
|
+
const hasKids = (n) => Boolean(n.children?.length || n.hasChildren && !n.children);
|
|
9
|
+
const text = (n) => n.textValue ?? (typeof n.label === "string" ? n.label : "");
|
|
10
|
+
const descendants = (n) => (n.children ?? []).flatMap((c) => [c.id, ...descendants(c)]);
|
|
11
|
+
function TreeView({
|
|
12
|
+
nodes,
|
|
13
|
+
expanded: expandedProp,
|
|
14
|
+
defaultExpanded,
|
|
15
|
+
onExpandedChange,
|
|
16
|
+
selected: selectedProp,
|
|
17
|
+
defaultSelected,
|
|
18
|
+
onSelectedChange,
|
|
19
|
+
checkable = false,
|
|
20
|
+
checked: checkedProp,
|
|
21
|
+
defaultChecked,
|
|
22
|
+
onCheckedChange,
|
|
23
|
+
onLoadChildren,
|
|
24
|
+
className,
|
|
25
|
+
...aria
|
|
26
|
+
}) {
|
|
27
|
+
const [expanded = [], setExpanded] = useControllableState({
|
|
28
|
+
value: expandedProp,
|
|
29
|
+
defaultValue: defaultExpanded ?? [],
|
|
30
|
+
onChange: onExpandedChange
|
|
31
|
+
});
|
|
32
|
+
const [selected = null, setSelected] = useControllableState({
|
|
33
|
+
value: selectedProp,
|
|
34
|
+
defaultValue: defaultSelected ?? null,
|
|
35
|
+
onChange: onSelectedChange
|
|
36
|
+
});
|
|
37
|
+
const [checked = [], setChecked] = useControllableState({
|
|
38
|
+
value: checkedProp,
|
|
39
|
+
defaultValue: defaultChecked ?? [],
|
|
40
|
+
onChange: onCheckedChange
|
|
41
|
+
});
|
|
42
|
+
const [loading, setLoading] = React.useState(null);
|
|
43
|
+
const [focused, setFocused] = React.useState(null);
|
|
44
|
+
const refs = React.useRef(/* @__PURE__ */ new Map());
|
|
45
|
+
const typed = React.useRef({ text: "", at: 0 });
|
|
46
|
+
const open = React.useMemo(() => new Set(expanded), [expanded]);
|
|
47
|
+
const checkedSet = React.useMemo(() => new Set(checked), [checked]);
|
|
48
|
+
const { all, visible } = React.useMemo(() => {
|
|
49
|
+
const all2 = /* @__PURE__ */ new Map();
|
|
50
|
+
const visible2 = [];
|
|
51
|
+
const walk = (list, level, parent, shown) => {
|
|
52
|
+
for (const node of list) {
|
|
53
|
+
const flat = { node, level, parent };
|
|
54
|
+
all2.set(node.id, flat);
|
|
55
|
+
if (shown) visible2.push(flat);
|
|
56
|
+
if (node.children) walk(node.children, level + 1, node.id, shown && open.has(node.id));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(nodes, 1, null, true);
|
|
60
|
+
return { all: all2, visible: visible2 };
|
|
61
|
+
}, [nodes, open]);
|
|
62
|
+
const tabStop = focused && visible.some((f) => f.node.id === focused) && focused || selected && visible.some((f) => f.node.id === selected) && selected || visible[0]?.node.id;
|
|
63
|
+
const focus = (id) => {
|
|
64
|
+
if (!id) return;
|
|
65
|
+
setFocused(id);
|
|
66
|
+
refs.current.get(id)?.focus();
|
|
67
|
+
};
|
|
68
|
+
const toggle = async (node, to = !open.has(node.id)) => {
|
|
69
|
+
if (!hasKids(node) || to === open.has(node.id)) return;
|
|
70
|
+
setExpanded(to ? [...expanded, node.id] : expanded.filter((id) => id !== node.id));
|
|
71
|
+
if (to && node.hasChildren && !node.children && onLoadChildren) {
|
|
72
|
+
setLoading(node.id);
|
|
73
|
+
try {
|
|
74
|
+
await onLoadChildren(node);
|
|
75
|
+
} finally {
|
|
76
|
+
setLoading((id) => id === node.id ? null : id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const toggleCheck = (node) => {
|
|
81
|
+
const next = new Set(checkedSet);
|
|
82
|
+
const on = !next.has(node.id);
|
|
83
|
+
for (const id of [node.id, ...descendants(node)]) {
|
|
84
|
+
if (on) next.add(id);
|
|
85
|
+
else next.delete(id);
|
|
86
|
+
}
|
|
87
|
+
for (let p = all.get(node.id)?.parent; p; p = all.get(p)?.parent ?? null) {
|
|
88
|
+
const kids = all.get(p).node.children ?? [];
|
|
89
|
+
if (kids.every((k) => next.has(k.id))) next.add(p);
|
|
90
|
+
else next.delete(p);
|
|
91
|
+
}
|
|
92
|
+
setChecked([...next]);
|
|
93
|
+
};
|
|
94
|
+
const activate = (node) => {
|
|
95
|
+
if (node.disabled) return;
|
|
96
|
+
if (checkable) toggleCheck(node);
|
|
97
|
+
else setSelected(node.id);
|
|
98
|
+
};
|
|
99
|
+
const onKeyDown = (e, flat) => {
|
|
100
|
+
const { node } = flat;
|
|
101
|
+
const i = visible.findIndex((f) => f.node.id === node.id);
|
|
102
|
+
const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
|
|
103
|
+
const flip = { ArrowLeft: "ArrowRight", ArrowRight: "ArrowLeft" };
|
|
104
|
+
const key = rtl ? flip[e.key] ?? e.key : e.key;
|
|
105
|
+
let handled = true;
|
|
106
|
+
switch (key) {
|
|
107
|
+
case "ArrowDown":
|
|
108
|
+
focus(visible[i + 1]?.node.id);
|
|
109
|
+
break;
|
|
110
|
+
case "ArrowUp":
|
|
111
|
+
focus(visible[i - 1]?.node.id);
|
|
112
|
+
break;
|
|
113
|
+
case "Home":
|
|
114
|
+
focus(visible[0]?.node.id);
|
|
115
|
+
break;
|
|
116
|
+
case "End":
|
|
117
|
+
focus(visible.at(-1)?.node.id);
|
|
118
|
+
break;
|
|
119
|
+
case "ArrowRight":
|
|
120
|
+
if (hasKids(node) && !open.has(node.id)) void toggle(node, true);
|
|
121
|
+
else if (open.has(node.id)) focus(node.children?.[0]?.id);
|
|
122
|
+
break;
|
|
123
|
+
case "ArrowLeft":
|
|
124
|
+
if (open.has(node.id)) void toggle(node, false);
|
|
125
|
+
else focus(flat.parent ?? void 0);
|
|
126
|
+
break;
|
|
127
|
+
case "Enter":
|
|
128
|
+
case " ":
|
|
129
|
+
activate(node);
|
|
130
|
+
break;
|
|
131
|
+
default: {
|
|
132
|
+
handled = false;
|
|
133
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) break;
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const t = typed.current;
|
|
136
|
+
t.text = (now - t.at > 500 ? "" : t.text) + e.key.toLowerCase();
|
|
137
|
+
t.at = now;
|
|
138
|
+
const order = [...visible.slice(i + 1), ...visible.slice(0, i + 1)];
|
|
139
|
+
const hit = order.find((f) => text(f.node).toLowerCase().startsWith(t.text));
|
|
140
|
+
if (hit) {
|
|
141
|
+
focus(hit.node.id);
|
|
142
|
+
handled = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (handled) {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const render = (list, level, parent) => list.map((node, index) => {
|
|
152
|
+
const kids = hasKids(node);
|
|
153
|
+
const isOpen = open.has(node.id);
|
|
154
|
+
const isChecked = checkedSet.has(node.id);
|
|
155
|
+
const mixed = !isChecked && descendants(node).some((id) => checkedSet.has(id));
|
|
156
|
+
const flat = { node, level, parent };
|
|
157
|
+
return /* @__PURE__ */ jsxs(
|
|
158
|
+
"li",
|
|
159
|
+
{
|
|
160
|
+
ref: (el) => {
|
|
161
|
+
if (el) refs.current.set(node.id, el);
|
|
162
|
+
else refs.current.delete(node.id);
|
|
163
|
+
},
|
|
164
|
+
role: "treeitem",
|
|
165
|
+
"aria-level": level,
|
|
166
|
+
"aria-setsize": list.length,
|
|
167
|
+
"aria-posinset": index + 1,
|
|
168
|
+
"aria-expanded": kids ? isOpen : void 0,
|
|
169
|
+
"aria-selected": checkable ? void 0 : selected === node.id,
|
|
170
|
+
"aria-checked": checkable ? mixed ? "mixed" : isChecked : void 0,
|
|
171
|
+
"aria-disabled": node.disabled || void 0,
|
|
172
|
+
"aria-busy": loading === node.id || void 0,
|
|
173
|
+
tabIndex: tabStop === node.id ? 0 : -1,
|
|
174
|
+
onKeyDown: (e) => onKeyDown(e, flat),
|
|
175
|
+
onFocus: (e) => {
|
|
176
|
+
if (e.target === e.currentTarget) setFocused(node.id);
|
|
177
|
+
},
|
|
178
|
+
className: "outline-none [&:focus-visible>div]:outline [&:focus-visible>div]:outline-2 [&:focus-visible>div]:-outline-offset-2 [&:focus-visible>div]:outline-[var(--color-primary)]",
|
|
179
|
+
children: [
|
|
180
|
+
/* @__PURE__ */ jsxs(
|
|
181
|
+
"div",
|
|
182
|
+
{
|
|
183
|
+
ref: (el) => {
|
|
184
|
+
if (el) el.style.paddingInlineStart = `${(level - 1) * 1.25 + 0.25}rem`;
|
|
185
|
+
},
|
|
186
|
+
onClick: () => {
|
|
187
|
+
focus(node.id);
|
|
188
|
+
if (kids && !checkable) void toggle(node);
|
|
189
|
+
activate(node);
|
|
190
|
+
},
|
|
191
|
+
className: cn(
|
|
192
|
+
"flex min-h-control-sm cursor-default items-center gap-1.5 rounded-shape-control pe-2 text-body-md text-[var(--color-on-surface)]",
|
|
193
|
+
"hover:bg-[var(--color-surface-container-high)]",
|
|
194
|
+
!checkable && selected === node.id && "bg-[var(--color-secondary-container)] text-[var(--color-on-secondary-container)] hover:bg-[var(--color-secondary-container)]",
|
|
195
|
+
node.disabled && "cursor-not-allowed text-[var(--color-text-disabled)] hover:bg-transparent"
|
|
196
|
+
),
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
"span",
|
|
200
|
+
{
|
|
201
|
+
"aria-hidden": "true",
|
|
202
|
+
onClick: (e) => {
|
|
203
|
+
if (!kids) return;
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
focus(node.id);
|
|
206
|
+
void toggle(node);
|
|
207
|
+
},
|
|
208
|
+
className: "inline-flex size-5 shrink-0 items-center justify-center text-[var(--color-on-surface-variant)]",
|
|
209
|
+
children: loading === node.id ? /* @__PURE__ */ jsx(Spinner, { size: "xs" }) : kids ? /* @__PURE__ */ jsx(
|
|
210
|
+
ChevronRight,
|
|
211
|
+
{
|
|
212
|
+
size: "xs",
|
|
213
|
+
className: cn("transition-transform rtl:-scale-x-100", isOpen && "rotate-90 rtl:-rotate-90")
|
|
214
|
+
}
|
|
215
|
+
) : null
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
checkable && /* @__PURE__ */ jsx(
|
|
219
|
+
"span",
|
|
220
|
+
{
|
|
221
|
+
"aria-hidden": "true",
|
|
222
|
+
className: cn(
|
|
223
|
+
"inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border-2",
|
|
224
|
+
isChecked || mixed ? "border-[var(--color-primary)] bg-[var(--color-primary)] text-[var(--color-on-primary)]" : "border-[var(--color-outline)]"
|
|
225
|
+
),
|
|
226
|
+
children: isChecked ? /* @__PURE__ */ jsx(Check, { size: "xs" }) : mixed ? /* @__PURE__ */ jsx(Minus, { size: "xs" }) : null
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0 text-[var(--color-on-surface-variant)]", children: node.icon }),
|
|
230
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: node.label })
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
),
|
|
234
|
+
kids && isOpen && node.children && /* @__PURE__ */ jsx("ul", { role: "group", className: "m-0 list-none p-0", children: render(node.children, level + 1, node.id) })
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
node.id
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
return /* @__PURE__ */ jsx(
|
|
241
|
+
"ul",
|
|
242
|
+
{
|
|
243
|
+
role: "tree",
|
|
244
|
+
"aria-multiselectable": checkable || void 0,
|
|
245
|
+
className: cn("m-0 flex list-none flex-col p-0", className),
|
|
246
|
+
...aria,
|
|
247
|
+
children: render(nodes, 1, null)
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
TreeView
|
|
253
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -226,6 +226,8 @@ export { DateTimePicker } from './components/DateTimePicker/index.js';
|
|
|
226
226
|
export type { DateTimePickerProps } from './components/DateTimePicker/index.js';
|
|
227
227
|
export { EventCalendar } from './components/EventCalendar/index.js';
|
|
228
228
|
export type { EventCalendarProps, EventCalendarView, CalendarEvent, EventTone } from './components/EventCalendar/index.js';
|
|
229
|
+
export { TreeView } from './components/TreeView/index.js';
|
|
230
|
+
export type { TreeViewProps, TreeNode } from './components/TreeView/index.js';
|
|
229
231
|
export type { OTPInputProps } from './components/OTPInput/index.js';
|
|
230
232
|
export { DatePicker } from './components/DatePicker/index.js';
|
|
231
233
|
export type { DatePickerProps } from './components/DatePicker/index.js';
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,7 @@ import { InlineEdit } from "./components/InlineEdit/index.js";
|
|
|
127
127
|
import { TimePicker } from "./components/TimePicker/index.js";
|
|
128
128
|
import { DateTimePicker } from "./components/DateTimePicker/index.js";
|
|
129
129
|
import { EventCalendar } from "./components/EventCalendar/index.js";
|
|
130
|
+
import { TreeView } from "./components/TreeView/index.js";
|
|
130
131
|
import { DatePicker } from "./components/DatePicker/index.js";
|
|
131
132
|
import { DateRangePicker } from "./components/DateRangePicker/index.js";
|
|
132
133
|
import { todayIn } from "./components/DateRangePicker/index.js";
|
|
@@ -281,6 +282,7 @@ export {
|
|
|
281
282
|
Tooltip,
|
|
282
283
|
Provider as TooltipProvider,
|
|
283
284
|
TopNav,
|
|
285
|
+
TreeView,
|
|
284
286
|
VisuallyHidden,
|
|
285
287
|
addonVariants,
|
|
286
288
|
alertVariants,
|