@neo4j-ndl/react 4.21.2 → 4.22.1

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 (185) hide show
  1. package/lib/cjs/_common/form-control/FormFieldLabel.js +52 -0
  2. package/lib/cjs/_common/form-control/FormFieldLabel.js.map +1 -0
  3. package/lib/cjs/_common/form-control/FormFieldMessage.js +72 -0
  4. package/lib/cjs/_common/form-control/FormFieldMessage.js.map +1 -0
  5. package/lib/cjs/_common/form-control/form-control-types.js +23 -0
  6. package/lib/cjs/_common/form-control/form-control-types.js.map +1 -0
  7. package/lib/cjs/_common/form-control/index.js +39 -0
  8. package/lib/cjs/_common/form-control/index.js.map +1 -0
  9. package/lib/cjs/_common/form-control/use-form-field.js +76 -0
  10. package/lib/cjs/_common/form-control/use-form-field.js.map +1 -0
  11. package/lib/cjs/_common/form-control/use-inline-field-label.js +76 -0
  12. package/lib/cjs/_common/form-control/use-inline-field-label.js.map +1 -0
  13. package/lib/cjs/checkbox/Checkbox.js +9 -21
  14. package/lib/cjs/checkbox/Checkbox.js.map +1 -1
  15. package/lib/cjs/combobox/Combobox.js +61 -45
  16. package/lib/cjs/combobox/Combobox.js.map +1 -1
  17. package/lib/cjs/combobox/combobox-types.js.map +1 -1
  18. package/lib/cjs/date-picker/DatePicker.js.map +1 -1
  19. package/lib/cjs/dropzone/stories/dropzone-full.story.js +5 -5
  20. package/lib/cjs/dropzone/stories/dropzone-full.story.js.map +1 -1
  21. package/lib/cjs/helpers/index.js +1 -0
  22. package/lib/cjs/helpers/index.js.map +1 -1
  23. package/lib/cjs/helpers/platform.js +59 -0
  24. package/lib/cjs/helpers/platform.js.map +1 -0
  25. package/lib/cjs/kbd/kbd-utils.js +6 -22
  26. package/lib/cjs/kbd/kbd-utils.js.map +1 -1
  27. package/lib/cjs/next/tree-view/TreeView.js +384 -66
  28. package/lib/cjs/next/tree-view/TreeView.js.map +1 -1
  29. package/lib/cjs/next/tree-view/selection.js +181 -0
  30. package/lib/cjs/next/tree-view/selection.js.map +1 -0
  31. package/lib/cjs/next/tree-view/stories/index.js +13 -1
  32. package/lib/cjs/next/tree-view/stories/index.js.map +1 -1
  33. package/lib/cjs/next/tree-view/stories/tree-view-large.story.js +57 -0
  34. package/lib/cjs/next/tree-view/stories/tree-view-large.story.js.map +1 -0
  35. package/lib/cjs/next/tree-view/stories/tree-view-lazy.story.js +1 -0
  36. package/lib/cjs/next/tree-view/stories/tree-view-lazy.story.js.map +1 -1
  37. package/lib/cjs/next/tree-view/stories/tree-view-multi.story.js +16 -40
  38. package/lib/cjs/next/tree-view/stories/tree-view-multi.story.js.map +1 -1
  39. package/lib/cjs/next/tree-view/stories/tree-view-multiple-replace.story.js +38 -0
  40. package/lib/cjs/next/tree-view/stories/tree-view-multiple-replace.story.js.map +1 -0
  41. package/lib/cjs/next/tree-view/stories/tree-view-non-selectable-folders.story.js +7 -4
  42. package/lib/cjs/next/tree-view/stories/tree-view-non-selectable-folders.story.js.map +1 -1
  43. package/lib/cjs/next/tree-view/stories/tree-view-root-selection.story.js +125 -0
  44. package/lib/cjs/next/tree-view/stories/tree-view-root-selection.story.js.map +1 -0
  45. package/lib/cjs/radio/Radio.js +12 -22
  46. package/lib/cjs/radio/Radio.js.map +1 -1
  47. package/lib/cjs/select/Select.js +14 -5
  48. package/lib/cjs/select/Select.js.map +1 -1
  49. package/lib/cjs/select/types.js.map +1 -1
  50. package/lib/cjs/switch/Switch.js +9 -21
  51. package/lib/cjs/switch/Switch.js.map +1 -1
  52. package/lib/cjs/text-area/TextArea.js +8 -21
  53. package/lib/cjs/text-area/TextArea.js.map +1 -1
  54. package/lib/cjs/text-input/TextInput.js +15 -38
  55. package/lib/cjs/text-input/TextInput.js.map +1 -1
  56. package/lib/cjs/text-input/stories/text-input-sizes.story.js +1 -1
  57. package/lib/cjs/text-input/stories/text-input-sizes.story.js.map +1 -1
  58. package/lib/cjs/time-picker/TimePicker.js +13 -7
  59. package/lib/cjs/time-picker/TimePicker.js.map +1 -1
  60. package/lib/cjs/timezone-picker/TimeZonePicker.js +13 -7
  61. package/lib/cjs/timezone-picker/TimeZonePicker.js.map +1 -1
  62. package/lib/cjs/toast/ToastControlled.js +1 -1
  63. package/lib/cjs/toast/ToastControlled.js.map +1 -1
  64. package/lib/esm/_common/form-control/FormFieldLabel.js +45 -0
  65. package/lib/esm/_common/form-control/FormFieldLabel.js.map +1 -0
  66. package/lib/esm/_common/form-control/FormFieldMessage.js +65 -0
  67. package/lib/esm/_common/form-control/FormFieldMessage.js.map +1 -0
  68. package/lib/esm/_common/form-control/form-control-types.js +22 -0
  69. package/lib/esm/_common/form-control/form-control-types.js.map +1 -0
  70. package/lib/esm/_common/form-control/index.js +32 -0
  71. package/lib/esm/_common/form-control/index.js.map +1 -0
  72. package/lib/esm/_common/form-control/use-form-field.js +72 -0
  73. package/lib/esm/_common/form-control/use-form-field.js.map +1 -0
  74. package/lib/esm/_common/form-control/use-inline-field-label.js +69 -0
  75. package/lib/esm/_common/form-control/use-inline-field-label.js.map +1 -0
  76. package/lib/esm/checkbox/Checkbox.js +10 -22
  77. package/lib/esm/checkbox/Checkbox.js.map +1 -1
  78. package/lib/esm/combobox/Combobox.js +62 -46
  79. package/lib/esm/combobox/Combobox.js.map +1 -1
  80. package/lib/esm/combobox/combobox-types.js.map +1 -1
  81. package/lib/esm/date-picker/DatePicker.js.map +1 -1
  82. package/lib/esm/dropzone/stories/dropzone-full.story.js +5 -5
  83. package/lib/esm/dropzone/stories/dropzone-full.story.js.map +1 -1
  84. package/lib/esm/helpers/index.js +1 -0
  85. package/lib/esm/helpers/index.js.map +1 -1
  86. package/lib/esm/helpers/platform.js +55 -0
  87. package/lib/esm/helpers/platform.js.map +1 -0
  88. package/lib/esm/kbd/kbd-utils.js +2 -18
  89. package/lib/esm/kbd/kbd-utils.js.map +1 -1
  90. package/lib/esm/next/tree-view/TreeView.js +385 -67
  91. package/lib/esm/next/tree-view/TreeView.js.map +1 -1
  92. package/lib/esm/next/tree-view/selection.js +175 -0
  93. package/lib/esm/next/tree-view/selection.js.map +1 -0
  94. package/lib/esm/next/tree-view/stories/index.js +9 -0
  95. package/lib/esm/next/tree-view/stories/index.js.map +1 -1
  96. package/lib/esm/next/tree-view/stories/tree-view-large.story.js +55 -0
  97. package/lib/esm/next/tree-view/stories/tree-view-large.story.js.map +1 -0
  98. package/lib/esm/next/tree-view/stories/tree-view-lazy.story.js +1 -0
  99. package/lib/esm/next/tree-view/stories/tree-view-lazy.story.js.map +1 -1
  100. package/lib/esm/next/tree-view/stories/tree-view-multi.story.js +16 -40
  101. package/lib/esm/next/tree-view/stories/tree-view-multi.story.js.map +1 -1
  102. package/lib/esm/next/tree-view/stories/tree-view-multiple-replace.story.js +36 -0
  103. package/lib/esm/next/tree-view/stories/tree-view-multiple-replace.story.js.map +1 -0
  104. package/lib/esm/next/tree-view/stories/tree-view-non-selectable-folders.story.js +7 -4
  105. package/lib/esm/next/tree-view/stories/tree-view-non-selectable-folders.story.js.map +1 -1
  106. package/lib/esm/next/tree-view/stories/tree-view-root-selection.story.js +123 -0
  107. package/lib/esm/next/tree-view/stories/tree-view-root-selection.story.js.map +1 -0
  108. package/lib/esm/radio/Radio.js +12 -22
  109. package/lib/esm/radio/Radio.js.map +1 -1
  110. package/lib/esm/select/Select.js +14 -5
  111. package/lib/esm/select/Select.js.map +1 -1
  112. package/lib/esm/select/types.js.map +1 -1
  113. package/lib/esm/switch/Switch.js +10 -22
  114. package/lib/esm/switch/Switch.js.map +1 -1
  115. package/lib/esm/text-area/TextArea.js +9 -22
  116. package/lib/esm/text-area/TextArea.js.map +1 -1
  117. package/lib/esm/text-input/TextInput.js +15 -38
  118. package/lib/esm/text-input/TextInput.js.map +1 -1
  119. package/lib/esm/text-input/stories/text-input-sizes.story.js +1 -1
  120. package/lib/esm/text-input/stories/text-input-sizes.story.js.map +1 -1
  121. package/lib/esm/time-picker/TimePicker.js +13 -7
  122. package/lib/esm/time-picker/TimePicker.js.map +1 -1
  123. package/lib/esm/timezone-picker/TimeZonePicker.js +13 -7
  124. package/lib/esm/timezone-picker/TimeZonePicker.js.map +1 -1
  125. package/lib/esm/toast/ToastControlled.js +1 -1
  126. package/lib/esm/toast/ToastControlled.js.map +1 -1
  127. package/lib/types/_common/form-control/FormFieldLabel.d.ts +58 -0
  128. package/lib/types/_common/form-control/FormFieldLabel.d.ts.map +1 -0
  129. package/lib/types/_common/form-control/FormFieldMessage.d.ts +46 -0
  130. package/lib/types/_common/form-control/FormFieldMessage.d.ts.map +1 -0
  131. package/lib/types/_common/form-control/form-control-types.d.ts +92 -0
  132. package/lib/types/_common/form-control/form-control-types.d.ts.map +1 -0
  133. package/lib/types/_common/form-control/index.d.ts +33 -0
  134. package/lib/types/_common/form-control/index.d.ts.map +1 -0
  135. package/lib/types/_common/form-control/use-form-field.d.ts +53 -0
  136. package/lib/types/_common/form-control/use-form-field.d.ts.map +1 -0
  137. package/lib/types/_common/form-control/use-inline-field-label.d.ts +69 -0
  138. package/lib/types/_common/form-control/use-inline-field-label.d.ts.map +1 -0
  139. package/lib/types/checkbox/Checkbox.d.ts.map +1 -1
  140. package/lib/types/combobox/Combobox.d.ts.map +1 -1
  141. package/lib/types/combobox/combobox-types.d.ts +1 -1
  142. package/lib/types/combobox/combobox-types.d.ts.map +1 -1
  143. package/lib/types/helpers/index.d.ts +1 -0
  144. package/lib/types/helpers/index.d.ts.map +1 -1
  145. package/lib/types/helpers/platform.d.ts +29 -0
  146. package/lib/types/helpers/platform.d.ts.map +1 -0
  147. package/lib/types/kbd/kbd-utils.d.ts +2 -5
  148. package/lib/types/kbd/kbd-utils.d.ts.map +1 -1
  149. package/lib/types/next/tree-view/TreeView.d.ts +29 -5
  150. package/lib/types/next/tree-view/TreeView.d.ts.map +1 -1
  151. package/lib/types/next/tree-view/selection.d.ts +78 -0
  152. package/lib/types/next/tree-view/selection.d.ts.map +1 -0
  153. package/lib/types/next/tree-view/stories/index.d.ts +6 -0
  154. package/lib/types/next/tree-view/stories/index.d.ts.map +1 -1
  155. package/lib/types/next/tree-view/stories/tree-view-large.story.d.ts +24 -0
  156. package/lib/types/next/tree-view/stories/tree-view-large.story.d.ts.map +1 -0
  157. package/lib/types/next/tree-view/stories/tree-view-lazy.story.d.ts.map +1 -1
  158. package/lib/types/next/tree-view/stories/tree-view-multi.story.d.ts +10 -0
  159. package/lib/types/next/tree-view/stories/tree-view-multi.story.d.ts.map +1 -1
  160. package/lib/types/next/tree-view/stories/tree-view-multiple-replace.story.d.ts +29 -0
  161. package/lib/types/next/tree-view/stories/tree-view-multiple-replace.story.d.ts.map +1 -0
  162. package/lib/types/next/tree-view/stories/tree-view-non-selectable-folders.story.d.ts +5 -0
  163. package/lib/types/next/tree-view/stories/tree-view-non-selectable-folders.story.d.ts.map +1 -1
  164. package/lib/types/next/tree-view/stories/tree-view-root-selection.story.d.ts +24 -0
  165. package/lib/types/next/tree-view/stories/tree-view-root-selection.story.d.ts.map +1 -0
  166. package/lib/types/radio/Radio.d.ts.map +1 -1
  167. package/lib/types/select/Select.d.ts.map +1 -1
  168. package/lib/types/select/types.d.ts.map +1 -1
  169. package/lib/types/switch/Switch.d.ts.map +1 -1
  170. package/lib/types/text-area/TextArea.d.ts.map +1 -1
  171. package/lib/types/text-input/TextInput.d.ts.map +1 -1
  172. package/lib/types/text-input/stories/text-input-sizes.story.d.ts.map +1 -1
  173. package/lib/types/time-picker/TimePicker.d.ts +3 -1
  174. package/lib/types/time-picker/TimePicker.d.ts.map +1 -1
  175. package/lib/types/timezone-picker/TimeZonePicker.d.ts +4 -1
  176. package/lib/types/timezone-picker/TimeZonePicker.d.ts.map +1 -1
  177. package/package.json +2 -2
  178. package/skills/ndl-react/SKILL.md +1 -1
  179. package/skills/ndl-react/components/color-picker.md +32 -32
  180. package/skills/ndl-react/components/combobox.md +2 -2
  181. package/skills/ndl-react/components/dropzone.md +5 -5
  182. package/skills/ndl-react/components/next/tree-view.md +444 -104
  183. package/skills/ndl-react/components/text-input.md +6 -0
  184. package/skills/ndl-react/components/time-picker.md +1 -0
  185. package/skills/ndl-react/components/timezone-picker.md +1 -0
