@dbcdk/react-components 0.0.207 → 0.0.209

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.
Files changed (36) hide show
  1. package/dist/client.cjs +10 -0
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +2 -0
  4. package/dist/components/listbox/Listbox.cjs +153 -0
  5. package/dist/components/listbox/Listbox.d.ts +22 -0
  6. package/dist/components/listbox/Listbox.js +146 -0
  7. package/dist/components/listbox/Listbox.module.css +97 -0
  8. package/dist/components/menu/Menu.cjs +7 -24
  9. package/dist/components/menu/Menu.js +7 -24
  10. package/dist/components/overlay/side-panel/SidePanel.cjs +80 -2
  11. package/dist/components/overlay/side-panel/SidePanel.js +80 -2
  12. package/dist/components/split-pane/SplitPane.cjs +22 -7
  13. package/dist/components/split-pane/SplitPane.d.ts +5 -2
  14. package/dist/components/split-pane/SplitPane.js +22 -7
  15. package/dist/components/split-pane/SplitPane.module.css +9 -0
  16. package/dist/components/split-pane/provider/SplitPaneContext.cjs +5 -2
  17. package/dist/components/split-pane/provider/SplitPaneContext.d.ts +3 -1
  18. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -2
  19. package/dist/components/status-text/StatusText.cjs +27 -0
  20. package/dist/components/status-text/StatusText.d.ts +7 -0
  21. package/dist/components/status-text/StatusText.js +21 -0
  22. package/dist/components/status-text/StatusText.module.css +41 -0
  23. package/dist/components/tree-view/TreeNodeRow.cjs +70 -30
  24. package/dist/components/tree-view/TreeNodeRow.d.ts +6 -1
  25. package/dist/components/tree-view/TreeNodeRow.js +70 -30
  26. package/dist/components/tree-view/TreeView.cjs +107 -19
  27. package/dist/components/tree-view/TreeView.d.ts +1 -1
  28. package/dist/components/tree-view/TreeView.js +107 -19
  29. package/dist/components/tree-view/TreeView.module.css +47 -3
  30. package/dist/components/tree-view/types.d.ts +6 -0
  31. package/dist/hooks/useRovingFocus.cjs +45 -0
  32. package/dist/hooks/useRovingFocus.d.ts +10 -0
  33. package/dist/hooks/useRovingFocus.js +43 -0
  34. package/dist/index.css +2 -0
  35. package/dist/styles/css-helper-classes/typography.css +4 -0
  36. package/package.json +1 -1
