@basic-ui/core 0.0.60 → 0.0.61

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 (214) hide show
  1. package/build/cjs/index.js.map +1 -1
  2. package/build/esm/Accordion/AccordionBody.d.ts.map +1 -1
  3. package/build/esm/Accordion/AccordionBody.js +6 -26
  4. package/build/esm/Accordion/AccordionBody.js.map +1 -1
  5. package/build/esm/Accordion/AccordionHeader.d.ts.map +1 -1
  6. package/build/esm/Accordion/AccordionHeader.js +21 -69
  7. package/build/esm/Accordion/AccordionHeader.js.map +1 -1
  8. package/build/esm/Accordion/AccordionItem.d.ts.map +1 -1
  9. package/build/esm/Accordion/AccordionItem.js +31 -18
  10. package/build/esm/Accordion/AccordionItem.js.map +1 -1
  11. package/build/esm/Accordion/context.d.ts +0 -8
  12. package/build/esm/Accordion/context.d.ts.map +1 -1
  13. package/build/esm/Accordion/context.js +0 -11
  14. package/build/esm/Accordion/context.js.map +1 -1
  15. package/build/esm/Accordion/scopeQuery.d.ts +1 -0
  16. package/build/esm/Accordion/scopeQuery.d.ts.map +1 -1
  17. package/build/esm/Accordion/scopeQuery.js +3 -0
  18. package/build/esm/Accordion/scopeQuery.js.map +1 -1
  19. package/build/esm/Collapsible/Collapsible.d.ts +13 -0
  20. package/build/esm/Collapsible/Collapsible.d.ts.map +1 -0
  21. package/build/esm/Collapsible/Collapsible.js +53 -0
  22. package/build/esm/Collapsible/Collapsible.js.map +1 -0
  23. package/build/esm/Collapsible/CollapsiblePanel.d.ts +10 -0
  24. package/build/esm/Collapsible/CollapsiblePanel.d.ts.map +1 -0
  25. package/build/esm/Collapsible/CollapsiblePanel.js +85 -0
  26. package/build/esm/Collapsible/CollapsiblePanel.js.map +1 -0
  27. package/build/esm/Collapsible/CollapsibleTrigger.d.ts +11 -0
  28. package/build/esm/Collapsible/CollapsibleTrigger.d.ts.map +1 -0
  29. package/build/esm/Collapsible/CollapsibleTrigger.js +51 -0
  30. package/build/esm/Collapsible/CollapsibleTrigger.js.map +1 -0
  31. package/build/esm/Collapsible/context.d.ts +16 -0
  32. package/build/esm/Collapsible/context.d.ts.map +1 -0
  33. package/build/esm/Collapsible/context.js +11 -0
  34. package/build/esm/Collapsible/context.js.map +1 -0
  35. package/build/esm/Collapsible/index.d.ts +4 -0
  36. package/build/esm/Collapsible/index.d.ts.map +1 -0
  37. package/build/esm/Collapsible/index.js +4 -0
  38. package/build/esm/Collapsible/index.js.map +1 -0
  39. package/build/esm/Menu/Menu.d.ts +3 -2
  40. package/build/esm/Menu/Menu.d.ts.map +1 -1
  41. package/build/esm/Menu/Menu.js +64 -4
  42. package/build/esm/Menu/Menu.js.map +1 -1
  43. package/build/esm/Menu/MenuButton.d.ts.map +1 -1
  44. package/build/esm/Menu/MenuButton.js +85 -8
  45. package/build/esm/Menu/MenuButton.js.map +1 -1
  46. package/build/esm/Menu/MenuItem.d.ts.map +1 -1
  47. package/build/esm/Menu/MenuItem.js +16 -4
  48. package/build/esm/Menu/MenuItem.js.map +1 -1
  49. package/build/esm/Menu/MenuList.d.ts.map +1 -1
  50. package/build/esm/Menu/MenuList.js +47 -12
  51. package/build/esm/Menu/MenuList.js.map +1 -1
  52. package/build/esm/Menu/MenuPopover.d.ts.map +1 -1
  53. package/build/esm/Menu/MenuPopover.js +12 -1
  54. package/build/esm/Menu/MenuPopover.js.map +1 -1
  55. package/build/esm/Menu/MenuSubmenuTrigger.d.ts +8 -0
  56. package/build/esm/Menu/MenuSubmenuTrigger.d.ts.map +1 -0
  57. package/build/esm/Menu/MenuSubmenuTrigger.js +131 -0
  58. package/build/esm/Menu/MenuSubmenuTrigger.js.map +1 -0
  59. package/build/esm/Menu/context.d.ts +13 -3
  60. package/build/esm/Menu/context.d.ts.map +1 -1
  61. package/build/esm/Menu/context.js +1 -0
  62. package/build/esm/Menu/context.js.map +1 -1
  63. package/build/esm/Menu/index.d.ts +3 -0
  64. package/build/esm/Menu/index.d.ts.map +1 -1
  65. package/build/esm/Menu/index.js +2 -0
  66. package/build/esm/Menu/index.js.map +1 -1
  67. package/build/esm/Menu/scope.d.ts +1 -0
  68. package/build/esm/Menu/scope.d.ts.map +1 -1
  69. package/build/esm/Menu/scope.js +2 -1
  70. package/build/esm/Menu/scope.js.map +1 -1
  71. package/build/esm/MenuBar/MenuBar.d.ts +11 -0
  72. package/build/esm/MenuBar/MenuBar.d.ts.map +1 -0
  73. package/build/esm/MenuBar/MenuBar.js +153 -0
  74. package/build/esm/MenuBar/MenuBar.js.map +1 -0
  75. package/build/esm/MenuBar/context.d.ts +29 -0
  76. package/build/esm/MenuBar/context.d.ts.map +1 -0
  77. package/build/esm/MenuBar/context.js +7 -0
  78. package/build/esm/MenuBar/context.js.map +1 -0
  79. package/build/esm/MenuBar/index.d.ts +2 -0
  80. package/build/esm/MenuBar/index.d.ts.map +1 -0
  81. package/build/esm/MenuBar/index.js +2 -0
  82. package/build/esm/MenuBar/index.js.map +1 -0
  83. package/build/esm/Slider/Slider.d.ts +47 -1
  84. package/build/esm/Slider/Slider.d.ts.map +1 -1
  85. package/build/esm/Slider/Slider.js +91 -5
  86. package/build/esm/Slider/Slider.js.map +1 -1
  87. package/build/esm/ToggleGroup/ToggleGroup.d.ts +40 -0
  88. package/build/esm/ToggleGroup/ToggleGroup.d.ts.map +1 -0
  89. package/build/esm/ToggleGroup/ToggleGroup.js +113 -0
  90. package/build/esm/ToggleGroup/ToggleGroup.js.map +1 -0
  91. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts +10 -0
  92. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts.map +1 -0
  93. package/build/esm/ToggleGroup/ToggleGroupContext.js +6 -0
  94. package/build/esm/ToggleGroup/ToggleGroupContext.js.map +1 -0
  95. package/build/esm/ToggleGroup/index.d.ts +3 -0
  96. package/build/esm/ToggleGroup/index.d.ts.map +1 -0
  97. package/build/esm/ToggleGroup/index.js +3 -0
  98. package/build/esm/ToggleGroup/index.js.map +1 -0
  99. package/build/esm/Tree/Tree.d.ts +3 -0
  100. package/build/esm/Tree/Tree.d.ts.map +1 -0
  101. package/build/esm/Tree/Tree.js +730 -0
  102. package/build/esm/Tree/Tree.js.map +1 -0
  103. package/build/esm/Tree/TreeHeader.d.ts +3 -0
  104. package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
  105. package/build/esm/Tree/TreeHeader.js +5 -0
  106. package/build/esm/Tree/TreeHeader.js.map +1 -0
  107. package/build/esm/Tree/TreeItem.d.ts +3 -0
  108. package/build/esm/Tree/TreeItem.d.ts.map +1 -0
  109. package/build/esm/Tree/TreeItem.js +5 -0
  110. package/build/esm/Tree/TreeItem.js.map +1 -0
  111. package/build/esm/Tree/TreeItemContent.d.ts +3 -0
  112. package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
  113. package/build/esm/Tree/TreeItemContent.js +69 -0
  114. package/build/esm/Tree/TreeItemContent.js.map +1 -0
  115. package/build/esm/Tree/TreeSection.d.ts +3 -0
  116. package/build/esm/Tree/TreeSection.d.ts.map +1 -0
  117. package/build/esm/Tree/TreeSection.js +5 -0
  118. package/build/esm/Tree/TreeSection.js.map +1 -0
  119. package/build/esm/Tree/collection.d.ts +18 -0
  120. package/build/esm/Tree/collection.d.ts.map +1 -0
  121. package/build/esm/Tree/collection.js +252 -0
  122. package/build/esm/Tree/collection.js.map +1 -0
  123. package/build/esm/Tree/context.d.ts +3 -0
  124. package/build/esm/Tree/context.d.ts.map +1 -0
  125. package/build/esm/Tree/context.js +3 -0
  126. package/build/esm/Tree/context.js.map +1 -0
  127. package/build/esm/Tree/index.d.ts +8 -0
  128. package/build/esm/Tree/index.d.ts.map +1 -0
  129. package/build/esm/Tree/index.js +7 -0
  130. package/build/esm/Tree/index.js.map +1 -0
  131. package/build/esm/Tree/types.d.ts +128 -0
  132. package/build/esm/Tree/types.d.ts.map +1 -0
  133. package/build/esm/Tree/types.js +2 -0
  134. package/build/esm/Tree/types.js.map +1 -0
  135. package/build/esm/hooks/index.d.ts +1 -0
  136. package/build/esm/hooks/index.d.ts.map +1 -1
  137. package/build/esm/hooks/index.js +1 -0
  138. package/build/esm/hooks/index.js.map +1 -1
  139. package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
  140. package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
  141. package/build/esm/hooks/useTransitionStatus.js +48 -0
  142. package/build/esm/hooks/useTransitionStatus.js.map +1 -0
  143. package/build/esm/index.d.ts +5 -0
  144. package/build/esm/index.d.ts.map +1 -1
  145. package/build/esm/index.js +5 -0
  146. package/build/esm/index.js.map +1 -1
  147. package/build/esm/toggle/Toggle.d.ts +28 -0
  148. package/build/esm/toggle/Toggle.d.ts.map +1 -0
  149. package/build/esm/toggle/Toggle.js +55 -0
  150. package/build/esm/toggle/Toggle.js.map +1 -0
  151. package/build/esm/toggle/index.d.ts +2 -0
  152. package/build/esm/toggle/index.d.ts.map +1 -0
  153. package/build/esm/toggle/index.js +2 -0
  154. package/build/esm/toggle/index.js.map +1 -0
  155. package/build/esm/utils/assign-ref.d.ts +3 -3
  156. package/build/esm/utils/assign-ref.d.ts.map +1 -1
  157. package/build/esm/utils/assign-ref.js +1 -1
  158. package/build/esm/utils/assign-ref.js.map +1 -1
  159. package/build/tsconfig-build.tsbuildinfo +1 -1
  160. package/build/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +7 -4
  162. package/src/Accordion/AccordionBody.tsx +6 -35
  163. package/src/Accordion/AccordionHeader.tsx +29 -103
  164. package/src/Accordion/AccordionItem.tsx +40 -29
  165. package/src/Accordion/context.ts +0 -18
  166. package/src/Accordion/scopeQuery.ts +4 -0
  167. package/src/Collapsible/Collapsible.story.tsx +153 -0
  168. package/src/Collapsible/Collapsible.tsx +79 -0
  169. package/src/Collapsible/CollapsiblePanel.tsx +103 -0
  170. package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
  171. package/src/Collapsible/context.ts +28 -0
  172. package/src/Collapsible/index.ts +3 -0
  173. package/src/Menu/Menu.story.tsx +70 -1
  174. package/src/Menu/Menu.tsx +141 -65
  175. package/src/Menu/MenuButton.tsx +115 -9
  176. package/src/Menu/MenuItem.tsx +20 -3
  177. package/src/Menu/MenuList.tsx +50 -13
  178. package/src/Menu/MenuPopover.tsx +12 -2
  179. package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
  180. package/src/Menu/context.ts +20 -10
  181. package/src/Menu/index.ts +3 -0
  182. package/src/Menu/scope.ts +4 -1
  183. package/src/Menu/styles.css +57 -22
  184. package/src/MenuBar/MenuBar.story.tsx +92 -0
  185. package/src/MenuBar/MenuBar.tsx +236 -0
  186. package/src/MenuBar/context.ts +46 -0
  187. package/src/MenuBar/index.ts +1 -0
  188. package/src/MenuBar/styles.css +78 -0
  189. package/src/Slider/Slider.story.tsx +1 -1
  190. package/src/Slider/Slider.tsx +145 -8
  191. package/src/Toggle/Toggle.story.tsx +42 -0
  192. package/src/Toggle/Toggle.tsx +95 -0
  193. package/src/Toggle/index.ts +1 -0
  194. package/src/Toggle/styles.css +39 -0
  195. package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
  196. package/src/ToggleGroup/ToggleGroup.tsx +185 -0
  197. package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
  198. package/src/ToggleGroup/index.ts +2 -0
  199. package/src/ToggleGroup/styles.css +66 -0
  200. package/src/Tree/Tree.story.tsx +221 -0
  201. package/src/Tree/Tree.tsx +1081 -0
  202. package/src/Tree/TreeHeader.tsx +9 -0
  203. package/src/Tree/TreeItem.tsx +9 -0
  204. package/src/Tree/TreeItemContent.tsx +91 -0
  205. package/src/Tree/TreeSection.tsx +9 -0
  206. package/src/Tree/collection.tsx +371 -0
  207. package/src/Tree/context.ts +6 -0
  208. package/src/Tree/index.ts +7 -0
  209. package/src/Tree/styles.css +135 -0
  210. package/src/Tree/types.ts +161 -0
  211. package/src/hooks/index.ts +1 -0
  212. package/src/hooks/useTransitionStatus.ts +65 -0
  213. package/src/index.ts +5 -0
  214. package/src/utils/assign-ref.ts +4 -4