@@ -32,31 +32,73 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
32
32
  */
33
33
  import { useMergeRefs } from '@floating-ui/react';
34
34
  import classNames from 'classnames';
35
- import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useId, useRef, useState, } from 'react';
35
+ import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
36
36
  import { useSemicontrolledState } from '../../_common/use-semi-controlled-state';
37
+ import { needleWarningMessage } from '../../_common/utils';
37
38
  import { Checkbox } from '../../checkbox';
38
39
  import { CleanIconButton } from '../../clean-icon-button';
39
40
  import { ConditionalWrap } from '../../conditional-wrap';
41
+ import { isApplePlatform } from '../../helpers/platform';
40
42
  import { ChevronRightIconOutline } from '../../icons/generated/heroIcons/ChevronRightIconOutline';
41
43
  import { EllipsisHorizontalIconOutline } from '../../icons/generated/heroIcons/EllipsisHorizontalIconOutline';
42
44
  import { Menu } from '../../menu';
43
45
  import { Skeleton } from '../../skeleton';
44
46
  import { Tooltip } from '../../tooltip';
47
+ import { computeNextSelection, computeSelectionEdges, countChangedIds, formatSelectionAnnouncement, } from './selection';
45
48
  const noop = () => undefined;
49
+ const EMPTY_EDGES = new Map();
50
+ const EMPTY_IDS = new Set();
51
+ const EMPTY_ID_LIST = [];
52
+ /**
53
+ * Volatile per-interaction state (focus, root-managed selection, edge
54
+ * styling) lives in this store instead of React context, so each item can
55
+ * subscribe to its own primitive slice via `useSyncExternalStore` and an
56
+ * interaction only re-renders the rows whose slice actually changed.
57
+ */
58
+ function createTreeViewStore(initialState) {
59
+ let state = Object.assign({ edgeById: EMPTY_EDGES, focusedId: null, selectedIds: EMPTY_IDS }, initialState);
60
+ const listeners = new Set();
61
+ return {
62
+ getState: () => state,
63
+ setState: (partial) => {
64
+ state = Object.assign(Object.assign({}, state), partial);
65
+ listeners.forEach((listener) => listener());
66
+ },
67
+ subscribe: (listener) => {
68
+ listeners.add(listener);
69
+ return () => {
70
+ listeners.delete(listener);
71
+ };
72
+ },
73
+ };
74
+ }
46
75
  const TREE_EVENTS = {
47
76
  action: 'treeitem-action',
48
77
  collapse: 'treeitem-collapse',
49
78
  expand: 'treeitem-expand',
50
- select: 'treeitem-select',
51
79
  };