@@ -16,12 +16,17 @@ function TreeNodeRow({
16
16
  wrapLabels,
17
17
  isExpanded,
18
18
  toggle,
19
+ expand,
20
+ expandOnSelect,
19
21
  resolvedChildren,
20
22
  loadingIds,
21
23
  errorIds,
22
24
  requestLoad,
23
25
  loadChildren,
24
26
  selectedId,
27
+ currentId,
28
+ focusedId,
29
+ onFocusNode,
25
30
  onSelectNode,
26
31
  renderLabel
27
32
  }) {
@@ -33,6 +38,7 @@ function TreeNodeRow({
33
38
  const loading = loadingIds.has(node.id);
34
39
  const error = errorIds[node.id];
35
40
  const selected = selectedId !== void 0 && selectedId === node.id;
41
+ const current = currentId !== void 0 && currentId === node.id;
36
42
  react.useEffect(() => {
37
43
  if (expanded && children === void 0 && !loading && !error && loadChildren) {
38
44
  requestLoad(node);
@@ -44,38 +50,67 @@ function TreeNodeRow({
44
50
  role: "treeitem",
45
51
  "aria-expanded": expandable ? expanded : void 0,
46
52
  "aria-selected": selected,
53
+ "aria-current": current ? "page" : void 0,
54
+ "aria-level": depth + 1,
55
+ "data-tree-node-id": node.id,
47
56
  style: { ["--tree-depth"]: depth },
48
57
  children: [
49
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.row, "data-selected": selected || void 0, children: [
50
- /* @__PURE__ */ jsxRuntime.jsx(
51
- "button",
52
- {
53
- type: "button",
54
- className: styles__default.default.toggle,
55
- onClick: () => expandable && toggle(node.id),
56
- disabled: !expandable,
57
- "aria-hidden": !expandable,
58
- tabIndex: expandable ? 0 : -1,
59
- children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: `spin ${styles__default.default.spinner}`, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
60
- // A resolved node's chevron means "this has children"; an
61
- // unresolved lazy node's dimmer chevron means "might have
62
- // children, click to check" - we genuinely don't know yet,
63
- // and shouldn't visually assert otherwise.
64
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: isResolved ? void 0 : styles__default.default.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { "aria-hidden": "true" }) })
65
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.leafSpacer, "aria-hidden": "true" })
66
- }
67
- ),
68
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: styles__default.default.labelButton, onClick: () => onSelectNode == null ? void 0 : onSelectNode(node), children: [
69
- node.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: node.icon }) : null,
70
- /* @__PURE__ */ jsxRuntime.jsx(
71
- "span",
72
- {
73
- className: [styles__default.default.label, wrapLabels ? styles__default.default.labelWrap : styles__default.default.labelTruncate].filter(Boolean).join(" "),
74
- children: renderLabel ? renderLabel(node) : node.label
75
- }
76
- )
77
- ] })
78
- ] }),
58
+ /* @__PURE__ */ jsxRuntime.jsxs(
59
+ "div",
60
+ {
61
+ className: styles__default.default.row,
62
+ "data-tree-row": "true",
63
+ "data-selected": selected || void 0,
64
+ "data-current": current || void 0,
65
+ children: [
66
+ /* @__PURE__ */ jsxRuntime.jsx(
67
+ "button",
68
+ {
69
+ type: "button",
70
+ className: styles__default.default.toggle,
71
+ "data-tree-toggle": "true",
72
+ onClick: () => expandable && toggle(node.id),
73
+ disabled: !expandable,
74
+ "aria-hidden": !expandable,
75
+ tabIndex: -1,
76
+ "aria-label": expanded ? "Luk relationer" : "Vis relationer",
77
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: `spin ${styles__default.default.spinner}`, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
78
+ // A resolved node's chevron means "this has children"; an
79
+ // unresolved lazy node's dimmer chevron means "might have
80
+ // children, click to check" - we genuinely don't know yet,
81
+ // and shouldn't visually assert otherwise.
82
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: isResolved ? void 0 : styles__default.default.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { "aria-hidden": "true" }) })
83
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.leafSpacer, "aria-hidden": "true" })
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsxRuntime.jsxs(
87
+ "button",
88
+ {
89
+ type: "button",
90
+ className: styles__default.default.labelButton,
91
+ "data-tree-focus-target": "true",
92
+ "data-tree-focus-node-id": node.id,
93
+ tabIndex: focusedId === node.id ? 0 : -1,
94
+ onFocus: () => onFocusNode(node.id),
95
+ onClick: () => {
96
+ if (expandOnSelect && expandable) expand(node.id);
97
+ onSelectNode == null ? void 0 : onSelectNode(node);
98
+ },
99
+ children: [
100
+ node.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: node.icon }) : null,
101
+ /* @__PURE__ */ jsxRuntime.jsx(
102
+ "span",
103
+ {
104
+ className: [styles__default.default.label, wrapLabels ? styles__default.default.labelWrap : styles__default.default.labelTruncate].filter(Boolean).join(" "),
105
+ children: renderLabel ? renderLabel(node) : node.label
106
+ }
107
+ )
108
+ ]
109
+ }
110
+ )
111
+ ]
112
+ }
113
+ ),
79
114
  error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.errorRow, children: [
80
115
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: error }),
81
116
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: styles__default.default.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
@@ -88,12 +123,17 @@ function TreeNodeRow({
88
123
  wrapLabels,
89
124
  isExpanded,
90
125
  toggle,
126
+ expand,
127
+ expandOnSelect,
91
128
  resolvedChildren,
92
129
  loadingIds,
93
130
  errorIds,
94
131
  requestLoad,
95
132
  loadChildren,
96
133
  selectedId,
134
+ currentId,
135
+ focusedId,
136
+ onFocusNode,
97
137
  onSelectNode,
98
138
  renderLabel
99
139
  },
@@ -6,14 +6,19 @@ type TreeNodeRowProps<T> = {
6
6
  wrapLabels: boolean;
7
7
  isExpanded: (id: string) => boolean;
8
8
  toggle: (id: string) => void;
9
+ expand: (id: string) => void;
10
+ expandOnSelect: boolean;
9
11
  resolvedChildren: Record<string, TreeNode<T>[]>;
10
12
  loadingIds: Set<string>;
11
13
  errorIds: Record<string, ReactNode>;
12
14
  requestLoad: (node: TreeNode<T>) => void;
13
15
  loadChildren?: LoadTreeChildren<T>;
14
16
  selectedId?: string;
17
+ currentId?: string;
18
+ focusedId?: string;
19
+ onFocusNode: (id: string) => void;
15
20
  onSelectNode?: (node: TreeNode<T>) => void;
16
21
  renderLabel?: (node: TreeNode<T>) => ReactNode;
17
22
  };
18
- export declare function TreeNodeRow<T>({ node, depth, wrapLabels, isExpanded, toggle, resolvedChildren, loadingIds, errorIds, requestLoad, loadChildren, selectedId, onSelectNode, renderLabel, }: TreeNodeRowProps<T>): JSX.Element;
23
+ export declare function TreeNodeRow<T>({ node, depth, wrapLabels, isExpanded, toggle, expand, expandOnSelect, resolvedChildren, loadingIds, errorIds, requestLoad, loadChildren, selectedId, currentId, focusedId, onFocusNode, onSelectNode, renderLabel, }: TreeNodeRowProps<T>): JSX.Element;
19
24
  export {};
@@ -10,12 +10,17 @@ function TreeNodeRow({
10
10
  wrapLabels,
11
11
  isExpanded,
12
12
  toggle,
13
+ expand,
14
+ expandOnSelect,
13
15
  resolvedChildren,
14
16
  loadingIds,
15
17
  errorIds,
16
18
  requestLoad,
17
19
  loadChildren,
18
20
  selectedId,
21
+ currentId,
22
+ focusedId,
23
+ onFocusNode,
19
24
  onSelectNode,
20
25
  renderLabel
21
26
  }) {
@@ -27,6 +32,7 @@ function TreeNodeRow({
27
32
  const loading = loadingIds.has(node.id);
28
33
  const error = errorIds[node.id];
29
34
  const selected = selectedId !== void 0 && selectedId === node.id;
35
+ const current = currentId !== void 0 && currentId === node.id;
30
36
  useEffect(() => {
31
37
  if (expanded && children === void 0 && !loading && !error && loadChildren) {
32
38
  requestLoad(node);
@@ -38,38 +44,67 @@ function TreeNodeRow({
38
44
  role: "treeitem",
39
45
  "aria-expanded": expandable ? expanded : void 0,
40
46
  "aria-selected": selected,
47
+ "aria-current": current ? "page" : void 0,
48
+ "aria-level": depth + 1,
49
+ "data-tree-node-id": node.id,
41
50
  style: { ["--tree-depth"]: depth },
42
51
  children: [
43
- /* @__PURE__ */ jsxs("div", { className: styles.row, "data-selected": selected || void 0, children: [
44
- /* @__PURE__ */ jsx(
45
- "button",
46
- {
47
- type: "button",
48
- className: styles.toggle,
49
- onClick: () => expandable && toggle(node.id),
50
- disabled: !expandable,
51
- "aria-hidden": !expandable,
52
- tabIndex: expandable ? 0 : -1,
53
- children: loading ? /* @__PURE__ */ jsx("span", { className: `spin ${styles.spinner}`, children: /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
54
- // A resolved node's chevron means "this has children"; an
55
- // unresolved lazy node's dimmer chevron means "might have
56
- // children, click to check" - we genuinely don't know yet,
57
- // and shouldn't visually assert otherwise.
58
- /* @__PURE__ */ jsx("span", { className: isResolved ? void 0 : styles.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) })
59
- ) : /* @__PURE__ */ jsx("span", { className: styles.leafSpacer, "aria-hidden": "true" })
60
- }
61
- ),
62
- /* @__PURE__ */ jsxs("button", { type: "button", className: styles.labelButton, onClick: () => onSelectNode == null ? void 0 : onSelectNode(node), children: [
63
- node.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: node.icon }) : null,
64
- /* @__PURE__ */ jsx(
65
- "span",
66
- {
67
- className: [styles.label, wrapLabels ? styles.labelWrap : styles.labelTruncate].filter(Boolean).join(" "),
68
- children: renderLabel ? renderLabel(node) : node.label
69
- }
70
- )
71
- ] })
72
- ] }),
52
+ /* @__PURE__ */ jsxs(
53
+ "div",
54
+ {
55
+ className: styles.row,
56
+ "data-tree-row": "true",
57
+ "data-selected": selected || void 0,
58
+ "data-current": current || void 0,
59
+ children: [
60
+ /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ type: "button",
64
+ className: styles.toggle,
65
+ "data-tree-toggle": "true",
66
+ onClick: () => expandable && toggle(node.id),
67
+ disabled: !expandable,
68
+ "aria-hidden": !expandable,
69
+ tabIndex: -1,
70
+ "aria-label": expanded ? "Luk relationer" : "Vis relationer",
71
+ children: loading ? /* @__PURE__ */ jsx("span", { className: `spin ${styles.spinner}`, children: /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
72
+ // A resolved node's chevron means "this has children"; an
73
+ // unresolved lazy node's dimmer chevron means "might have
74
+ // children, click to check" - we genuinely don't know yet,
75
+ // and shouldn't visually assert otherwise.
76
+ /* @__PURE__ */ jsx("span", { className: isResolved ? void 0 : styles.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) })
77
+ ) : /* @__PURE__ */ jsx("span", { className: styles.leafSpacer, "aria-hidden": "true" })
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsxs(
81
+ "button",
82
+ {
83
+ type: "button",
84
+ className: styles.labelButton,
85
+ "data-tree-focus-target": "true",
86
+ "data-tree-focus-node-id": node.id,
87
+ tabIndex: focusedId === node.id ? 0 : -1,
88
+ onFocus: () => onFocusNode(node.id),
89
+ onClick: () => {
90
+ if (expandOnSelect && expandable) expand(node.id);
91
+ onSelectNode == null ? void 0 : onSelectNode(node);
92
+ },
93
+ children: [
94
+ node.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: node.icon }) : null,
95
+ /* @__PURE__ */ jsx(
96
+ "span",
97
+ {
98
+ className: [styles.label, wrapLabels ? styles.labelWrap : styles.labelTruncate].filter(Boolean).join(" "),
99
+ children: renderLabel ? renderLabel(node) : node.label
100
+ }
101
+ )
102
+ ]
103
+ }
104
+ )
105
+ ]
106
+ }
107
+ ),
73
108
  error ? /* @__PURE__ */ jsxs("div", { className: styles.errorRow, children: [
74
109
  /* @__PURE__ */ jsx("span", { children: error }),
75
110
  /* @__PURE__ */ jsx("button", { type: "button", className: styles.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
@@ -82,12 +117,17 @@ function TreeNodeRow({
82
117
  wrapLabels,
83
118
  isExpanded,
84
119
  toggle,
120
+ expand,
121
+ expandOnSelect,
85
122
  resolvedChildren,
86
123
  loadingIds,
87
124
  errorIds,
88
125
  requestLoad,
89
126
  loadChildren,
90
127
  selectedId,
128
+ currentId,
129
+ focusedId,
130
+ onFocusNode,
91
131
  onSelectNode,
92
132
  renderLabel
93
133
  },
@@ -14,19 +14,29 @@ var styles__default = /*#__PURE__*/_interopDefault(styles);
14
14
 
15
15
  function TreeView({
16
16
  data,
17
+ size = "sm",
17
18
  loadChildren,
18
19
  defaultExpandedIds,
19
20
  expandedIds,
20
21
  onExpandedIdsChange,
21
22
  selectedId,
23
+ currentId,
22
24
  onSelectNode,
25
+ expandOnSelect = false,
23
26
  renderLabel,
24
27
  wrapLabels = true,
25
28
  emptyMessage = "Intet at vise.",
26
29
  loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
27
30
  className,
31
+ "aria-label": ariaLabel,
28
32
  ...rest
29
33
  }) {
34
+ const [focusedId, setFocusedId] = react.useState(
35
+ () => {
36
+ var _a, _b;
37
+ return (_b = currentId != null ? currentId : selectedId) != null ? _b : (_a = data[0]) == null ? void 0 : _a.id;
38
+ }
39
+ );
30
40
  const { isExpanded, toggle, expand } = useTreeExpansion.useTreeExpansion(
31
41
  defaultExpandedIds,
32
42
  expandedIds,
@@ -60,28 +70,106 @@ function TreeView({
60
70
  },
61
71
  [loadChildren, loadingIds, expand, loadErrorMessage]
62
72
  );
73
+ const handleTreeKeyDown = react.useCallback((event) => {
74
+ var _a, _b, _c, _d;
75
+ const target = event.target;
76
+ const focusedLabel = target.closest('[data-tree-focus-target="true"]');
77
+ if (!focusedLabel) return;
78
+ const visibleItems = Array.from(
79
+ event.currentTarget.querySelectorAll('[data-tree-focus-target="true"]')
80
+ );
81
+ const treeItem = focusedLabel.closest('[role="treeitem"]');
82
+ if (!treeItem) return;
83
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
84
+ const index = visibleItems.indexOf(focusedLabel);
85
+ const nextIndex = Math.max(
86
+ 0,
87
+ Math.min(index + (event.key === "ArrowDown" ? 1 : -1), visibleItems.length - 1)
88
+ );
89
+ if (nextIndex !== index) {
90
+ event.preventDefault();
91
+ (_a = visibleItems[nextIndex]) == null ? void 0 : _a.focus();
92
+ }
93
+ return;
94
+ }
95
+ if (event.key === "ArrowRight") {
96
+ const expanded = treeItem.getAttribute("aria-expanded") === "true";
97
+ if (treeItem.getAttribute("aria-expanded") === "false") {
98
+ event.preventDefault();
99
+ (_b = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _b.click();
100
+ } else if (expanded) {
101
+ const firstChild = treeItem.querySelector(
102
+ ':scope > [role="group"] [data-tree-focus-target="true"]'
103
+ );
104
+ if (firstChild) {
105
+ event.preventDefault();
106
+ firstChild.focus();
107
+ }
108
+ }
109
+ return;
110
+ }
111
+ if (event.key === "ArrowLeft") {
112
+ if (treeItem.getAttribute("aria-expanded") === "true") {
113
+ event.preventDefault();
114
+ (_c = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _c.click();
115
+ return;
116
+ }
117
+ const parentTreeItem = (_d = treeItem.parentElement) == null ? void 0 : _d.closest('[role="treeitem"]');
118
+ const parentLabel = parentTreeItem == null ? void 0 : parentTreeItem.querySelector(
119
+ ':scope > [data-tree-row] [data-tree-focus-target="true"]'
120
+ );
121
+ if (parentLabel) {
122
+ event.preventDefault();
123
+ parentLabel.focus();
124
+ }
125
+ }
126
+ }, []);
63
127
  if (data.length === 0) {
64
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [styles__default.default.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
128
+ return /* @__PURE__ */ jsxRuntime.jsx(
129
+ "div",
130
+ {
131
+ className: [styles__default.default.emptyState, className].filter(Boolean).join(" "),
132
+ "aria-label": ariaLabel,
133
+ ...rest,
134
+ children: emptyMessage
135
+ }
136
+ );
65
137
  }
66
- return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tree", className: [styles__default.default.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
67
- TreeNodeRow.TreeNodeRow,
138
+ return /* @__PURE__ */ jsxRuntime.jsx(
139
+ "div",
68
140
  {
69
- node,
70
- depth: 0,
71
- wrapLabels,
72
- isExpanded,
73
- toggle,
74
- resolvedChildren,
75
- loadingIds,
76
- errorIds,
77
- requestLoad,
78
- loadChildren,
79
- selectedId,
80
- onSelectNode,
81
- renderLabel
82
- },
83
- node.id
84
- )) });
141
+ role: "tree",
142
+ "data-size": size,
143
+ "aria-label": ariaLabel,
144
+ className: [styles__default.default.tree, className].filter(Boolean).join(" "),
145
+ onKeyDown: handleTreeKeyDown,
146
+ ...rest,
147
+ children: data.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
148
+ TreeNodeRow.TreeNodeRow,
149
+ {
150
+ node,
151
+ depth: 0,
152
+ wrapLabels,
153
+ isExpanded,
154
+ toggle,
155
+ expand,
156
+ expandOnSelect,
157
+ resolvedChildren,
158
+ loadingIds,
159
+ errorIds,
160
+ requestLoad,
161
+ loadChildren,
162
+ selectedId,
163
+ currentId,
164
+ focusedId,
165
+ onFocusNode: setFocusedId,
166
+ onSelectNode,
167
+ renderLabel
168
+ },
169
+ node.id
170
+ ))
171
+ }
172
+ );
85
173
  }
86
174
 
87
175
  Object.defineProperty(exports, "collectExpandableIds", {
@@ -17,4 +17,4 @@ export { collectExpandableIds, findAncestorIds } from './utils';
17
17
  * useTreeExpansion), and it can be left uncontrolled or owned by the
18
18
  * caller via `expandedIds`/`onExpandedIdsChange`.
19
19
  */
20
- export declare function TreeView<T = unknown>({ data, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, onSelectNode, renderLabel, wrapLabels, emptyMessage, loadErrorMessage, className, ...rest }: TreeViewProps<T>): JSX.Element;
20
+ export declare function TreeView<T = unknown>({ data, size, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, currentId, onSelectNode, expandOnSelect, renderLabel, wrapLabels, emptyMessage, loadErrorMessage, className, 'aria-label': ariaLabel, ...rest }: TreeViewProps<T>): JSX.Element;
@@ -8,19 +8,29 @@ export { collectExpandableIds, findAncestorIds } from './utils';
8
8
 
9
9
  function TreeView({
10
10
  data,
11
+ size = "sm",
11
12
  loadChildren,
12
13
  defaultExpandedIds,
13
14
  expandedIds,
14
15
  onExpandedIdsChange,
15
16
  selectedId,
17
+ currentId,
16
18
  onSelectNode,
19
+ expandOnSelect = false,
17
20
  renderLabel,
18
21
  wrapLabels = true,
19
22
  emptyMessage = "Intet at vise.",
20
23
  loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
21
24
  className,
25
+ "aria-label": ariaLabel,
22
26
  ...rest
23
27
  }) {
28
+ const [focusedId, setFocusedId] = useState(
29
+ () => {
30
+ var _a, _b;
31
+ return (_b = currentId != null ? currentId : selectedId) != null ? _b : (_a = data[0]) == null ? void 0 : _a.id;
32
+ }
33
+ );
24
34
  const { isExpanded, toggle, expand } = useTreeExpansion(
25
35
  defaultExpandedIds,
26
36
  expandedIds,
@@ -54,28 +64,106 @@ function TreeView({
54
64
  },
55
65
  [loadChildren, loadingIds, expand, loadErrorMessage]
56
66
  );
67
+ const handleTreeKeyDown = useCallback((event) => {
68
+ var _a, _b, _c, _d;
69
+ const target = event.target;
70
+ const focusedLabel = target.closest('[data-tree-focus-target="true"]');
71
+ if (!focusedLabel) return;
72
+ const visibleItems = Array.from(
73
+ event.currentTarget.querySelectorAll('[data-tree-focus-target="true"]')
74
+ );
75
+ const treeItem = focusedLabel.closest('[role="treeitem"]');
76
+ if (!treeItem) return;
77
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
78
+ const index = visibleItems.indexOf(focusedLabel);
79
+ const nextIndex = Math.max(
80
+ 0,
81
+ Math.min(index + (event.key === "ArrowDown" ? 1 : -1), visibleItems.length - 1)
82
+ );
83
+ if (nextIndex !== index) {
84
+ event.preventDefault();
85
+ (_a = visibleItems[nextIndex]) == null ? void 0 : _a.focus();
86
+ }
87
+ return;
88
+ }
89
+ if (event.key === "ArrowRight") {
90
+ const expanded = treeItem.getAttribute("aria-expanded") === "true";
91
+ if (treeItem.getAttribute("aria-expanded") === "false") {
92
+ event.preventDefault();
93
+ (_b = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _b.click();
94
+ } else if (expanded) {
95
+ const firstChild = treeItem.querySelector(
96
+ ':scope > [role="group"] [data-tree-focus-target="true"]'
97
+ );
98
+ if (firstChild) {
99
+ event.preventDefault();
100
+ firstChild.focus();
101
+ }
102
+ }
103
+ return;
104
+ }
105
+ if (event.key === "ArrowLeft") {
106
+ if (treeItem.getAttribute("aria-expanded") === "true") {
107
+ event.preventDefault();
108
+ (_c = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _c.click();
109
+ return;
110
+ }
111
+ const parentTreeItem = (_d = treeItem.parentElement) == null ? void 0 : _d.closest('[role="treeitem"]');
112
+ const parentLabel = parentTreeItem == null ? void 0 : parentTreeItem.querySelector(
113
+ ':scope > [data-tree-row] [data-tree-focus-target="true"]'
114
+ );
115
+ if (parentLabel) {
116
+ event.preventDefault();
117
+ parentLabel.focus();
118
+ }
119
+ }
120
+ }, []);
57
121
  if (data.length === 0) {
58
- return /* @__PURE__ */ jsx("div", { className: [styles.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
122
+ return /* @__PURE__ */ jsx(
123
+ "div",
124
+ {
125
+ className: [styles.emptyState, className].filter(Boolean).join(" "),
126
+ "aria-label": ariaLabel,
127
+ ...rest,
128
+ children: emptyMessage
129
+ }
130
+ );
59
131
  }
60
- return /* @__PURE__ */ jsx("div", { role: "tree", className: [styles.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsx(
61
- TreeNodeRow,
132
+ return /* @__PURE__ */ jsx(
133
+ "div",
62
134
  {
63
- node,
64
- depth: 0,
65
- wrapLabels,
66
- isExpanded,
67
- toggle,
68
- resolvedChildren,
69
- loadingIds,
70
- errorIds,
71
- requestLoad,
72
- loadChildren,
73
- selectedId,
74
- onSelectNode,
75
- renderLabel
76
- },
77
- node.id
78
- )) });
135
+ role: "tree",
136
+ "data-size": size,
137
+ "aria-label": ariaLabel,
138
+ className: [styles.tree, className].filter(Boolean).join(" "),
139
+ onKeyDown: handleTreeKeyDown,
140
+ ...rest,
141
+ children: data.map((node) => /* @__PURE__ */ jsx(
142
+ TreeNodeRow,
143
+ {
144
+ node,
145
+ depth: 0,
146
+ wrapLabels,
147
+ isExpanded,
148
+ toggle,
149
+ expand,
150
+ expandOnSelect,
151
+ resolvedChildren,
152
+ loadingIds,
153
+ errorIds,
154
+ requestLoad,
155
+ loadChildren,
156
+ selectedId,
157
+ currentId,
158
+ focusedId,
159
+ onFocusNode: setFocusedId,
160
+ onSelectNode,
161
+ renderLabel
162
+ },
163
+ node.id
164
+ ))
165
+ }
166
+ );
79
167
  }
80
168
 
81
169
  export { TreeView };