@@ -0,0 +1,1081 @@
1
+ import type {
2
+ CSSProperties,
3
+ FocusEvent,
4
+ KeyboardEvent,
5
+ MouseEvent,
6
+ ReactElement,
7
+ ReactNode,
8
+ } from 'react';
9
+ import {
10
+ forwardRef,
11
+ useCallback,
12
+ useEffect,
13
+ useId,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from 'react';
18
+
19
+ import {
20
+ flattenAllItems,
21
+ parseTreeChildren,
22
+ shouldRenderLoadingRow,
23
+ useTreeCollection,
24
+ } from './collection';
25
+ import { TreeItemRenderContext } from './context';
26
+ import type {
27
+ ParsedTreeItem,
28
+ ParsedTreeNode,
29
+ ParsedTreeSection,
30
+ TreeDisabledBehavior,
31
+ TreeExpandedKeys,
32
+ TreeHeaderProps,
33
+ TreeItemProps,
34
+ TreeItemRenderContextValue,
35
+ TreeKey,
36
+ TreeProps,
37
+ TreeRenderState,
38
+ TreeSectionProps,
39
+ TreeSelection,
40
+ TreeVisibleRow,
41
+ } from './types';
42
+ import { assignMultipleRefs, wrapEvent } from '../utils';
43
+
44
+ const TYPEAHEAD_TIMEOUT = 500;
45
+
46
+ export const Tree = forwardRef<HTMLDivElement, TreeProps>(function Tree(
47
+ props,
48
+ forwardedRef
49
+ ) {
50
+ const {
51
+ as: Comp = 'div',
52
+ innerAs,
53
+ children,
54
+ selectionMode = 'none',
55
+ selectionBehavior = 'toggle',
56
+ selectedKeys: selectedKeysProp,
57
+ defaultSelectedKeys,
58
+ onSelectionChange,
59
+ disallowEmptySelection = false,
60
+ disabledKeys: disabledKeysProp,
61
+ disabledBehavior = 'all',
62
+ expandedKeys: expandedKeysProp,
63
+ defaultExpandedKeys,
64
+ onExpandedChange,
65
+ autoFocus = false,
66
+ onAction,
67
+ keyboardNavigationBehavior = 'arrow',
68
+ escapeKeyBehavior = 'clearSelection',
69
+ shouldFocusWrap = false,
70
+ disallowTypeAhead = false,
71
+ disallowSelectAll = false,
72
+ direction: directionProp,
73
+ renderEmptyState,
74
+ renderLoadingState,
75
+ renderedRowIndices,
76
+ onVisibleRowsChange,
77
+ onFocusedKeyChange,
78
+ renderRow,
79
+ onKeyDown,
80
+ onFocus,
81
+ onBlur,
82
+ onMouseDown,
83
+ id: idProp,
84
+ tabIndex,
85
+ ...otherProps
86
+ } = props;
87
+
88
+ const generatedId = useId();
89
+ const treeId = idProp ?? generatedId;
90
+ const rootRef = useRef<HTMLDivElement | null>(null);
91
+ const itemRefs = useRef(new Map<TreeKey, HTMLDivElement>());
92
+ const typeaheadStringRef = useRef('');
93
+ const typeaheadTimeoutRef = useRef<
94
+ ReturnType<typeof setTimeout> | undefined
95
+ >(undefined);
96
+ const [isFocused, setFocused] = useState(false);
97
+ const [isFocusVisible, setFocusVisible] = useState(false);
98
+ const [focusedKey, setFocusedKey] = useState<TreeKey | null>(null);
99
+ const [pressedKey, setPressedKey] = useState<TreeKey | null>(null);
100
+ const lastSelectedKeyRef = useRef<TreeKey | null>(null);
101
+
102
+ const disabledKeys = useMemo(
103
+ () => new Set(disabledKeysProp ?? []),
104
+ [disabledKeysProp]
105
+ );
106
+
107
+ const parsedTree = useMemo(() => parseTreeChildren(children), [children]);
108
+ const allItems = useMemo(() => flattenAllItems(parsedTree), [parsedTree]);
109
+ const allItemKeys = useMemo(() => allItems.map((item) => item.key), [allItems]);
110
+ const expandableKeys = useMemo(
111
+ () =>
112
+ allItems
113
+ .filter((item) => item.hasChildItems || item.childItems.length > 0)
114
+ .map((item) => item.key),
115
+ [allItems]
116
+ );
117
+
118
+ const [uncontrolledExpandedKeys, setUncontrolledExpandedKeys] = useState(
119
+ () => makeKeySet(defaultExpandedKeys, expandableKeys)
120
+ );
121
+ const expandedKeys =
122
+ expandedKeysProp !== undefined
123
+ ? makeKeySet(expandedKeysProp, expandableKeys)
124
+ : uncontrolledExpandedKeys;
125
+
126
+ const [uncontrolledSelectedKeys, setUncontrolledSelectedKeys] = useState(() =>
127
+ makeKeySet(defaultSelectedKeys, allItemKeys)
128
+ );
129
+ const selectedKeys =
130
+ selectedKeysProp !== undefined
131
+ ? makeKeySet(selectedKeysProp, allItemKeys)
132
+ : uncontrolledSelectedKeys;
133
+
134
+ const treeCollection = useTreeCollection(children, {
135
+ expandedKeys,
136
+ renderLoadingState,
137
+ });
138
+ const visibleItems = treeCollection.visibleItems;
139
+ const visibleRows = treeCollection.visibleRows;
140
+
141
+ const focusableItems = useMemo(
142
+ () =>
143
+ visibleItems.filter(
144
+ (item) => !(disabledBehavior === 'all' && isItemDisabled(item, disabledKeys))
145
+ ),
146
+ [disabledBehavior, disabledKeys, visibleItems]
147
+ );
148
+
149
+ const direction =
150
+ directionProp ??
151
+ (typeof document !== 'undefined' && document.dir === 'rtl' ? 'rtl' : 'ltr');
152
+ const expandKey = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
153
+ const collapseKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
154
+
155
+ const setExpandedKeys = useCallback(
156
+ (keys: Set<TreeKey>) => {
157
+ if (expandedKeysProp === undefined) {
158
+ setUncontrolledExpandedKeys(keys);
159
+ }
160
+ onExpandedChange?.(new Set(keys));
161
+ },
162
+ [expandedKeysProp, onExpandedChange]
163
+ );
164
+
165
+ const setSelectedKeys = useCallback(
166
+ (keys: Set<TreeKey>) => {
167
+ if (selectedKeysProp === undefined) {
168
+ setUncontrolledSelectedKeys(keys);
169
+ }
170
+ onSelectionChange?.(new Set(keys));
171
+ },
172
+ [onSelectionChange, selectedKeysProp]
173
+ );
174
+
175
+ const getItemByKey = useCallback(
176
+ (key: TreeKey | null) =>
177
+ key == null ? undefined : allItems.find((item) => item.key === key),
178
+ [allItems]
179
+ );
180
+
181
+ const canSelectItem = useCallback(
182
+ (item: ParsedTreeItem) =>
183
+ selectionMode !== 'none' && !isItemDisabled(item, disabledKeys),
184
+ [disabledKeys, selectionMode]
185
+ );
186
+
187
+ const focusItem = useCallback(
188
+ (key: TreeKey | null) => {
189
+ setFocusedKey(key);
190
+ if (key == null) {
191
+ onFocusedKeyChange?.(null, null);
192
+ return;
193
+ }
194
+
195
+ const rowIndex = visibleRows.findIndex(
196
+ (row) => row.type === 'item' && row.item.key === key
197
+ );
198
+ onFocusedKeyChange?.(key, rowIndex === -1 ? null : rowIndex);
199
+
200
+ requestAnimationFrame(() => {
201
+ itemRefs.current.get(key)?.focus({ preventScroll: true });
202
+ });
203
+ },
204
+ [onFocusedKeyChange, visibleRows]
205
+ );
206
+
207
+ const updateSelection = useCallback(
208
+ (
209
+ item: ParsedTreeItem,
210
+ event?: MouseEvent<any> | KeyboardEvent<any>,
211
+ options?: { forceToggle?: boolean; range?: boolean }
212
+ ) => {
213
+ if (!canSelectItem(item)) {
214
+ return;
215
+ }
216
+
217
+ let next = new Set(selectedKeys);
218
+ const isSelected = next.has(item.key);
219
+
220
+ if (selectionMode === 'single') {
221
+ if (isSelected && !disallowEmptySelection) {
222
+ next.delete(item.key);
223
+ } else {
224
+ next = new Set([item.key]);
225
+ }
226
+ } else if (selectionMode === 'multiple') {
227
+ if (
228
+ options?.range &&
229
+ lastSelectedKeyRef.current != null &&
230
+ visibleItems.some((visibleItem) => visibleItem.key === lastSelectedKeyRef.current)
231
+ ) {
232
+ next = addRangeToSelection(
233
+ visibleItems,
234
+ lastSelectedKeyRef.current,
235
+ item.key,
236
+ selectedKeys,
237
+ (candidate) => canSelectItem(candidate)
238
+ );
239
+ } else if (
240
+ options?.forceToggle ||
241
+ selectionBehavior === 'toggle' ||
242
+ event?.ctrlKey ||
243
+ event?.metaKey
244
+ ) {
245
+ if (isSelected) {
246
+ if (!disallowEmptySelection || next.size > 1) {
247
+ next.delete(item.key);
248
+ }
249
+ } else {
250
+ next.add(item.key);
251
+ }
252
+ } else {
253
+ next = new Set([item.key]);
254
+ }
255
+ }
256
+
257
+ lastSelectedKeyRef.current = item.key;
258
+ setSelectedKeys(next);
259
+ },
260
+ [
261
+ canSelectItem,
262
+ disallowEmptySelection,
263
+ selectedKeys,
264
+ selectionBehavior,
265
+ selectionMode,
266
+ setSelectedKeys,
267
+ visibleItems,
268
+ ]
269
+ );
270
+
271
+ const toggleExpanded = useCallback(
272
+ (item: ParsedTreeItem) => {
273
+ if (!item.hasChildItems && item.childItems.length === 0) {
274
+ return;
275
+ }
276
+
277
+ const next = new Set(expandedKeys);
278
+ if (next.has(item.key)) {
279
+ next.delete(item.key);
280
+ } else {
281
+ next.add(item.key);
282
+ }
283
+ setExpandedKeys(next);
284
+ },
285
+ [expandedKeys, setExpandedKeys]
286
+ );
287
+
288
+ const navigateByIndex = useCallback(
289
+ (currentKey: TreeKey | null, delta: number) => {
290
+ if (focusableItems.length === 0) {
291
+ return;
292
+ }
293
+
294
+ const currentIndex = focusableItems.findIndex(
295
+ (item) => item.key === currentKey
296
+ );
297
+ let nextIndex = currentIndex === -1 ? (delta > 0 ? 0 : -1) : currentIndex + delta;
298
+
299
+ if (nextIndex < 0 || nextIndex >= focusableItems.length) {
300
+ if (!shouldFocusWrap) {
301
+ return;
302
+ }
303
+ nextIndex =
304
+ ((nextIndex % focusableItems.length) + focusableItems.length) %
305
+ focusableItems.length;
306
+ }
307
+
308
+ focusItem(focusableItems[nextIndex].key);
309
+ },
310
+ [focusItem, focusableItems, shouldFocusWrap]
311
+ );
312
+
313
+ const navigateToEdge = useCallback(
314
+ (edge: 'first' | 'last') => {
315
+ const item =
316
+ edge === 'first' ? focusableItems[0] : focusableItems[focusableItems.length - 1];
317
+ if (item) {
318
+ focusItem(item.key);
319
+ }
320
+ },
321
+ [focusItem, focusableItems]
322
+ );
323
+
324
+ const handleTypeahead = useCallback(
325
+ (event: KeyboardEvent<HTMLDivElement>) => {
326
+ if (disallowTypeAhead || event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) {
327
+ return false;
328
+ }
329
+
330
+ event.preventDefault();
331
+ const key = event.key.toLocaleLowerCase();
332
+ if (
333
+ typeaheadStringRef.current.length === 0 ||
334
+ typeaheadStringRef.current.slice(-1) !== key
335
+ ) {
336
+ typeaheadStringRef.current += key;
337
+ }
338
+
339
+ clearTimeout(typeaheadTimeoutRef.current);
340
+ typeaheadTimeoutRef.current = setTimeout(() => {
341
+ typeaheadStringRef.current = '';
342
+ }, TYPEAHEAD_TIMEOUT);
343
+
344
+ const search = typeaheadStringRef.current;
345
+ const currentIndex = focusableItems.findIndex(
346
+ (item) => item.key === focusedKey
347
+ );
348
+ for (
349
+ let i = search.length === 1 ? 1 : 0;
350
+ i <= focusableItems.length;
351
+ i++
352
+ ) {
353
+ const index =
354
+ ((currentIndex + i) % focusableItems.length + focusableItems.length) %
355
+ focusableItems.length;
356
+ const item = focusableItems[index];
357
+ if (item?.textValue.toLocaleLowerCase().startsWith(search)) {
358
+ focusItem(item.key);
359
+ return true;
360
+ }
361
+ }
362
+
363
+ return true;
364
+ },
365
+ [disallowTypeAhead, focusItem, focusableItems, focusedKey]
366
+ );
367
+
368
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
369
+ const target = event.target;
370
+ if (!(target instanceof Node) || !rootRef.current?.contains(target)) {
371
+ return;
372
+ }
373
+
374
+ setFocusVisible(true);
375
+
376
+ switch (event.key) {
377
+ case 'ArrowDown':
378
+ event.preventDefault();
379
+ navigateByIndex(focusedKey, 1);
380
+ break;
381
+ case 'ArrowUp':
382
+ event.preventDefault();
383
+ navigateByIndex(focusedKey, -1);
384
+ break;
385
+ case 'Home':
386
+ event.preventDefault();
387
+ navigateToEdge('first');
388
+ break;
389
+ case 'End':
390
+ event.preventDefault();
391
+ navigateToEdge('last');
392
+ break;
393
+ case 'a':
394
+ if (
395
+ (event.ctrlKey || event.metaKey) &&
396
+ selectionMode === 'multiple' &&
397
+ !disallowSelectAll
398
+ ) {
399
+ event.preventDefault();
400
+ setSelectedKeys(
401
+ new Set(
402
+ allItems
403
+ .filter((item) => canSelectItem(item))
404
+ .map((item) => item.key)
405
+ )
406
+ );
407
+ } else {
408
+ handleTypeahead(event);
409
+ }
410
+ break;
411
+ case 'Escape':
412
+ if (
413
+ escapeKeyBehavior === 'clearSelection' &&
414
+ !disallowEmptySelection &&
415
+ selectedKeys.size > 0
416
+ ) {
417
+ event.preventDefault();
418
+ event.stopPropagation();
419
+ setSelectedKeys(new Set());
420
+ }
421
+ break;
422
+ case ' ':
423
+ if (focusedKey != null && event.target === itemRefs.current.get(focusedKey)) {
424
+ const item = getItemByKey(focusedKey);
425
+ if (item) {
426
+ event.preventDefault();
427
+ updateSelection(item, event, {
428
+ forceToggle: true,
429
+ range: event.shiftKey,
430
+ });
431
+ }
432
+ }
433
+ break;
434
+ case 'Enter':
435
+ if (focusedKey != null && event.target === itemRefs.current.get(focusedKey)) {
436
+ const item = getItemByKey(focusedKey);
437
+ if (item && !isInteractionDisabled(item, disabledKeys, disabledBehavior)) {
438
+ event.preventDefault();
439
+ if (item.props.onAction) {
440
+ item.props.onAction();
441
+ } else if (onAction) {
442
+ onAction(item.key);
443
+ } else if (selectionMode === 'none' && item.hasChildItems) {
444
+ toggleExpanded(item);
445
+ }
446
+ }
447
+ }
448
+ break;
449
+ default:
450
+ handleTypeahead(event);
451
+ break;
452
+ }
453
+ };
454
+
455
+ const handleFocus = (event: FocusEvent<HTMLDivElement>) => {
456
+ if (!rootRef.current?.contains(event.target)) {
457
+ return;
458
+ }
459
+
460
+ setFocused(true);
461
+ if (event.target === rootRef.current) {
462
+ const key =
463
+ focusedKey ??
464
+ firstVisibleSelectedKey(visibleItems, selectedKeys) ??
465
+ focusableItems[0]?.key ??
466
+ null;
467
+ focusItem(key);
468
+ }
469
+ };
470
+
471
+ const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
472
+ if (
473
+ event.relatedTarget instanceof Node &&
474
+ rootRef.current?.contains(event.relatedTarget)
475
+ ) {
476
+ return;
477
+ }
478
+
479
+ setFocused(false);
480
+ };
481
+
482
+ const handleMouseDown = () => {
483
+ setFocusVisible(false);
484
+ };
485
+
486
+ useEffect(() => {
487
+ if (focusedKey != null && !focusableItems.some((item) => item.key === focusedKey)) {
488
+ setFocusedKey(focusableItems[0]?.key ?? null);
489
+ }
490
+ }, [focusableItems, focusedKey]);
491
+
492
+ useEffect(() => {
493
+ onVisibleRowsChange?.(visibleRows);
494
+ }, [onVisibleRowsChange, visibleRows]);
495
+
496
+ useEffect(() => {
497
+ if (!autoFocus) {
498
+ return;
499
+ }
500
+
501
+ const key =
502
+ autoFocus === 'last'
503
+ ? focusableItems[focusableItems.length - 1]?.key
504
+ : firstVisibleSelectedKey(visibleItems, selectedKeys) ??
505
+ focusableItems[0]?.key;
506
+ if (key != null) {
507
+ setFocused(true);
508
+ focusItem(key);
509
+ } else {
510
+ rootRef.current?.focus();
511
+ }
512
+ // eslint-disable-next-line react-hooks/exhaustive-deps
513
+ }, []);
514
+
515
+ useEffect(() => {
516
+ return () => {
517
+ clearTimeout(typeaheadTimeoutRef.current);
518
+ };
519
+ }, []);
520
+
521
+ if (process.env.NODE_ENV !== 'production') {
522
+ if (!otherProps['aria-label'] && !otherProps['aria-labelledby']) {
523
+ console.warn(
524
+ 'An aria-label or aria-labelledby prop is required for <Tree> accessibility.'
525
+ );
526
+ }
527
+ }
528
+
529
+ const renderState: TreeRenderState = {
530
+ allItems,
531
+ visibleItems,
532
+ visibleRows,
533
+ focusedKey,
534
+ expandedKeys,
535
+ selectedKeys,
536
+ selectionMode,
537
+ selectionBehavior,
538
+ };
539
+
540
+ const renderItem = (
541
+ item: ParsedTreeItem,
542
+ options: {
543
+ rowIndex?: number;
544
+ virtualIndex?: number;
545
+ measureElement?: (element: HTMLElement | null) => void;
546
+ } = {}
547
+ ) => {
548
+ const isDisabled = isInteractionDisabled(item, disabledKeys, disabledBehavior);
549
+ const isExpanded = expandedKeys.has(item.key);
550
+ const isSelected = selectedKeys.has(item.key);
551
+ const rowId = `${treeId}-row-${sanitizeId(item.keyString)}`;
552
+ const rowProps = getItemDOMProps(item.props);
553
+ const canSelect = canSelectItem(item);
554
+ const itemOnClick = rowProps.onClick;
555
+ const itemOnDoubleClick = rowProps.onDoubleClick;
556
+ const itemOnFocus = rowProps.onFocus;
557
+ const itemOnKeyDownCapture = rowProps.onKeyDownCapture;
558
+ const itemOnMouseDown = rowProps.onMouseDown;
559
+ const itemOnMouseUp = rowProps.onMouseUp;
560
+ const itemOnMouseLeave = rowProps.onMouseLeave;
561
+ const itemOnKeyDown = rowProps.onKeyDown;
562
+
563
+ const contextValue: TreeItemRenderContextValue = {
564
+ item,
565
+ rowId,
566
+ isExpanded,
567
+ isSelected,
568
+ isDisabled,
569
+ canSelect,
570
+ selectionMode,
571
+ selectionBehavior,
572
+ toggleExpanded: (event) => {
573
+ event.preventDefault();
574
+ event.stopPropagation();
575
+ if (!isDisabled) {
576
+ toggleExpanded(item);
577
+ focusItem(item.key);
578
+ }
579
+ },
580
+ toggleSelection: (event) => {
581
+ event.stopPropagation();
582
+ updateSelection(item, event, { forceToggle: true });
583
+ focusItem(item.key);
584
+ },
585
+ };
586
+
587
+ const handleRowKeyDownCapture = (event: KeyboardEvent<HTMLDivElement>) => {
588
+ if (!rootRef.current || !itemRefs.current.get(item.key)) {
589
+ return;
590
+ }
591
+
592
+ const row = itemRefs.current.get(item.key)!;
593
+ const activeElement = getActiveElement(row);
594
+
595
+ if (activeElement === row) {
596
+ if (event.key === expandKey && item.hasChildItems && !isExpanded && !isDisabled) {
597
+ event.preventDefault();
598
+ event.stopPropagation();
599
+ toggleExpanded(item);
600
+ } else if (event.key === collapseKey) {
601
+ if (item.hasChildItems && isExpanded && !isDisabled) {
602
+ event.preventDefault();
603
+ event.stopPropagation();
604
+ toggleExpanded(item);
605
+ } else if (item.parentKey != null) {
606
+ event.preventDefault();
607
+ event.stopPropagation();
608
+ focusItem(item.parentKey);
609
+ }
610
+ }
611
+ return;
612
+ }
613
+
614
+ if (
615
+ keyboardNavigationBehavior === 'arrow' &&
616
+ activeElement instanceof HTMLElement &&
617
+ row.contains(activeElement) &&
618
+ (event.key === 'ArrowLeft' || event.key === 'ArrowRight')
619
+ ) {
620
+ const focusables = getFocusableElements(row);
621
+ const currentIndex = focusables.indexOf(activeElement);
622
+ const delta =
623
+ (event.key === 'ArrowRight' && direction === 'ltr') ||
624
+ (event.key === 'ArrowLeft' && direction === 'rtl')
625
+ ? 1
626
+ : -1;
627
+ const next = focusables[currentIndex + delta] ?? row;
628
+ event.preventDefault();
629
+ event.stopPropagation();
630
+ next.focus({ preventScroll: true });
631
+ }
632
+ };
633
+
634
+ const handleRowClick = (event: MouseEvent<HTMLDivElement>) => {
635
+ if (isDisabled || isInteractiveEventTarget(event, itemRefs.current.get(item.key))) {
636
+ return;
637
+ }
638
+
639
+ focusItem(item.key);
640
+
641
+ if (selectionMode === 'none') {
642
+ if (item.props.onAction) {
643
+ item.props.onAction();
644
+ } else if (onAction) {
645
+ onAction(item.key);
646
+ } else if (item.hasChildItems) {
647
+ toggleExpanded(item);
648
+ }
649
+ return;
650
+ }
651
+
652
+ updateSelection(item, event, { range: event.shiftKey });
653
+ };
654
+
655
+ const handleRowDoubleClick = () => {
656
+ if (
657
+ selectionBehavior === 'replace' &&
658
+ !isDisabled &&
659
+ (item.props.onAction || onAction)
660
+ ) {
661
+ item.props.onAction?.();
662
+ if (!item.props.onAction) {
663
+ onAction?.(item.key);
664
+ }
665
+ }
666
+ };
667
+
668
+ const handleRowFocus = () => {
669
+ setFocusedKey(item.key);
670
+ };
671
+
672
+ return (
673
+ <TreeItemRenderContext.Provider key={item.keyString} value={contextValue}>
674
+ <div
675
+ {...rowProps}
676
+ ref={(node) => {
677
+ options.measureElement?.(node);
678
+ if (node) {
679
+ itemRefs.current.set(item.key, node);
680
+ } else {
681
+ itemRefs.current.delete(item.key);
682
+ }
683
+ }}
684
+ id={rowId}
685
+ role="row"
686
+ data-index={options.virtualIndex}
687
+ aria-rowindex={options.rowIndex}
688
+ tabIndex={focusedKey === item.key ? 0 : -1}
689
+ aria-label={(item.props['aria-label'] ?? item.textValue) || undefined}
690
+ style={
691
+ {
692
+ ...rowProps.style,
693
+ '--tree-item-level': item.level,
694
+ } as CSSProperties
695
+ }
696
+ aria-expanded={item.hasChildItems ? isExpanded : undefined}
697
+ aria-level={item.level}
698
+ aria-posinset={item.posInSet}
699
+ aria-setsize={item.setSize}
700
+ aria-selected={canSelect ? isSelected : undefined}
701
+ aria-disabled={isDisabled || undefined}
702
+ data-tree-item=""
703
+ data-key={item.keyString}
704
+ data-expanded={item.hasChildItems && isExpanded ? '' : undefined}
705
+ data-has-child-items={item.hasChildItems ? '' : undefined}
706
+ data-level={item.level}
707
+ data-selected={isSelected ? '' : undefined}
708
+ data-disabled={isDisabled ? '' : undefined}
709
+ data-focused={focusedKey === item.key && isFocused ? '' : undefined}
710
+ data-focus-visible={
711
+ focusedKey === item.key && isFocusVisible ? '' : undefined
712
+ }
713
+ data-pressed={pressedKey === item.key ? '' : undefined}
714
+ data-selection-mode={
715
+ selectionMode === 'none' ? undefined : selectionMode
716
+ }
717
+ onClick={wrapEvent(itemOnClick as any, handleRowClick)}
718
+ onDoubleClick={wrapEvent(
719
+ itemOnDoubleClick as any,
720
+ handleRowDoubleClick
721
+ )}
722
+ onFocus={wrapEvent(itemOnFocus as any, handleRowFocus)}
723
+ onKeyDownCapture={wrapEvent(
724
+ itemOnKeyDownCapture as any,
725
+ handleRowKeyDownCapture
726
+ )}
727
+ onKeyDown={itemOnKeyDown}
728
+ onMouseDown={wrapEvent(itemOnMouseDown as any, () => {
729
+ if (!isDisabled) {
730
+ setPressedKey(item.key);
731
+ }
732
+ })}
733
+ onMouseUp={wrapEvent(itemOnMouseUp as any, () => {
734
+ setPressedKey(null);
735
+ })}
736
+ onMouseLeave={wrapEvent(itemOnMouseLeave as any, () => {
737
+ setPressedKey(null);
738
+ })}
739
+ >
740
+ <div role="gridcell" aria-colindex={1}>
741
+ {renderTreeItemContent(item, contextValue)}
742
+ </div>
743
+ </div>
744
+ </TreeItemRenderContext.Provider>
745
+ );
746
+ };
747
+
748
+ const renderItemBranch = (item: ParsedTreeItem): ReactNode[] => {
749
+ const rows: ReactNode[] = [renderItem(item)];
750
+ if (expandedKeys.has(item.key)) {
751
+ item.childItems.forEach((childItem) => {
752
+ rows.push(...renderItemBranch(childItem));
753
+ });
754
+ if (
755
+ shouldRenderLoadingRow(item, renderLoadingState)
756
+ ) {
757
+ rows.push(renderLoadingRow(item));
758
+ }
759
+ }
760
+ return rows;
761
+ };
762
+
763
+ const renderItemBranches = (items: ParsedTreeItem[]) => {
764
+ const rows: ReactNode[] = [];
765
+ items.forEach((item) => {
766
+ rows.push(...renderItemBranch(item));
767
+ });
768
+ return rows;
769
+ };
770
+
771
+ const renderNode = (node: ParsedTreeNode) => {
772
+ if (node.type === 'item') {
773
+ return renderItemBranch(node);
774
+ }
775
+
776
+ const sectionDOMProps = getSectionDOMProps(node.sectionProps);
777
+ const headerId = `${treeId}-section-${sanitizeId(node.key)}-header`;
778
+ return (
779
+ <div
780
+ {...sectionDOMProps}
781
+ key={node.key}
782
+ role="rowgroup"
783
+ aria-labelledby={node.header ? headerId : undefined}
784
+ aria-label={
785
+ node.header ? undefined : node.sectionProps['aria-label'] ?? undefined
786
+ }
787
+ >
788
+ {node.header ? (
789
+ <div role="row">
790
+ <div
791
+ {...getHeaderDOMProps(node.headerProps)}
792
+ id={headerId}
793
+ role="rowheader"
794
+ >
795
+ {node.header}
796
+ </div>
797
+ </div>
798
+ ) : null}
799
+ {renderItemBranches(node.items)}
800
+ </div>
801
+ );
802
+ };
803
+
804
+ const renderSectionRow = (
805
+ node: ParsedTreeSection,
806
+ options: {
807
+ rowIndex?: number;
808
+ virtualIndex?: number;
809
+ measureElement?: (element: HTMLElement | null) => void;
810
+ } = {}
811
+ ) => {
812
+ const headerId = `${treeId}-section-${sanitizeId(node.key)}-header`;
813
+ return (
814
+ <div
815
+ key={node.key}
816
+ ref={options.measureElement}
817
+ role="row"
818
+ data-index={options.virtualIndex}
819
+ aria-rowindex={options.rowIndex}
820
+ >
821
+ <div
822
+ {...getHeaderDOMProps(node.headerProps)}
823
+ id={headerId}
824
+ role="rowheader"
825
+ >
826
+ {node.header}
827
+ </div>
828
+ </div>
829
+ );
830
+ };
831
+
832
+ const renderLoadingRow = (
833
+ item: ParsedTreeItem,
834
+ options: {
835
+ rowIndex?: number;
836
+ virtualIndex?: number;
837
+ measureElement?: (element: HTMLElement | null) => void;
838
+ } = {}
839
+ ) => (
840
+ <div
841
+ key={`${item.keyString}-loading`}
842
+ ref={options.measureElement}
843
+ role="row"
844
+ data-index={options.virtualIndex}
845
+ aria-level={item.level + 1}
846
+ aria-rowindex={options.rowIndex}
847
+ data-tree-loading-row=""
848
+ data-level={item.level + 1}
849
+ style={
850
+ {
851
+ '--tree-item-level': item.level + 1,
852
+ } as CSSProperties
853
+ }
854
+ >
855
+ <div role="gridcell" aria-colindex={1}>
856
+ {renderLoadingState?.(item)}
857
+ </div>
858
+ </div>
859
+ );
860
+
861
+ const renderVisibleRow = (
862
+ row: TreeVisibleRow,
863
+ index: number,
864
+ measureElement?: (element: HTMLElement | null) => void
865
+ ) => {
866
+ let defaultRow: ReactElement;
867
+ if (row.type === 'item') {
868
+ defaultRow = renderItem(row.item, {
869
+ rowIndex: index + 1,
870
+ virtualIndex: index,
871
+ measureElement,
872
+ });
873
+ } else if (row.type === 'section') {
874
+ defaultRow = renderSectionRow(row.section, {
875
+ rowIndex: index + 1,
876
+ virtualIndex: index,
877
+ measureElement,
878
+ });
879
+ } else {
880
+ defaultRow = renderLoadingRow(row.item, {
881
+ rowIndex: index + 1,
882
+ virtualIndex: index,
883
+ measureElement,
884
+ });
885
+ }
886
+
887
+ return renderRow?.(row, defaultRow, renderState) ?? defaultRow;
888
+ };
889
+
890
+ const emptyState =
891
+ allItems.length === 0 && renderEmptyState ? (
892
+ <div role="row" aria-level={1}>
893
+ <div role="gridcell" aria-colindex={1}>
894
+ {renderEmptyState()}
895
+ </div>
896
+ </div>
897
+ ) : null;
898
+
899
+ return (
900
+ <Comp
901
+ {...otherProps}
902
+ as={innerAs}
903
+ ref={assignMultipleRefs(forwardedRef, rootRef)}
904
+ id={treeId}
905
+ role="treegrid"
906
+ aria-rowcount={renderedRowIndices ? visibleRows.length : undefined}
907
+ tabIndex={focusedKey == null ? tabIndex ?? 0 : tabIndex ?? -1}
908
+ aria-multiselectable={selectionMode === 'multiple' ? true : undefined}
909
+ data-tree=""
910
+ data-empty={allItems.length === 0 ? '' : undefined}
911
+ data-focused={isFocused ? '' : undefined}
912
+ data-focus-visible={isFocusVisible ? '' : undefined}
913
+ data-selection-mode={selectionMode}
914
+ onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
915
+ onFocus={wrapEvent(onFocus, handleFocus)}
916
+ onBlur={wrapEvent(onBlur, handleBlur)}
917
+ onMouseDown={wrapEvent(onMouseDown, handleMouseDown)}
918
+ >
919
+ {emptyState ??
920
+ (renderedRowIndices
921
+ ? renderFlatRows(visibleRows, renderedRowIndices, renderVisibleRow)
922
+ : parsedTree.map(renderNode))}
923
+ </Comp>
924
+ );
925
+ });
926
+
927
+ function renderFlatRows(
928
+ visibleRows: TreeVisibleRow[],
929
+ renderedRowIndices: Iterable<number>,
930
+ renderVisibleRow: (
931
+ row: TreeVisibleRow,
932
+ index: number
933
+ ) => ReactNode
934
+ ) {
935
+ return Array.from(renderedRowIndices, (index) => {
936
+ const row = visibleRows[index];
937
+ return row ? renderVisibleRow(row, index) : null;
938
+ });
939
+ }
940
+
941
+ function renderTreeItemContent(
942
+ item: ParsedTreeItem,
943
+ context: TreeItemRenderContextValue
944
+ ) {
945
+ if (item.props.title === undefined || !item.hasChildItems) {
946
+ return item.content;
947
+ }
948
+
949
+ return (
950
+ <>
951
+ <button
952
+ type="button"
953
+ slot="chevron"
954
+ aria-expanded={context.isExpanded}
955
+ aria-label={context.isExpanded ? 'Collapse' : 'Expand'}
956
+ aria-labelledby={context.rowId}
957
+ disabled={context.isDisabled}
958
+ tabIndex={-1}
959
+ onClick={context.toggleExpanded}
960
+ >
961
+ <span aria-hidden="true">&gt;</span>
962
+ </button>
963
+ {item.content}
964
+ </>
965
+ );
966
+ }
967
+
968
+ function makeKeySet(
969
+ keys: TreeSelection | TreeExpandedKeys | undefined,
970
+ allKeys: TreeKey[]
971
+ ) {
972
+ if (!keys) {
973
+ return new Set<TreeKey>();
974
+ }
975
+
976
+ if (keys === 'all') {
977
+ return new Set(allKeys);
978
+ }
979
+
980
+ return new Set(keys);
981
+ }
982
+
983
+ function addRangeToSelection(
984
+ visibleItems: ParsedTreeItem[],
985
+ anchorKey: TreeKey,
986
+ key: TreeKey,
987
+ selectedKeys: Set<TreeKey>,
988
+ canSelect: (item: ParsedTreeItem) => boolean
989
+ ) {
990
+ const anchorIndex = visibleItems.findIndex((item) => item.key === anchorKey);
991
+ const keyIndex = visibleItems.findIndex((item) => item.key === key);
992
+ if (anchorIndex === -1 || keyIndex === -1) {
993
+ return new Set(selectedKeys);
994
+ }
995
+
996
+ const [start, end] =
997
+ anchorIndex < keyIndex ? [anchorIndex, keyIndex] : [keyIndex, anchorIndex];
998
+ const next = new Set(selectedKeys);
999
+ for (let index = start; index <= end; index++) {
1000
+ const item = visibleItems[index];
1001
+ if (canSelect(item)) {
1002
+ next.add(item.key);
1003
+ }
1004
+ }
1005
+ return next;
1006
+ }
1007
+
1008
+ function firstVisibleSelectedKey(
1009
+ visibleItems: ParsedTreeItem[],
1010
+ selectedKeys: Set<TreeKey>
1011
+ ) {
1012
+ return visibleItems.find((item) => selectedKeys.has(item.key))?.key ?? null;
1013
+ }
1014
+
1015
+ function isItemDisabled(item: ParsedTreeItem, disabledKeys: Set<TreeKey>) {
1016
+ return item.disabled || disabledKeys.has(item.key);
1017
+ }
1018
+
1019
+ function isInteractionDisabled(
1020
+ item: ParsedTreeItem,
1021
+ disabledKeys: Set<TreeKey>,
1022
+ disabledBehavior: TreeDisabledBehavior
1023
+ ) {
1024
+ return disabledBehavior === 'all' && isItemDisabled(item, disabledKeys);
1025
+ }
1026
+
1027
+ function getItemDOMProps(props: TreeItemProps) {
1028
+ const {
1029
+ id,
1030
+ title,
1031
+ textValue,
1032
+ isDisabled,
1033
+ disabled,
1034
+ hasChildItems,
1035
+ isLoading,
1036
+ onAction,
1037
+ children,
1038
+ ...domProps
1039
+ } = props;
1040
+ return domProps;
1041
+ }
1042
+
1043
+ function getSectionDOMProps(props: TreeSectionProps) {
1044
+ const { as, innerAs, children, ...domProps } = props;
1045
+ return domProps;
1046
+ }
1047
+
1048
+ function getHeaderDOMProps(props: TreeHeaderProps) {
1049
+ const { as, innerAs, children, ...domProps } = props;
1050
+ return domProps;
1051
+ }
1052
+
1053
+ function sanitizeId(value: string) {
1054
+ return value.replace(/[^A-Za-z0-9_-]/g, '-');
1055
+ }
1056
+
1057
+ function getActiveElement(root: HTMLElement) {
1058
+ return root.ownerDocument.activeElement;
1059
+ }
1060
+
1061
+ function getFocusableElements(root: HTMLElement) {
1062
+ return Array.from(
1063
+ root.querySelectorAll<HTMLElement>(
1064
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
1065
+ )
1066
+ ).filter((element) => !element.hasAttribute('disabled'));
1067
+ }
1068
+
1069
+ function isInteractiveEventTarget(
1070
+ event: MouseEvent<HTMLElement>,
1071
+ row: HTMLElement | undefined
1072
+ ) {
1073
+ if (!row || !(event.target instanceof HTMLElement) || event.target === row) {
1074
+ return false;
1075
+ }
1076
+
1077
+ const interactiveElement = event.target.closest(
1078
+ 'a[href], button, input, select, textarea, [role="button"], [role="checkbox"], [tabindex]'
1079
+ );
1080
+ return Boolean(interactiveElement && interactiveElement !== row);
1081
+ }