52
80
  const TreeViewContext = createContext({
53
- focusedId: null,
81
+ isRootManaged: false,
54
82
  level: 1,
83
+ onSelectionIntent: noop,
84
+ parentId: null,
85
+ registerItem: noop,
55
86
  selectionMode: 'single',
56
87
  setFocusedId: noop,
88
+ store: createTreeViewStore(),
57
89
  });
90
+ /**
91
+ * On Apple platforms Ctrl+click is the context menu gesture, so only Cmd may
92
+ * stand for an additive selection there.
93
+ */
94
+ function getSelectionIntent(event) {
95
+ return {
96
+ isRange: event.shiftKey,
97
+ isToggle: isApplePlatform() ? event.metaKey : event.ctrlKey,
98
+ };
99
+ }
58
100
  function getVisibleTreeItems(root) {
59
- return Array.from(root.querySelectorAll('[role="treeitem"]:not([role="group"][hidden] [role="treeitem"])')).filter((el) => {
101
+ return Array.from(root.querySelectorAll('[role="treeitem"]')).filter((el) => {
60
102
  let parent = el.parentElement;
61
103
  while (parent && parent !== root) {
62
104
  if (parent.getAttribute('role') === 'group' && parent.hidden) {
@@ -67,6 +109,31 @@ function getVisibleTreeItems(root) {
67
109
  return true;
68
110
  });
69
111
  }
112
+ function getVisibleItemIds(root) {
113
+ return getVisibleTreeItems(root).map((el) => el.id);
114
+ }
115
+ function areIdSetsEqual(first, second) {
116
+ if (first.size !== second.size) {
117
+ return false;
118
+ }
119
+ for (const id of first) {
120
+ if (!second.has(id)) {
121
+ return false;
122
+ }
123
+ }
124
+ return true;
125
+ }
126
+ function areEdgeMapsEqual(first, second) {
127
+ if (first.size !== second.size) {
128
+ return false;
129
+ }
130
+ for (const [id, edge] of first) {
131
+ if (second.get(id) !== edge) {
132
+ return false;
133
+ }
134
+ }
135
+ return true;
136
+ }
70
137
  function annotateTreeItems(children) {
71
138
  const items = Children.toArray(children);
72
139
  const treeItemCount = items.filter((child) => isValidElement(child) && child.type === Item).length;
@@ -99,10 +166,186 @@ function annotateTreeItems(children) {
99
166
  * @alpha - this component is still in development and may change in the future. It might not have feature parity with the old tree view yet.
100
167
  */
101
168
  const Root = (_a) => {
102
- var { children, selectionMode = 'single', ariaLabel, ariaLabelledby, className, style, htmlAttributes, ref } = _a, restProps = __rest(_a, ["children", "selectionMode", "ariaLabel", "ariaLabelledby", "className", "style", "htmlAttributes", "ref"]);
169
+ var _b, _c;
170
+ var { children, defaultSelectedIds, selectedIds: selectedIdsProp, selectionMode = 'single', ariaLabel, ariaLabelledby, className, style, htmlAttributes, onSelectedIdsChange, ref } = _a, restProps = __rest(_a, ["children", "defaultSelectedIds", "selectedIds", "selectionMode", "ariaLabel", "ariaLabelledby", "className", "style", "htmlAttributes", "onSelectedIdsChange", "ref"]);
103
171
  const treeRef = useRef(null);
104
172
  const mergedRef = useMergeRefs([treeRef, ref]);
105
- const [focusedId, setFocusedId] = useState(null);
173
+ const [announcement, setAnnouncement] = useState('');
174
+ const anchorRef = useRef(null);
175
+ const entriesRef = useRef(new Map());
176
+ const isRootManaged = selectedIdsProp !== undefined || defaultSelectedIds !== undefined;
177
+ const storeRef = useRef(null);
178
+ if (storeRef.current === null) {
179
+ storeRef.current = createTreeViewStore({
180
+ selectedIds: new Set((_b = selectedIdsProp !== null && selectedIdsProp !== void 0 ? selectedIdsProp : defaultSelectedIds) !== null && _b !== void 0 ? _b : EMPTY_ID_LIST),
181
+ });
182
+ }
183
+ const store = storeRef.current;
184
+ const setFocusedId = useCallback((id) => {
185
+ if (store.getState().focusedId !== id) {
186
+ store.setState({ focusedId: id });
187
+ }
188
+ }, [store]);
189
+ const [selectedIdList, setSelectedIdList] = useSemicontrolledState({
190
+ isControlled: selectedIdsProp !== undefined,
191
+ onChange: onSelectedIdsChange,
192
+ state: (_c = selectedIdsProp !== null && selectedIdsProp !== void 0 ? selectedIdsProp : defaultSelectedIds) !== null && _c !== void 0 ? _c : EMPTY_ID_LIST,
193
+ });
194
+ // useSemicontrolledState recreates its setter whenever the value changes;
195
+ // reading it through a ref keeps onSelectionIntent (and with it the
196
+ // memoized context value) stable across selection changes.
197
+ const setSelectedIdListRef = useRef(setSelectedIdList);
198
+ useLayoutEffect(() => {
199
+ setSelectedIdListRef.current = setSelectedIdList;
200
+ });
201
+ const readSelectedIds = useCallback(() => {
202
+ if (isRootManaged) {
203
+ return new Set(store.getState().selectedIds);
204
+ }
205
+ const selected = new Set();
206
+ entriesRef.current.forEach((entry, itemId) => {
207
+ if (entry.isSelected) {
208
+ selected.add(itemId);
209
+ }
210
+ });
211
+ return selected;
212
+ }, [isRootManaged, store]);
213
+ const readItemMaps = useCallback(() => {
214
+ const parentIdById = new Map();
215
+ const selectableIds = new Set();
216
+ entriesRef.current.forEach((entry, itemId) => {
217
+ if (entry.parentId !== null) {
218
+ parentIdById.set(itemId, entry.parentId);
219
+ }
220
+ if (entry.isSelectable && !entry.isDisabled) {
221
+ selectableIds.add(itemId);
222
+ }
223
+ });
224
+ return { parentIdById, selectableIds };
225
+ }, []);
226
+ const recomputeEdges = useCallback(() => {
227
+ const root = treeRef.current;
228
+ const previousEdges = store.getState().edgeById;
229
+ if (!root || selectionMode !== 'multiple-replace') {
230
+ if (previousEdges.size > 0) {
231
+ store.setState({ edgeById: EMPTY_EDGES });
232
+ }
233
+ return;
234
+ }
235
+ const selectedIds = readSelectedIds();
236
+ if (selectedIds.size === 0 && previousEdges.size === 0) {
237
+ return;
238
+ }
239
+ const nextEdges = computeSelectionEdges(getVisibleItemIds(root), selectedIds);
240
+ if (!areEdgeMapsEqual(previousEdges, nextEdges)) {
241
+ store.setState({ edgeById: nextEdges });
242
+ }
243
+ }, [readSelectedIds, selectionMode, store]);
244
+ const recomputeEdgesRef = useRef(recomputeEdges);
245
+ useLayoutEffect(() => {
246
+ recomputeEdgesRef.current = recomputeEdges;
247
+ });
248
+ // Debounced through a microtask so a commit that touches many items (mount,
249
+ // expansion, range selection) triggers a single recompute, after all item
250
+ // layout effects have registered and before the browser paints.
251
+ const isEdgeRecomputeScheduledRef = useRef(false);
252
+ const scheduleEdgeRecompute = useCallback(() => {
253
+ if (isEdgeRecomputeScheduledRef.current) {
254
+ return;
255
+ }
256
+ isEdgeRecomputeScheduledRef.current = true;
257
+ queueMicrotask(() => {
258
+ isEdgeRecomputeScheduledRef.current = false;
259
+ recomputeEdgesRef.current();
260
+ });
261
+ }, []);
262
+ const registerItem = useCallback((itemId, entry) => {
263
+ if (entry === null) {
264
+ entriesRef.current.delete(itemId);
265
+ }
266
+ else {
267
+ entriesRef.current.set(itemId, entry);
268
+ }
269
+ // Covers item mount/unmount and expansion toggles (a collapsing parent
270
+ // re-registers because its toggleExpanded callback is recreated).
271
+ scheduleEdgeRecompute();
272
+ }, [scheduleEdgeRecompute]);
273
+ const onSelectionIntent = useCallback((itemId, intent) => {
274
+ var _a;
275
+ const root = treeRef.current;
276
+ const entry = entriesRef.current.get(itemId);
277
+ if (!root || !entry || entry.isDisabled) {
278
+ return;
279
+ }
280
+ if (!entry.isSelectable) {
281
+ entry.toggleExpanded();
282
+ return;
283
+ }
284
+ const { parentIdById, selectableIds } = readItemMaps();
285
+ const selectedIds = readSelectedIds();
286
+ const result = computeNextSelection({
287
+ anchor: anchorRef.current,
288
+ intent,
289
+ parentIdById,
290
+ selectableIds,
291
+ selectedIds,
292
+ selectionMode,
293
+ targetId: itemId,
294
+ visibleIds: getVisibleItemIds(root),
295
+ });
296
+ anchorRef.current = result.anchor;
297
+ const nextSelectedIds = result.selectedIds;
298
+ const changedCount = countChangedIds(selectedIds, nextSelectedIds);
299
+ if (isRootManaged) {
300
+ if (changedCount > 0) {
301
+ setSelectedIdListRef.current([...nextSelectedIds]);
302
+ }
303
+ }
304
+ else if (selectionMode === 'single') {
305
+ // Single mode leaves clearing the previously selected item to the
306
+ // consumer, so only the clicked item is told about the change.
307
+ if (!selectedIds.has(itemId)) {
308
+ (_a = entry.onSelectedChange) === null || _a === void 0 ? void 0 : _a.call(entry, true);
309
+ }
310
+ }
311
+ else {
312
+ entriesRef.current.forEach((itemEntry, id) => {
313
+ var _a;
314
+ const isSelected = nextSelectedIds.has(id);
315
+ if (itemEntry.isSelected !== isSelected) {
316
+ (_a = itemEntry.onSelectedChange) === null || _a === void 0 ? void 0 : _a.call(itemEntry, isSelected);
317
+ }
318
+ });
319
+ }
320
+ scheduleEdgeRecompute();
321
+ if (selectionMode !== 'single') {
322
+ setAnnouncement(intent.isRange
323
+ ? formatSelectionAnnouncement(nextSelectedIds.size)
324
+ : '');
325
+ }
326
+ }, [
327
+ isRootManaged,
328
+ readItemMaps,
329
+ readSelectedIds,
330
+ scheduleEdgeRecompute,
331
+ selectionMode,
332
+ ]);
333
+ // Mirrors the root-managed selection into the store, so items subscribe to
334
+ // their own slice and event handlers read it without a state dependency.
335
+ useLayoutEffect(() => {
336
+ const next = isRootManaged ? new Set(selectedIdList) : EMPTY_IDS;
337
+ if (areIdSetsEqual(store.getState().selectedIds, next)) {
338
+ return;
339
+ }
340
+ store.setState({ selectedIds: next });
341
+ scheduleEdgeRecompute();
342
+ }, [isRootManaged, scheduleEdgeRecompute, selectedIdList, store]);
343
+ // Computes edges on mount and whenever the edge inputs change identity
344
+ // (selection mode, root-managed flag). Interaction-driven recomputes go
345
+ // through scheduleEdgeRecompute instead.
346
+ useLayoutEffect(() => {
347
+ recomputeEdges();
348
+ }, [recomputeEdges]);
106
349
  const handleKeyDown = useCallback((event) => {
107
350
  const root = treeRef.current;
108
351
  if (!root) {
@@ -112,13 +355,35 @@ const Root = (_a) => {
112
355
  if (items.length === 0) {
113
356
  return;
114
357
  }
115
- const currentIndex = items.findIndex((el) => el.id === focusedId);
358
+ const currentIndex = items.findIndex((el) => el.id === store.getState().focusedId);
116
359
  let isHandled = true;
360
+ const extendRangeTo = (item) => {
361
+ if (selectionMode === 'single' || currentIndex < 0) {
362
+ return;
363
+ }
364
+ const entry = entriesRef.current.get(item.id);
365
+ if (!(entry === null || entry === void 0 ? void 0 : entry.isSelectable) || entry.isDisabled) {
366
+ return;
367
+ }
368
+ if (anchorRef.current === null) {
369
+ // A keyboard range has no preceding click to repeat, so it grows the
370
+ // selection outwards from wherever focus already is.
371
+ anchorRef.current = {
372
+ baseSelectedIds: readSelectedIds(),
373
+ id: items[currentIndex].id,
374
+ isSelected: true,
375
+ };
376
+ }
377
+ onSelectionIntent(item.id, { isRange: true, isToggle: false });
378
+ };
117
379
  switch (event.key) {
118
380
  case 'ArrowDown': {
119
381
  const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : currentIndex;
120
382
  const nextItem = items[nextIndex];
121
383
  if (nextItem) {
384
+ if (event.shiftKey) {
385
+ extendRangeTo(nextItem);
386
+ }
122
387
  setFocusedId(nextItem.id);
123
388
  nextItem.focus();
124
389
  }
@@ -128,6 +393,9 @@ const Root = (_a) => {
128
393
  const prevIndex = currentIndex > 0 ? currentIndex - 1 : 0;
129
394
  const prevItem = items[prevIndex];
130
395
  if (prevItem) {
396
+ if (event.shiftKey) {
397
+ extendRangeTo(prevItem);
398
+ }
131
399
  setFocusedId(prevItem.id);
132
400
  prevItem.focus();
133
401
  }
@@ -193,7 +461,7 @@ const Root = (_a) => {
193
461
  case ' ': {
194
462
  const current = currentIndex >= 0 ? items[currentIndex] : null;
195
463
  if (current) {
196
- current.dispatchEvent(new CustomEvent(TREE_EVENTS.select));
464
+ onSelectionIntent(current.id, getSelectionIntent(event));
197
465
  }
198
466
  break;
199
467
  }
@@ -216,7 +484,7 @@ const Root = (_a) => {
216
484
  event.preventDefault();
217
485
  event.stopPropagation();
218
486
  }
219
- }, [focusedId]);
487
+ }, [onSelectionIntent, readSelectedIds, selectionMode, setFocusedId, store]);
220
488
  const handleFocus = useCallback((event) => {
221
489
  const root = treeRef.current;
222
490
  if (!root) {
@@ -224,6 +492,7 @@ const Root = (_a) => {
224
492
  }
225
493
  if (event.target === root) {
226
494
  const items = getVisibleTreeItems(root);
495
+ const { focusedId } = store.getState();
227
496
  const alreadyFocused = focusedId
228
497
  ? items.find((el) => el.id === focusedId)
229
498
  : null;
@@ -233,9 +502,36 @@ const Root = (_a) => {
233
502
  target.focus();
234
503
  }
235
504
  }
236
- }, [focusedId]);
237
- const classes = classNames(className, 'ndl-tree-view-new');
238
- return (_jsx(TreeViewContext.Provider, { value: { focusedId, level: 1, selectionMode, setFocusedId }, children: _jsx("div", Object.assign({ ref: mergedRef, role: "tree", "aria-multiselectable": selectionMode === 'multiple' || undefined, tabIndex: 0, className: classes, onKeyDown: handleKeyDown, onFocus: handleFocus, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : undefined, "aria-labelledby": ariaLabelledby, style: style }, htmlAttributes, restProps, { children: annotateTreeItems(children) })) }));
505
+ }, [setFocusedId, store]);
506
+ const classes = classNames(className, 'ndl-tree-view-new', {
507
+ 'ndl-tree-view-new--checkbox': selectionMode === 'multiple',
508
+ });
509
+ // Cached on children identity so Root-internal state commits (announcement,
510
+ // uncontrolled selection) reuse the same annotated elements and React bails
511
+ // out of the whole item subtree instead of re-rendering every row.
512
+ const annotatedChildren = useMemo(() => annotateTreeItems(children), [children]);
513
+ // Everything in here is stable or primitive, so items only re-render via
514
+ // context when the tree's configuration changes, not per interaction.
515
+ const contextValue = useMemo(() => ({
516
+ isRootManaged,
517
+ level: 1,
518
+ onSelectionIntent,
519
+ parentId: null,
520
+ registerItem,
521
+ selectionMode,
522
+ setFocusedId,
523
+ store,
524
+ }), [
525
+ isRootManaged,
526
+ onSelectionIntent,
527
+ registerItem,
528
+ selectionMode,
529
+ setFocusedId,
530
+ store,
531
+ ]);
532
+ return (_jsxs(TreeViewContext.Provider, { value: contextValue, children: [_jsx("div", Object.assign({ ref: mergedRef, role: "tree", "aria-multiselectable": selectionMode === 'multiple' || selectionMode === 'multiple-replace'
533
+ ? true
534
+ : undefined, tabIndex: 0, className: classes, onKeyDown: handleKeyDown, onFocus: handleFocus, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : undefined, "aria-labelledby": ariaLabelledby, style: style }, htmlAttributes, restProps, { children: annotatedChildren })), _jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "ndl-tree-view-new-live-region", children: announcement })] }));
239
535
  };
240
536
  /**
241
537
  * A single item within a `TreeView`. Renders a `role="treeitem"` element that
@@ -260,16 +556,23 @@ const Root = (_a) => {
260
556
  * ```
261
557
  */
262
558
  const Item = (_a) => {
263
- var { actionMenuItems, children, defaultExpanded = false, hasChildren: hasChildrenProp, isDisabled, isExpanded: isExpandedProp, isIndeterminate, isLoading, isSelected, leadingVisual, title, tooltipContent, tooltipProps, trailingContent, onExpandedChange, onSelectedChange, actionMenuProps, className, style, htmlAttributes, ref } = _a, restProps = __rest(_a, ["actionMenuItems", "children", "defaultExpanded", "hasChildren", "isDisabled", "isExpanded", "isIndeterminate", "isLoading", "isSelected", "leadingVisual", "title", "tooltipContent", "tooltipProps", "trailingContent", "onExpandedChange", "onSelectedChange", "actionMenuProps", "className", "style", "htmlAttributes", "ref"]);
559
+ var _b;
560
+ var { actionMenuItems, children, defaultExpanded = false, hasChildren: hasChildrenProp, id: idProp, isDisabled, isExpanded: isExpandedProp, isIndeterminate, isLoading, isSelectable: isSelectableProp, isSelected: isSelectedProp, leadingVisual, title, tooltipContent, tooltipProps, trailingContent, onExpandedChange, onSelectedChange, actionMenuProps, className, style, htmlAttributes, ref } = _a, restProps = __rest(_a, ["actionMenuItems", "children", "defaultExpanded", "hasChildren", "id", "isDisabled", "isExpanded", "isIndeterminate", "isLoading", "isSelectable", "isSelected", "leadingVisual", "title", "tooltipContent", "tooltipProps", "trailingContent", "onExpandedChange", "onSelectedChange", "actionMenuProps", "className", "style", "htmlAttributes", "ref"]);
264
561
  const generatedId = useId();
265
- const itemId = `ndl-treeitem-${generatedId}`;
562
+ const itemId = (_b = idProp !== null && idProp !== void 0 ? idProp : htmlAttributes === null || htmlAttributes === void 0 ? void 0 : htmlAttributes.id) !== null && _b !== void 0 ? _b : `ndl-treeitem-${generatedId}`;
266
563
  const titleId = `ndl-treeitem-title-${generatedId}`;
267
564
  const groupId = `ndl-treegroup-${generatedId}`;
268
565
  const itemRef = useRef(null);
269
566
  const actionButtonRef = useRef(null);
270
567
  const didOpenViaKeyboard = useRef(false);
271
568
  const [isMenuOpen, setIsMenuOpen] = useState(false);
272
- const { focusedId, level, selectionMode, setFocusedId } = useContext(TreeViewContext);
569
+ const treeViewContext = useContext(TreeViewContext);
570
+ const { isRootManaged, level, onSelectionIntent, parentId, registerItem, selectionMode, setFocusedId, store, } = treeViewContext;
571
+ // Primitive slices of the store, so Object.is bails out of re-renders for
572
+ // rows whose focus/selection/edge state did not change.
573
+ const isFocused = useSyncExternalStore(store.subscribe, () => store.getState().focusedId === itemId, () => store.getState().focusedId === itemId);
574
+ const isRootSelected = useSyncExternalStore(store.subscribe, () => store.getState().selectedIds.has(itemId), () => store.getState().selectedIds.has(itemId));
575
+ const edge = useSyncExternalStore(store.subscribe, () => store.getState().edgeById.get(itemId), () => store.getState().edgeById.get(itemId));
273
576
  const isControlledExpansion = isExpandedProp !== undefined;
274
577
  const [isExpanded, setIsExpanded] = useSemicontrolledState({
275
578
  isControlled: isControlledExpansion,
@@ -277,7 +580,10 @@ const Item = (_a) => {
277
580
  state: isExpandedProp !== null && isExpandedProp !== void 0 ? isExpandedProp : defaultExpanded,
278
581
  });
279
582
  const hasChildren = hasChildrenProp !== null && hasChildrenProp !== void 0 ? hasChildrenProp : Children.count(children) > 0;
280
- const isSelectable = onSelectedChange !== undefined;
583
+ const isSelectable = isRootManaged
584
+ ? (isSelectableProp !== null && isSelectableProp !== void 0 ? isSelectableProp : true)
585
+ : (isSelectableProp !== null && isSelectableProp !== void 0 ? isSelectableProp : onSelectedChange !== undefined);
586
+ const isSelected = isRootManaged ? isRootSelected : (isSelectedProp !== null && isSelectedProp !== void 0 ? isSelectedProp : false);
281
587
  const isMultiple = selectionMode === 'multiple';
282
588
  const toggleExpanded = useCallback(() => {
283
589
  if (isDisabled) {
@@ -287,31 +593,46 @@ const Item = (_a) => {
287
593
  setIsExpanded(!isExpanded);
288
594
  }
289
595
  }, [isDisabled, hasChildren, setIsExpanded, isExpanded]);
290
- const handleSelect = useCallback(() => {
291
- if (isDisabled) {
596
+ useLayoutEffect(() => {
597
+ registerItem(itemId, {
598
+ isDisabled: isDisabled !== null && isDisabled !== void 0 ? isDisabled : false,
599
+ isSelectable,
600
+ isSelected,
601
+ onSelectedChange,
602
+ parentId,
603
+ toggleExpanded,
604
+ });
605
+ }, [
606
+ isDisabled,
607
+ isSelectable,
608
+ isSelected,
609
+ itemId,
610
+ onSelectedChange,
611
+ parentId,
612
+ registerItem,
613
+ toggleExpanded,
614
+ ]);
615
+ useLayoutEffect(() => {
616
+ return () => registerItem(itemId, null);
617
+ }, [itemId, registerItem]);
618
+ useEffect(() => {
619
+ if (!isRootManaged) {
292
620
  return;
293
621
  }
294
- onSelectedChange === null || onSelectedChange === void 0 ? void 0 : onSelectedChange(!isSelected);
295
- }, [isDisabled, isSelected, onSelectedChange]);
296
- const handleClick = useCallback(() => {
622
+ if (idProp === undefined) {
623
+ needleWarningMessage('TreeView.Item needs an `id` when the tree manages selection with `selectedIds`.');
624
+ }
625
+ if (isSelectedProp !== undefined || onSelectedChange !== undefined) {
626
+ needleWarningMessage('TreeView.Item `isSelected` and `onSelectedChange` are ignored when the tree manages selection with `selectedIds`.');
627
+ }
628
+ }, [idProp, isRootManaged, isSelectedProp, onSelectedChange]);
629
+ const handleClick = useCallback((event) => {
297
630
  if (isDisabled) {
298
631
  return;
299
632
  }
300
633
  setFocusedId(itemId);
301
- if (isSelectable) {
302
- handleSelect();
303
- }
304
- else {
305
- toggleExpanded();
306
- }
307
- }, [
308
- isDisabled,
309
- setFocusedId,
310
- itemId,
311
- isSelectable,
312
- handleSelect,
313
- toggleExpanded,
314
- ]);
634
+ onSelectionIntent(itemId, getSelectionIntent(event));
635
+ }, [isDisabled, setFocusedId, itemId, onSelectionIntent]);
315
636
  const handleChevronClick = useCallback((event) => {
316
637
  event.stopPropagation();
317
638
  if (isDisabled) {
@@ -341,15 +662,6 @@ const Item = (_a) => {
341
662
  setIsExpanded(false);
342
663
  }
343
664
  }
344
- else if (event.type === TREE_EVENTS.select) {
345
- event.stopPropagation();
346
- if (isSelectable) {
347
- handleSelect();
348
- }
349
- else {
350
- toggleExpanded();
351
- }
352
- }
353
665
  else if (event.type === TREE_EVENTS.action) {
354
666
  event.stopPropagation();
355
667
  if (actionMenuItems && !isDisabled) {
@@ -357,59 +669,51 @@ const Item = (_a) => {
357
669
  setIsMenuOpen(true);
358
670
  }
359
671
  }
360
- }, [
361
- actionMenuItems,
362
- hasChildren,
363
- isExpanded,
364
- isDisabled,
365
- isSelectable,
366
- setIsExpanded,
367
- handleSelect,
368
- toggleExpanded,
369
- ]);
672
+ }, [actionMenuItems, hasChildren, isExpanded, isDisabled, setIsExpanded]);
370
673
  const callbackRef = useCallback((node) => {
371
674
  const prev = itemRef.current;
372
675
  if (prev) {
373
676
  prev.removeEventListener(TREE_EVENTS.expand, handleCustomEvents);
374
677
  prev.removeEventListener(TREE_EVENTS.collapse, handleCustomEvents);
375
- prev.removeEventListener(TREE_EVENTS.select, handleCustomEvents);
376
678
  prev.removeEventListener(TREE_EVENTS.action, handleCustomEvents);
377
679
  }
378
680
  itemRef.current = node;
379
681
  if (node) {
380
682
  node.addEventListener(TREE_EVENTS.expand, handleCustomEvents);
381
683
  node.addEventListener(TREE_EVENTS.collapse, handleCustomEvents);
382
- node.addEventListener(TREE_EVENTS.select, handleCustomEvents);
383
684
  node.addEventListener(TREE_EVENTS.action, handleCustomEvents);
384
685
  }
385
686
  }, [handleCustomEvents]);
386
687
  const mergedRef = useMergeRefs([callbackRef, ref]);
387
- const isFocused = focusedId === itemId;
388
688
  const selectionAriaProps = (() => {
389
689
  if (!isSelectable) {
390
690
  return {};
391
691
  }
392
692
  if (isMultiple) {
393
693
  return {
394
- 'aria-checked': isIndeterminate
395
- ? 'mixed'
396
- : (isSelected !== null && isSelected !== void 0 ? isSelected : false),
694
+ 'aria-checked': isIndeterminate ? 'mixed' : isSelected,
397
695
  };
398
696
  }
399
- return { 'aria-selected': isSelected !== null && isSelected !== void 0 ? isSelected : false };
697
+ return { 'aria-selected': isSelected };
400
698
  })();
699
+ const edgeClass = edge === undefined ? undefined : `ndl-tree-view-new-item--selected-${edge}`;
700
+ const childContextValue = useMemo(() => (Object.assign(Object.assign({}, treeViewContext), { level: treeViewContext.level + 1, parentId: itemId })), [itemId, treeViewContext]);
701
+ // Same caching as in Root: keeps child element identity stable across
702
+ // renders of this item (selection, focus, expansion) so unchanged
703
+ // descendants bail out of re-rendering.
704
+ const annotatedChildren = useMemo(() => annotateTreeItems(children), [children]);
401
705
  const itemElement = (
402
706
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events -- keyboard handled on tree root; aria-selected/aria-checked applied via spread
403
- _jsx("div", Object.assign({ ref: mergedRef, id: itemId,
707
+ _jsx("div", Object.assign({ ref: mergedRef,
404
708
  // eslint-disable-next-line jsx-a11y/role-has-required-aria-props -- aria-selected/aria-checked applied conditionally via spread
405
- role: "treeitem" }, selectionAriaProps, { "aria-expanded": hasChildren ? isExpanded : undefined, "aria-owns": hasChildren ? groupId : undefined, "aria-disabled": isDisabled || undefined, "aria-busy": isLoading || undefined, "aria-keyshortcuts": actionMenuItems ? 'Shift+F10' : undefined, "aria-labelledby": titleId, "aria-level": level, tabIndex: isFocused ? 0 : -1, className: classNames('ndl-tree-view-new-item', className, {
709
+ role: "treeitem" }, selectionAriaProps, { "aria-expanded": hasChildren ? isExpanded : undefined, "aria-owns": hasChildren ? groupId : undefined, "aria-disabled": isDisabled || undefined, "aria-busy": isLoading || undefined, "aria-keyshortcuts": actionMenuItems ? 'Shift+F10' : undefined, "aria-labelledby": titleId, "aria-level": level, tabIndex: isFocused ? 0 : -1, className: classNames('ndl-tree-view-new-item', className, edgeClass, {
406
710
  'ndl-tree-view-new-item--disabled': isDisabled,
407
711
  'ndl-tree-view-new-item--parent': hasChildren,
408
712
  'ndl-tree-view-new-item--selected': isSelected && isSelectable,
409
713
  }), style: Object.assign({ '--tree-level': level - 1 }, style), onClick: (event) => {
410
714
  event.stopPropagation();
411
- handleClick();
412
- } }, htmlAttributes, restProps, { children: _jsxs("div", { className: classNames('ndl-tree-view-new-item-content', {
715
+ handleClick(event);
716
+ } }, htmlAttributes, restProps, { id: itemId, children: _jsxs("div", { className: classNames('ndl-tree-view-new-item-content', {
413
717
  'ndl-tree-view-new-item-content--parent': hasChildren,
414
718
  }), children: [hasChildren && (
415
719
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- keyboard handled on tree root
@@ -428,7 +732,7 @@ const Item = (_a) => {
428
732
  },
429
733
  tabIndex: -1,
430
734
  }, children: _jsx(EllipsisHorizontalIconOutline, {}) }), _jsx(Menu, Object.assign({ isOpen: isMenuOpen, anchorRef: actionButtonRef, onClose: handleMenuClose }, actionMenuProps, { children: actionMenuItems }))] }))] }) })));
431
- return (_jsxs(_Fragment, { children: [_jsx(ConditionalWrap, { shouldWrap: tooltipContent !== undefined && tooltipContent !== null, wrap: (children) => (_jsxs(Tooltip, Object.assign({ type: "simple", hoverDelay: { close: 0, open: 500 }, isDisabled: isMenuOpen, followCursor: "initial" }, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.root, { children: [_jsx(Tooltip.Trigger, Object.assign({ hasButtonWrapper: true }, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.trigger, { children: children })), _jsx(Tooltip.Content, Object.assign({}, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.content, { children: tooltipContent }))] }))), children: itemElement }), hasChildren && (_jsx(TreeViewContext.Provider, { value: { focusedId, level: level + 1, selectionMode, setFocusedId }, children: _jsx("div", { role: "group", id: groupId, hidden: !isExpanded, style: { '--tree-level': level - 1 }, children: annotateTreeItems(children) }) }))] }));
735
+ return (_jsxs(_Fragment, { children: [_jsx(ConditionalWrap, { shouldWrap: tooltipContent !== undefined && tooltipContent !== null, wrap: (children) => (_jsxs(Tooltip, Object.assign({ type: "simple", hoverDelay: { close: 0, open: 500 }, isDisabled: isMenuOpen, followCursor: "initial" }, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.root, { children: [_jsx(Tooltip.Trigger, Object.assign({ hasButtonWrapper: true }, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.trigger, { children: children })), _jsx(Tooltip.Content, Object.assign({}, tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.content, { children: tooltipContent }))] }))), children: itemElement }), hasChildren && (_jsx(TreeViewContext.Provider, { value: childContextValue, children: _jsx("div", { role: "group", id: groupId, hidden: !isExpanded, style: { '--tree-level': level - 1 }, children: annotatedChildren }) }))] }));
432
736
  };
433
737
  /**
434
738
  * A skeleton placeholder for tree items that are being loaded. Renders one or
@@ -440,7 +744,7 @@ const Item = (_a) => {
440
744
  *
441
745
  * @example
442
746
  * ```tsx
443
- * <TreeView.Item title="Lazy folder" hasChildren isLoading={isLoading} isExpanded>
747
+ * <TreeView.Item title="Lazy folder" hasChildren isLoading isExpanded>
444
748
  * {isLoading && <TreeView.SkeletonItem rows={3} />}
445
749
  * </TreeView.Item>
446
750
  * ```
@@ -450,6 +754,20 @@ const SkeletonItem = (_a) => {
450
754
  const { level } = useContext(TreeViewContext);
451
755
  return (_jsx("div", Object.assign({ ref: ref, "aria-hidden": "true", className: classNames('ndl-tree-view-new-skeleton', className), style: Object.assign({ '--tree-level': level - 1 }, style) }, htmlAttributes, restProps, { children: Array.from({ length: rows }, (_unused, index) => (_jsx(Skeleton, { height: "18px", width: "100%" }, index))) })));
452
756
  };
757
+ /**
758
+ * @remarks
759
+ * Selection can be managed per item, with `isSelected` and `onSelectedChange`
760
+ * on each `TreeView.Item`, or by the tree itself, by passing `selectedIds` (or
761
+ * `defaultSelectedIds`) together with `onSelectedIdsChange`. When the tree
762
+ * manages selection, every item needs an `id`, and each item's own
763
+ * `isSelected` and `onSelectedChange` are ignored.
764
+ *
765
+ * The available selection modes are:
766
+ *
767
+ * - `single` — one item at a time. An unmodified click selects the clicked item and never clears it.
768
+ * - `multiple` — multi-select with checkboxes. An unmodified click toggles an item, and `Shift` repeats that across a range without clearing anything outside it.
769
+ * - `multiple-replace` — multi-select without checkboxes. An unmodified click selects only that item, `Cmd`/`Ctrl` toggles it and moves the anchor, and `Shift` extends from that anchor while retaining its selection snapshot. `Shift` + arrow extends the range.
770
+ */
453
771
  const TreeView = Object.assign(Root, { Item, SkeletonItem });
454
772
  export { TreeView };
455
773
  //# sourceMappingURL=TreeView.js.map