@astryxdesign/core 0.1.2-canary.bd8c3d3 → 0.1.2-canary.bfcbf64

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 (223) hide show
  1. package/dist/Avatar/Avatar.d.ts.map +1 -1
  2. package/dist/Avatar/Avatar.js +11 -5
  3. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  4. package/dist/Breadcrumbs/BreadcrumbItem.js +20 -4
  5. package/dist/Calendar/Calendar.d.ts.map +1 -1
  6. package/dist/Calendar/Calendar.js +11 -11
  7. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  8. package/dist/CheckboxInput/CheckboxInput.js +4 -2
  9. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  10. package/dist/CheckboxList/CheckboxList.js +12 -4
  11. package/dist/ContextMenu/ContextMenu.d.ts +6 -1
  12. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  13. package/dist/ContextMenu/ContextMenu.js +58 -7
  14. package/dist/DateInput/DateInput.d.ts.map +1 -1
  15. package/dist/DateInput/DateInput.js +17 -2
  16. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  17. package/dist/DateRangeInput/DateRangeInput.js +9 -4
  18. package/dist/DateTimeInput/DateTimeInput.d.ts +7 -1
  19. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  20. package/dist/DateTimeInput/DateTimeInput.js +15 -3
  21. package/dist/Dialog/Dialog.d.ts.map +1 -1
  22. package/dist/Dialog/Dialog.js +12 -0
  23. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenu.js +25 -4
  25. package/dist/Field/Field.d.ts +19 -2
  26. package/dist/Field/Field.d.ts.map +1 -1
  27. package/dist/Field/Field.js +4 -0
  28. package/dist/Field/FieldLabel.d.ts +21 -2
  29. package/dist/Field/FieldLabel.d.ts.map +1 -1
  30. package/dist/Field/FieldLabel.js +37 -22
  31. package/dist/FileInput/FileInput.d.ts.map +1 -1
  32. package/dist/FileInput/FileInput.js +27 -6
  33. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  34. package/dist/InputGroup/InputGroup.js +4 -1
  35. package/dist/Kbd/Kbd.d.ts.map +1 -1
  36. package/dist/Kbd/Kbd.js +33 -1
  37. package/dist/Layer/useLayer.d.ts.map +1 -1
  38. package/dist/Layer/useLayer.js +36 -5
  39. package/dist/Layout/padding.stylex.d.ts +51 -0
  40. package/dist/Layout/padding.stylex.d.ts.map +1 -1
  41. package/dist/Layout/padding.stylex.js +63 -0
  42. package/dist/Link/Link.d.ts +7 -1
  43. package/dist/Link/Link.d.ts.map +1 -1
  44. package/dist/Link/Link.js +12 -4
  45. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  46. package/dist/MultiSelector/MultiSelector.js +35 -6
  47. package/dist/MultiSelector/hooks.d.ts +12 -1
  48. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  49. package/dist/MultiSelector/hooks.js +14 -1
  50. package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
  51. package/dist/NavMenu/NavHeadingMenu.js +17 -2
  52. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  53. package/dist/NumberInput/NumberInput.js +7 -1
  54. package/dist/Pagination/Pagination.d.ts.map +1 -1
  55. package/dist/Pagination/Pagination.js +8 -0
  56. package/dist/ProgressBar/ProgressBar.js +1 -1
  57. package/dist/RadioList/RadioList.d.ts +1 -1
  58. package/dist/RadioList/RadioList.d.ts.map +1 -1
  59. package/dist/RadioList/RadioList.js +76 -3
  60. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  61. package/dist/SegmentedControl/SegmentedControlItem.js +6 -2
  62. package/dist/Selector/Selector.d.ts.map +1 -1
  63. package/dist/Selector/Selector.js +15 -8
  64. package/dist/Selector/hooks.d.ts +8 -1
  65. package/dist/Selector/hooks.d.ts.map +1 -1
  66. package/dist/Selector/hooks.js +14 -1
  67. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  68. package/dist/Skeleton/Skeleton.js +8 -2
  69. package/dist/Stack/Stack.d.ts +28 -1
  70. package/dist/Stack/Stack.d.ts.map +1 -1
  71. package/dist/Stack/Stack.js +17 -1
  72. package/dist/Stack/StackItem.d.ts +12 -1
  73. package/dist/Stack/StackItem.d.ts.map +1 -1
  74. package/dist/Stack/StackItem.js +8 -1
  75. package/dist/TabList/Tab.d.ts.map +1 -1
  76. package/dist/TabList/Tab.js +6 -0
  77. package/dist/TabList/TabList.d.ts +15 -4
  78. package/dist/TabList/TabList.d.ts.map +1 -1
  79. package/dist/TabList/TabList.js +58 -4
  80. package/dist/TabList/TabListContext.d.ts +5 -0
  81. package/dist/TabList/TabListContext.d.ts.map +1 -1
  82. package/dist/TabList/TabListContext.js +5 -0
  83. package/dist/TabList/TabMenu.js +2 -0
  84. package/dist/Toast/Toast.d.ts.map +1 -1
  85. package/dist/Toast/Toast.js +14 -0
  86. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  87. package/dist/Toast/ToastViewport.js +132 -3
  88. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  89. package/dist/Toolbar/Toolbar.js +7 -3
  90. package/dist/TreeList/TreeList.d.ts +1 -1
  91. package/dist/TreeList/TreeList.d.ts.map +1 -1
  92. package/dist/TreeList/TreeList.js +78 -2
  93. package/dist/TreeList/TreeListItem.d.ts +10 -1
  94. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  95. package/dist/TreeList/TreeListItem.js +44 -10
  96. package/dist/TreeList/treeListItem.markers.stylex.d.ts +16 -0
  97. package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +1 -0
  98. package/dist/TreeList/treeListItem.markers.stylex.js +22 -0
  99. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  100. package/dist/Typeahead/BaseTypeahead.js +41 -3
  101. package/dist/astryx.css +3 -0
  102. package/dist/astryx.umd.js +47 -47
  103. package/dist/astryx.umd.js.map +4 -4
  104. package/dist/hooks/index.d.ts +10 -2
  105. package/dist/hooks/index.d.ts.map +1 -1
  106. package/dist/hooks/index.js +6 -2
  107. package/dist/hooks/useAnnounce.d.ts +40 -0
  108. package/dist/hooks/useAnnounce.d.ts.map +1 -0
  109. package/dist/hooks/useAnnounce.js +142 -0
  110. package/dist/hooks/useClickableContainer.d.ts +16 -1
  111. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  112. package/dist/hooks/useClickableContainer.js +16 -4
  113. package/dist/hooks/useFocusTrap.d.ts +18 -0
  114. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  115. package/dist/hooks/useFocusTrap.js +65 -0
  116. package/dist/hooks/useListFocus.d.ts +79 -8
  117. package/dist/hooks/useListFocus.d.ts.map +1 -1
  118. package/dist/hooks/useListFocus.js +262 -48
  119. package/dist/hooks/useLongPress.d.ts +21 -0
  120. package/dist/hooks/useLongPress.d.ts.map +1 -0
  121. package/dist/hooks/useLongPress.js +89 -0
  122. package/dist/hooks/useTreeFocus.d.ts +133 -0
  123. package/dist/hooks/useTreeFocus.d.ts.map +1 -0
  124. package/dist/hooks/useTreeFocus.js +274 -0
  125. package/dist/hooks/useTypeahead.d.ts +63 -0
  126. package/dist/hooks/useTypeahead.d.ts.map +1 -0
  127. package/dist/hooks/useTypeahead.js +117 -0
  128. package/package.json +1 -1
  129. package/src/Avatar/Avatar.test.tsx +37 -0
  130. package/src/Avatar/Avatar.tsx +10 -5
  131. package/src/Breadcrumbs/BreadcrumbItem.tsx +20 -5
  132. package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -10
  133. package/src/Calendar/Calendar.test.tsx +25 -0
  134. package/src/Calendar/Calendar.tsx +10 -12
  135. package/src/CheckboxInput/CheckboxInput.test.tsx +14 -25
  136. package/src/CheckboxInput/CheckboxInput.tsx +4 -2
  137. package/src/CheckboxList/CheckboxList.test.tsx +30 -14
  138. package/src/CheckboxList/CheckboxList.tsx +18 -3
  139. package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
  140. package/src/ContextMenu/ContextMenu.test.tsx +115 -1
  141. package/src/ContextMenu/ContextMenu.tsx +75 -4
  142. package/src/DateInput/DateInput.test.tsx +59 -0
  143. package/src/DateInput/DateInput.tsx +24 -7
  144. package/src/DateRangeInput/DateRangeInput.test.tsx +69 -18
  145. package/src/DateRangeInput/DateRangeInput.tsx +7 -3
  146. package/src/DateTimeInput/DateTimeInput.doc.mjs +7 -0
  147. package/src/DateTimeInput/DateTimeInput.test.tsx +99 -6
  148. package/src/DateTimeInput/DateTimeInput.tsx +32 -3
  149. package/src/Dialog/Dialog.tsx +12 -0
  150. package/src/DropdownMenu/DropdownMenu.test.tsx +26 -4
  151. package/src/DropdownMenu/DropdownMenu.tsx +36 -2
  152. package/src/Field/Field.test.tsx +32 -0
  153. package/src/Field/Field.tsx +23 -5
  154. package/src/Field/FieldLabel.tsx +53 -17
  155. package/src/FileInput/FileInput.test.tsx +104 -49
  156. package/src/FileInput/FileInput.tsx +27 -3
  157. package/src/InputGroup/InputGroup.test.tsx +21 -63
  158. package/src/InputGroup/InputGroup.tsx +4 -1
  159. package/src/Kbd/Kbd.test.tsx +22 -4
  160. package/src/Kbd/Kbd.tsx +35 -3
  161. package/src/Layer/useLayer.tsx +52 -5
  162. package/src/Layout/padding.stylex.ts +52 -0
  163. package/src/Link/Link.doc.mjs +14 -0
  164. package/src/Link/Link.test.tsx +32 -0
  165. package/src/Link/Link.tsx +23 -1
  166. package/src/MultiSelector/MultiSelector.test.tsx +124 -2
  167. package/src/MultiSelector/MultiSelector.tsx +50 -9
  168. package/src/MultiSelector/hooks.ts +27 -0
  169. package/src/NavMenu/NavHeadingMenu.test.tsx +32 -1
  170. package/src/NavMenu/NavHeadingMenu.tsx +30 -2
  171. package/src/NumberInput/NumberInput.test.tsx +72 -38
  172. package/src/NumberInput/NumberInput.tsx +12 -1
  173. package/src/Pagination/Pagination.test.tsx +58 -5
  174. package/src/Pagination/Pagination.tsx +12 -0
  175. package/src/ProgressBar/ProgressBar.test.tsx +34 -26
  176. package/src/ProgressBar/ProgressBar.tsx +1 -1
  177. package/src/RadioList/RadioList.test.tsx +129 -14
  178. package/src/RadioList/RadioList.tsx +103 -8
  179. package/src/SegmentedControl/SegmentedControl.test.tsx +35 -0
  180. package/src/SegmentedControl/SegmentedControlItem.tsx +4 -1
  181. package/src/Selector/Selector.test.tsx +53 -0
  182. package/src/Selector/Selector.tsx +15 -8
  183. package/src/Selector/hooks.ts +23 -0
  184. package/src/Skeleton/Skeleton.test.tsx +25 -0
  185. package/src/Skeleton/Skeleton.tsx +11 -1
  186. package/src/Stack/Stack.doc.mjs +57 -0
  187. package/src/Stack/Stack.test.tsx +55 -5
  188. package/src/Stack/Stack.tsx +53 -1
  189. package/src/Stack/StackItem.test.tsx +23 -0
  190. package/src/Stack/StackItem.tsx +20 -0
  191. package/src/TabList/Tab.tsx +6 -0
  192. package/src/TabList/TabList.doc.mjs +6 -0
  193. package/src/TabList/TabList.test.tsx +167 -0
  194. package/src/TabList/TabList.tsx +72 -9
  195. package/src/TabList/TabListContext.ts +7 -3
  196. package/src/TabList/TabMenu.tsx +3 -3
  197. package/src/Toast/Toast.tsx +14 -0
  198. package/src/Toast/ToastViewport.test.tsx +204 -0
  199. package/src/Toast/ToastViewport.tsx +167 -21
  200. package/src/Toolbar/Toolbar.test.tsx +45 -0
  201. package/src/Toolbar/Toolbar.tsx +5 -2
  202. package/src/TreeList/TreeList.test.tsx +273 -0
  203. package/src/TreeList/TreeList.tsx +84 -1
  204. package/src/TreeList/TreeListItem.tsx +42 -2
  205. package/src/TreeList/treeListItem.markers.stylex.ts +20 -0
  206. package/src/Typeahead/BaseTypeahead.tsx +48 -2
  207. package/src/Typeahead/Typeahead.test.tsx +125 -0
  208. package/src/hooks/index.ts +21 -2
  209. package/src/hooks/useAnnounce.test.tsx +101 -0
  210. package/src/hooks/useAnnounce.ts +168 -0
  211. package/src/hooks/useClickableContainer.ts +16 -4
  212. package/src/hooks/useFocusTrap.test.tsx +67 -3
  213. package/src/hooks/useFocusTrap.ts +75 -0
  214. package/src/hooks/useListFocus.doc.mjs +49 -10
  215. package/src/hooks/useListFocus.test.tsx +269 -0
  216. package/src/hooks/useListFocus.ts +339 -54
  217. package/src/hooks/useLongPress.test.tsx +134 -0
  218. package/src/hooks/useLongPress.ts +119 -0
  219. package/src/hooks/useTreeFocus.doc.mjs +126 -0
  220. package/src/hooks/useTreeFocus.test.tsx +214 -0
  221. package/src/hooks/useTreeFocus.ts +464 -0
  222. package/src/hooks/useTypeahead.test.tsx +109 -0
  223. package/src/hooks/useTypeahead.ts +183 -0
@@ -2,11 +2,12 @@
2
2
 
3
3
  'use client';
4
4
 
5
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
6
6
  import * as stylex from '@stylexjs/stylex';
7
7
  import "../theme/tokens.stylex.js";
8
8
  import { spacingVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
9
9
  import { mergeProps } from "../utils/index.js";
10
+ import { INTERACTIVE_SELECTORS } from "../hooks/useClickableContainer.js";
10
11
  import { Toast } from "./Toast.js";
11
12
  import { ToastContext } from "./ToastContext.js";
12
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -74,6 +75,39 @@ export function ToastViewport({
74
75
  const [exitingIds, setExitingIds] = useState(new Set());
75
76
  const toastsRef = useRef(toasts);
76
77
  toastsRef.current = toasts;
78
+
79
+ // Show the popover on mount so it enters the top layer.
80
+ const viewportRef = useRef(null);
81
+ // The element that was focused before the user jumped into the viewport
82
+ // (via F6). Used to restore focus once all toasts are dismissed so focus
83
+ // never falls to <body>.
84
+ const prevFocusRef = useRef(null);
85
+ // When a toast is dismissed while focus lives inside it, we need to move
86
+ // focus to a sensible neighbor after that toast unmounts. This holds the
87
+ // id of the toast whose removal should trigger a focus handoff.
88
+ const focusHandoffIdRef = useRef(null);
89
+ // The next toast id that should receive focus once the dismissed toast has
90
+ // unmounted, or 'restore' to fall back to the previously-focused element.
91
+ const pendingFocusRef = useRef(null);
92
+
93
+ // Collect a focusable control within a toast node, if any.
94
+ // Reuses the canonical INTERACTIVE_SELECTORS list (native controls plus
95
+ // role-based interactive elements) instead of a hand-rolled subset, then
96
+ // narrows to the first candidate that can actually receive focus —
97
+ // excluding elements opted out with `tabindex="-1"` and disabled controls.
98
+ const getFocusable = useCallback(container => {
99
+ if (!container) {
100
+ return null;
101
+ }
102
+ const candidates = container.querySelectorAll(INTERACTIVE_SELECTORS);
103
+ for (const candidate of candidates) {
104
+ if (candidate.getAttribute('tabindex') === '-1' || candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
105
+ continue;
106
+ }
107
+ return candidate;
108
+ }
109
+ return null;
110
+ }, []);
77
111
  const addToast = useCallback(entry => {
78
112
  setToasts(prev => {
79
113
  const {
@@ -97,6 +131,25 @@ export function ToastViewport({
97
131
  if (entry) {
98
132
  entry.options.onHide?.(reason);
99
133
  }
134
+ // If focus currently lives inside the toast being dismissed, remember
135
+ // that its removal must hand focus off to a neighbor (or the element
136
+ // focused before the user entered the viewport) rather than <body>.
137
+ const el = viewportRef.current;
138
+ const active = document.activeElement;
139
+ const dismissedNode = el?.querySelector(`[data-toast-id="${id}"]`) ?? null;
140
+ if (dismissedNode && active instanceof Node && dismissedNode.contains(active)) {
141
+ focusHandoffIdRef.current = id;
142
+ // Pick the neighbor to receive focus while the DOM is still intact:
143
+ // prefer the next toast, then the previous, else restore.
144
+ const remaining = toastsRef.current.filter(t => t.id !== id);
145
+ if (remaining.length > 0) {
146
+ const dismissedIndex = toastsRef.current.findIndex(t => t.id === id);
147
+ const next = toastsRef.current[dismissedIndex + 1] ?? toastsRef.current[dismissedIndex - 1];
148
+ pendingFocusRef.current = next ? next.id : 'restore';
149
+ } else {
150
+ pendingFocusRef.current = 'restore';
151
+ }
152
+ }
100
153
  setExitingIds(prev => {
101
154
  if (prev.has(id)) {
102
155
  return prev;
@@ -115,6 +168,41 @@ export function ToastViewport({
115
168
  });
116
169
  setToasts(prev => prev.filter(t => t.id !== id));
117
170
  }, []);
171
+
172
+ // After a dismissed toast unmounts, hand focus off so it never falls to
173
+ // <body>. Runs in a layout effect once the toast list no longer contains
174
+ // the dismissed toast.
175
+ useLayoutEffect(() => {
176
+ const handoffId = focusHandoffIdRef.current;
177
+ const target = pendingFocusRef.current;
178
+ if (handoffId == null || target == null) {
179
+ return;
180
+ }
181
+ // Wait until the dismissed toast is actually gone from the list.
182
+ if (toasts.some(t => t.id === handoffId)) {
183
+ return;
184
+ }
185
+ focusHandoffIdRef.current = null;
186
+ pendingFocusRef.current = null;
187
+ const el = viewportRef.current;
188
+ if (target !== 'restore' && el) {
189
+ const nextNode = el.querySelector(`[data-toast-id="${target}"]`);
190
+ const focusable = getFocusable(nextNode) ?? nextNode;
191
+ if (focusable) {
192
+ focusable.focus();
193
+ return;
194
+ }
195
+ }
196
+ // No remaining toast to receive focus — restore the previously-focused
197
+ // element if it's still connected, else fall back to the container.
198
+ const prev = prevFocusRef.current;
199
+ prevFocusRef.current = null;
200
+ if (prev && prev.isConnected) {
201
+ prev.focus();
202
+ } else if (el) {
203
+ el.focus();
204
+ }
205
+ }, [toasts, getFocusable]);
118
206
  const findByUniqueID = useCallback(uid => {
119
207
  return toastsRef.current.find(t => t.options.uniqueID === uid);
120
208
  }, []);
@@ -139,7 +227,6 @@ export function ToastViewport({
139
227
  }
140
228
 
141
229
  // Show the popover on mount so it enters the top layer
142
- const viewportRef = useRef(null);
143
230
  useEffect(() => {
144
231
  if (!isTopLayer) {
145
232
  return;
@@ -153,13 +240,54 @@ export function ToastViewport({
153
240
  }
154
241
  }
155
242
  }, [isTopLayer]);
243
+
244
+ // F6 jumps focus into the toast viewport — the standard "go to
245
+ // notifications" affordance. Focus the first control in the newest toast,
246
+ // or the viewport container if none. Toasts are non-modal, so this only
247
+ // moves focus in; Shift+Tab / Escape let focus leave naturally.
248
+ const hasToasts = toasts.length > 0;
249
+ useEffect(() => {
250
+ if (!hasToasts) {
251
+ return;
252
+ }
253
+ const handleKeyDown = e => {
254
+ if (e.key !== 'F6') {
255
+ return;
256
+ }
257
+ const el = viewportRef.current;
258
+ if (!el) {
259
+ return;
260
+ }
261
+ // Already inside the viewport — nothing to do.
262
+ const active = document.activeElement;
263
+ if (active instanceof Node && el.contains(active)) {
264
+ return;
265
+ }
266
+ e.preventDefault();
267
+ // Remember where focus was so it can be restored on dismiss.
268
+ if (active instanceof HTMLElement) {
269
+ prevFocusRef.current = active;
270
+ }
271
+ // Newest toast is the last one rendered in the DOM.
272
+ const toastNodes = el.querySelectorAll('[data-toast-id]');
273
+ const newest = toastNodes[toastNodes.length - 1] ?? null;
274
+ const focusable = getFocusable(newest) ?? newest ?? el;
275
+ focusable.focus();
276
+ };
277
+ document.addEventListener('keydown', handleKeyDown);
278
+ return () => {
279
+ document.removeEventListener('keydown', handleKeyDown);
280
+ };
281
+ }, [hasToasts, getFocusable]);
156
282
  const posStyle = position === 'topEnd' ? styles.topEnd : position === 'topStart' ? styles.topStart : position === 'bottomStart' ? styles.bottomStart : styles.bottomEnd;
157
283
  return /*#__PURE__*/_jsxs(ToastContext, {
158
284
  value: contextValue,
159
285
  children: [children, /*#__PURE__*/_jsx("div", {
160
286
  ref: viewportRef,
161
287
  role: "region",
162
- "aria-label": "Notifications"
288
+ "aria-label": "Notifications",
289
+ "aria-modal": false,
290
+ tabIndex: -1
163
291
  // popover="manual" promotes to the top layer (above dialogs).
164
292
  // Omitted inside dialogs where the viewport is already in a top layer.
165
293
  ,
@@ -174,6 +302,7 @@ export function ToastViewport({
174
302
  const dur = o.autoHideDuration ?? 5000;
175
303
  const isExiting = exitingIds.has(entry.id);
176
304
  return /*#__PURE__*/_jsx("div", {
305
+ "data-toast-id": entry.id,
177
306
  ...{
178
307
  0: {
179
308
  className: "x67bb7w xrvj5dj x1tu4anv xvmdzux xjshnde xuedmi6 xsagj69 xlr8y92 x1sopeo2 x5rzir7"
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,4BAA4B,CAAC;AAoH5D,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC;AAEtC,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC7D,gDAAgD;IAChD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;CACnD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,EACtB,YAAY,EACZ,aAAa,EACb,UAAU,EACV,KAAK,EACL,IAAW,EACX,GAAO,EACP,WAA0B,EAC1B,OAAuB,EACvB,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,YAAY,+BAgGd;yBA/Ge,OAAO"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/Toolbar/Toolbar.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,4BAA4B,CAAC;AAoH5D,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC;AAEtC,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC7D,gDAAgD;IAChD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACxC;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;CACnD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,EACtB,YAAY,EACZ,aAAa,EACb,UAAU,EACV,KAAK,EACL,IAAW,EACX,GAAO,EACP,WAA0B,EAC1B,OAAuB,EACvB,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,YAAY,+BAmGd;yBAlHe,OAAO"}
@@ -175,10 +175,13 @@ export function Toolbar({
175
175
  const gapVar = spacingVars[spacingStepToVar[gap]];
176
176
  const {
177
177
  listRef,
178
- handleKeyDown
178
+ handleKeyDown,
179
+ handleFocus
179
180
  } = useListFocus({
180
- itemSelector: 'button, input, [tabindex="0"]',
181
- orientation
181
+ itemSelector: 'button, input, [tabindex]',
182
+ orientation,
183
+ hasRovingTabIndex: true,
184
+ hasCaretGuard: true
182
185
  });
183
186
  return /*#__PURE__*/_jsx(SizeProvider, {
184
187
  value: size,
@@ -196,6 +199,7 @@ export function Toolbar({
196
199
  "aria-label": label,
197
200
  "aria-orientation": orientation,
198
201
  onKeyDown: handleKeyDown,
202
+ onFocus: handleFocus,
199
203
  ...mergeProps(themeProps('toolbar', {
200
204
  size
201
205
  }), stylex.props(hasCenterContent ? styles.baseGrid : styles.baseFlex, orientation === 'vertical' && styles.vertical, sizeStyles.base, dynamicStyles.gap(gapVar), dynamicStyles.tabIndicatorBottom(`calc(-1 * (${blockPaddingVarForSize[size]}${dividers?.includes('bottom') ? ' + 1px' : ''}))`))),
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file TreeList.tsx
3
- * @input Uses React, StyleX, theme tokens, TreeListItem, TreeListTypes
3
+ * @input Uses React, StyleX, theme tokens, TreeListItem, TreeListTypes, useTreeFocus
4
4
  * @output Exports TreeList component, TreeListProps type
5
5
  * @position Core implementation; consumed by index.ts
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"TreeList.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAwC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAI5E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,gBAAgB,EAAE,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAOvE,OAAO,EAAC,KAAK,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAE1B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA0CD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,OAAoB,EACpB,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,GACJ,EAAE,aAAa,+BAsGf;yBA/Ge,QAAQ"}
1
+ {"version":3,"file":"TreeList.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAwC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAI5E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,gBAAgB,EAAE,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAQvE,OAAO,EAAC,KAAK,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAE1B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;IAE1B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,OAAoB,EACpB,MAAM,EACN,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,GACJ,EAAE,aAAa,+BA4Jf;yBArKe,QAAQ"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file TreeList.tsx
7
- * @input Uses React, StyleX, theme tokens, TreeListItem, TreeListTypes
7
+ * @input Uses React, StyleX, theme tokens, TreeListItem, TreeListTypes, useTreeFocus
8
8
  * @output Exports TreeList component, TreeListProps type
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -21,6 +21,7 @@ import { spacingVars } from "../theme/tokens.stylex.js";
21
21
  import { mergeProps } from "../utils/index.js";
22
22
  import { TreeListItem } from "./TreeListItem.js";
23
23
  import { themeProps } from "../utils/themeProps.js";
24
+ import { useTreeFocus } from "../hooks/useTreeFocus.js";
24
25
 
25
26
  // =============================================================================
26
27
  // Types
@@ -55,6 +56,34 @@ function collectExpandedKeys(items) {
55
56
  return keys;
56
57
  }
57
58
 
59
+ /**
60
+ * Compute the initial roving-tabindex owner: the first selected enabled item
61
+ * in document order, else the first enabled item, else the first item.
62
+ * Only the top-level candidates are considered for the default so the tab stop
63
+ * is reachable even when the tree is fully collapsed.
64
+ */
65
+ function findDefaultActiveId(items) {
66
+ let firstEnabled;
67
+ const walk = list => {
68
+ for (const item of list) {
69
+ if (item.isSelected && item.isDisabled !== true) {
70
+ return item.id;
71
+ }
72
+ if (firstEnabled == null && item.isDisabled !== true) {
73
+ firstEnabled = item.id;
74
+ }
75
+ if (item.children != null && item.children.length > 0) {
76
+ const selected = walk(item.children);
77
+ if (selected != null) {
78
+ return selected;
79
+ }
80
+ }
81
+ }
82
+ return undefined;
83
+ };
84
+ return walk(items) ?? firstEnabled ?? items[0]?.id;
85
+ }
86
+
58
87
  // =============================================================================
59
88
  // Component
60
89
  // =============================================================================
@@ -106,6 +135,48 @@ export function TreeList({
106
135
  return next;
107
136
  });
108
137
  }, [expandedKeysFromProps]);
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Roving tabindex + APG tree keyboard model (via useTreeFocus)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ // The treeitem that currently owns the tree's single tab stop. The hook moves
144
+ // it around via onActiveChange; a data-driven default seeds it (selected item
145
+ // or first enabled).
146
+ const [activeId, setActiveId] = useState(() => findDefaultActiveId(items));
147
+
148
+ // If the seed id is no longer present (items changed), fall back to a valid
149
+ // default so the tree always has exactly one reachable tab stop.
150
+ const resolvedActiveId = useMemo(() => {
151
+ if (activeId == null) {
152
+ return findDefaultActiveId(items);
153
+ }
154
+ const exists = list => list.some(item => item.id === activeId || item.children != null && exists(item.children));
155
+ return exists(items) ? activeId : findDefaultActiveId(items);
156
+ }, [activeId, items]);
157
+
158
+ // Enter/Space activation: prefer the treeitem's own inner action (link or
159
+ // button); return true when handled so the hook does not also toggle. Scoped
160
+ // to this treeitem's own row — never a descendant treeitem's action inside an
161
+ // expanded group.
162
+ const activateItem = useCallback(current => {
163
+ const candidates = current.querySelectorAll('a[href], button:not([aria-label="Toggle children"])');
164
+ for (const candidate of candidates) {
165
+ if (candidate.closest('[role="treeitem"]') === current) {
166
+ candidate.click();
167
+ return true;
168
+ }
169
+ }
170
+ return false;
171
+ }, []);
172
+ const {
173
+ treeRef,
174
+ handleKeyDown
175
+ } = useTreeFocus({
176
+ onToggleExpand: handleToggle,
177
+ onActivate: activateItem,
178
+ onActiveChange: setActiveId
179
+ });
109
180
  function renderItems(items, nestedLevel, ancestorsIsLast) {
110
181
  return items.map((item, index) => {
111
182
  const isLast = index === items.length - 1;
@@ -131,7 +202,10 @@ export function TreeList({
131
202
  isExpanded: isExpanded,
132
203
  onToggle: handleToggle,
133
204
  density: density,
134
- renderedChildren: renderedChildren
205
+ renderedChildren: renderedChildren,
206
+ posInSet: index + 1,
207
+ setSize: items.length,
208
+ isTabbable: item.id === resolvedActiveId
135
209
  }, item.id);
136
210
  });
137
211
  }
@@ -148,8 +222,10 @@ export function TreeList({
148
222
  },
149
223
  children: header
150
224
  }), /*#__PURE__*/_jsx("ul", {
225
+ ref: treeRef,
151
226
  role: "tree",
152
227
  "aria-labelledby": header != null ? headerId : undefined,
228
+ onKeyDown: handleKeyDown,
153
229
  ...{
154
230
  className: "x1ghz6dp x1717udv x3ct3a4"
155
231
  },
@@ -31,8 +31,17 @@ export interface TreeListItemInternalProps {
31
31
  density: TreeListDensity;
32
32
  /** Pre-rendered children subtree (rendered by the parent recursion) */
33
33
  renderedChildren?: ReactNode;
34
+ /** 1-based position of this item among its siblings (aria-posinset). */
35
+ posInSet: number;
36
+ /** Number of siblings at this level (aria-setsize). */
37
+ setSize: number;
38
+ /**
39
+ * Whether this treeitem currently owns the tree's single tab stop
40
+ * (roving tabindex). Exactly one visible treeitem is tabbable at a time.
41
+ */
42
+ isTabbable: boolean;
34
43
  }
35
- export declare function TreeListItem({ id, label, description, startContent, endContent, onClick, href, target, isDisabled, isSelected, hasChildren, nestedLevel, isLast, ancestorsIsLast, isExpanded, onToggle, density, renderedChildren, }: TreeListItemInternalProps): import("react").JSX.Element;
44
+ export declare function TreeListItem({ id, label, description, startContent, endContent, onClick, href, target, isDisabled, isSelected, hasChildren, nestedLevel, isLast, ancestorsIsLast, isExpanded, onToggle, density, renderedChildren, posInSet, setSize, isTabbable, }: TreeListItemInternalProps): import("react").JSX.Element;
36
45
  export declare namespace TreeListItem {
37
46
  var displayName: string;
38
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TreeListItem.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAiB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAcrD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;AA0MrD,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,SAAS,CAAC;CAC9B;AAMD,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,WAAW,EACX,WAAW,EACX,MAAM,EACN,eAAe,EACf,UAAU,EACV,QAAQ,EACR,OAAO,EACP,gBAAgB,GACjB,EAAE,yBAAyB,+BAyK3B;yBA5Le,YAAY"}
1
+ {"version":3,"file":"TreeListItem.d.ts","sourceRoot":"","sources":["../../src/TreeList/TreeListItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAiB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAcrD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAoNrD,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAMD,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAkB,EAClB,UAAkB,EAClB,WAAW,EACX,WAAW,EACX,MAAM,EACN,eAAe,EACf,UAAU,EACV,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,yBAAyB,+BA2L3B;yBAjNe,YAAY"}
@@ -21,6 +21,8 @@ import { getIcon } from "../Icon/globalIconRegistry.js";
21
21
  import { mergeProps } from "../utils/index.js";
22
22
  import { useLinkComponent } from "../Link/useLinkComponent.js";
23
23
  import { TreeListBranches } from "./TreeListBranches.js";
24
+ import "./treeListItem.markers.stylex.js";
25
+ import { treeItemScope } from "./treeListItem.markers.stylex.js";
24
26
  import { themeProps } from "../utils/themeProps.js";
25
27
 
26
28
  // =============================================================================
@@ -28,6 +30,15 @@ import { themeProps } from "../utils/themeProps.js";
28
30
  // =============================================================================
29
31
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
30
32
  const styles = {
33
+ wrapper: {
34
+ kH6xsr: "x3ct3a4",
35
+ kogj98: "x1ghz6dp",
36
+ kmVPX3: "x1717udv",
37
+ kVAEAm: "x1n2onr6",
38
+ kzqmXN: "xh8yej3",
39
+ kI3sdo: "x1a2a7pz",
40
+ $$css: true
41
+ },
31
42
  contentWrapper: {
32
43
  kaIpWk: "xh6dtrn",
33
44
  k1xSpc: "x78zum5",
@@ -50,8 +61,8 @@ const styles = {
50
61
  $$css: true
51
62
  },
52
63
  focusVisibleOutline: {
53
- kI3sdo: "x1a2a7pz x1irc7jg",
54
- kInvED: "x1wfwxd8 xdjuwb3",
64
+ kI3sdo: "x1a2a7pz xr21g96 x1irc7jg",
65
+ kInvED: "x1wfwxd8 xs4y3kv xdjuwb3",
55
66
  $$css: true
56
67
  },
57
68
  disabled: {
@@ -133,7 +144,10 @@ export function TreeListItem({
133
144
  isExpanded,
134
145
  onToggle,
135
146
  density,
136
- renderedChildren
147
+ renderedChildren,
148
+ posInSet,
149
+ setSize,
150
+ isTabbable
137
151
  }) {
138
152
  const labelId = useId();
139
153
  const descriptionId = useId();
@@ -198,7 +212,11 @@ export function TreeListItem({
198
212
  type: "button",
199
213
  "aria-expanded": isExpanded,
200
214
  "aria-label": "Toggle children",
201
- disabled: isDisabled,
215
+ disabled: isDisabled
216
+ // Roving tabindex lives on the treeitem row; the chevron toggle is not
217
+ // a separate tab stop. Row-level Enter/Space forwards to this button.
218
+ ,
219
+ tabIndex: -1,
202
220
  onClick: handleToggle,
203
221
  ...{
204
222
  className: "x78zum5 x6s0dn4 xl56j7k x12xnipv x6b6gus xho7bvi x1ypdohk xv9yike xx3sua9 x11g1kdw xmequkd"
@@ -224,8 +242,11 @@ export function TreeListItem({
224
242
  target: target,
225
243
  "aria-disabled": isDisabled || undefined,
226
244
  "aria-labelledby": labelId,
227
- "aria-describedby": description != null ? descriptionId : undefined,
228
- tabIndex: isDisabled ? -1 : undefined,
245
+ "aria-describedby": description != null ? descriptionId : undefined
246
+ // Roving tabindex lives on the treeitem row; inner action is not a
247
+ // separate tab stop. Activation is forwarded from the row.
248
+ ,
249
+ tabIndex: -1,
229
250
  ...{
230
251
  className: "xmper1u xln7xf2 x1heor9g x78zum5 xdt5ytf x98rzlu xeuugli x1yc453h x1hl2dhg x1a2a7pz"
231
252
  },
@@ -235,7 +256,11 @@ export function TreeListItem({
235
256
  onClick: onClick,
236
257
  disabled: isDisabled,
237
258
  "aria-labelledby": labelId,
238
- "aria-describedby": description != null ? descriptionId : undefined,
259
+ "aria-describedby": description != null ? descriptionId : undefined
260
+ // Roving tabindex lives on the treeitem row; inner action is not a
261
+ // separate tab stop. Activation is forwarded from the row.
262
+ ,
263
+ tabIndex: -1,
239
264
  ...{
240
265
  className: "xmper1u xln7xf2 x1heor9g x78zum5 xdt5ytf x98rzlu xeuugli x1yc453h x1a2a7pz"
241
266
  },
@@ -257,9 +282,18 @@ export function TreeListItem({
257
282
  "aria-expanded": hasChildren ? isExpanded : undefined,
258
283
  "aria-selected": isSelected || undefined,
259
284
  "aria-disabled": isDisabled || undefined,
260
- ...{
261
- className: "x3ct3a4 x1ghz6dp x1717udv x1n2onr6 xh8yej3"
262
- },
285
+ "aria-level": nestedLevel + 1,
286
+ "aria-posinset": posInSet,
287
+ "aria-setsize": setSize
288
+ // Roving tabindex: exactly one visible treeitem is tabbable at a time.
289
+ // Disabled items are skipped by the tree keyboard handler but remain
290
+ // in the accessibility tree.
291
+ ,
292
+ tabIndex: isDisabled ? -1 : isTabbable ? 0 : -1,
293
+ "data-tree-id": id,
294
+ "data-tree-level": nestedLevel + 1,
295
+ "data-tree-disabled": isDisabled || undefined,
296
+ ...stylex.props(styles.wrapper, treeItemScope),
263
297
  children: [/*#__PURE__*/_jsx("div", {
264
298
  ...{
265
299
  className: "x12gdq22"
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file treeListItem.markers.stylex.ts
3
+ * @input Uses StyleX defineMarker
4
+ * @output Exports treeItemScope marker
5
+ * @position Scoped marker; consumed by TreeListItem.tsx
6
+ *
7
+ * SYNC: When modified, update /packages/core/src/TreeList/TreeListItem.tsx
8
+ */
9
+ import * as stylex from '@stylexjs/stylex';
10
+ /**
11
+ * Scoped marker placed on each treeitem row (`<li role="treeitem">`).
12
+ * Binds the row's `:focus-visible` outline to its OWN treeitem, so focusing
13
+ * a parent treeitem does not leak the focus ring onto descendant rows.
14
+ */
15
+ export declare const treeItemScope: ReturnType<typeof stylex.defineMarker>;
16
+ //# sourceMappingURL=treeListItem.markers.stylex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"treeListItem.markers.stylex.d.ts","sourceRoot":"","sources":["../../src/TreeList/treeListItem.markers.stylex.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,YAAY,CAC1C,CAAC"}
@@ -0,0 +1,22 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file treeListItem.markers.stylex.ts
5
+ * @input Uses StyleX defineMarker
6
+ * @output Exports treeItemScope marker
7
+ * @position Scoped marker; consumed by TreeListItem.tsx
8
+ *
9
+ * SYNC: When modified, update /packages/core/src/TreeList/TreeListItem.tsx
10
+ */
11
+
12
+ import * as stylex from '@stylexjs/stylex';
13
+
14
+ /**
15
+ * Scoped marker placed on each treeitem row (`<li role="treeitem">`).
16
+ * Binds the row's `:focus-visible` outline to its OWN treeitem, so focusing
17
+ * a parent treeitem does not leak the focus ring onto descendant rows.
18
+ */
19
+ export const treeItemScope = {
20
+ x1g2q90y: "x1g2q90y",
21
+ $$css: true
22
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAanD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAO1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAuGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,EAmdrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
1
+ {"version":3,"file":"BaseTypeahead.d.ts","sourceRoot":"","sources":["../../src/Typeahead/BaseTypeahead.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAcnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAO1D,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CACxE,SAAS,CAAC,WAAW,CAAC,EACtB,UAAU,CACX;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;OAEG;IACH,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAEzC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAE/D;;;;OAIG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC3B;AAuGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,EAggBrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
@@ -19,6 +19,7 @@
19
19
  import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
20
20
  import * as stylex from '@stylexjs/stylex';
21
21
  import { usePopover } from "../Popover/usePopover.js";
22
+ import { useAnnounce } from "../hooks/useAnnounce.js";
22
23
  import { TypeaheadItem } from "./TypeaheadItem.js";
23
24
  import { Icon } from "../Icon/index.js";
24
25
  import "../theme/tokens.stylex.js";
@@ -158,6 +159,11 @@ export const BaseTypeahead = function BaseTypeahead({
158
159
  const listboxId = useId();
159
160
  const inputRef = useRef(null);
160
161
  const fallbackAnchorRef = useRef(null);
162
+
163
+ // Announce result counts / "no results" to screen readers via a persistent
164
+ // live region (comboboxes-6). The combobox's own popup carries no working
165
+ // live region, so highlight/result changes were previously silent.
166
+ const announce = useAnnounce();
161
167
  const [query, setQuery] = useState('');
162
168
  const [results, setResults] = useState([]);
163
169
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
@@ -239,11 +245,17 @@ export const BaseTypeahead = function BaseTypeahead({
239
245
  return;
240
246
  }
241
247
  resultsGenRef.current = gen;
242
- setResults(searchResults.slice(0, maxMenuItems));
248
+ const shown = searchResults.slice(0, maxMenuItems);
249
+ setResults(shown);
243
250
  setHighlightedIndex(searchResults.length > 0 ? 0 : -1);
244
251
  if (searchResults.length > 0 || searchQuery.length > 0) {
245
252
  showLayer();
246
253
  }
254
+ // Announce the outcome only for an active query (not the initial
255
+ // focus-open), so screen-reader users hear result counts / no-results.
256
+ if (searchQuery.length > 0) {
257
+ announce(shown.length === 0 ? emptySearchResultsText : `${shown.length} ${shown.length === 1 ? 'result' : 'results'}`);
258
+ }
247
259
  } catch {
248
260
  if (searchGenRef.current !== gen) {
249
261
  return;
@@ -255,7 +267,7 @@ export const BaseTypeahead = function BaseTypeahead({
255
267
  setIsLoading(false);
256
268
  }
257
269
  }
258
- }, [searchSource, maxMenuItems, showLayer]);
270
+ }, [searchSource, maxMenuItems, showLayer, announce, emptySearchResultsText]);
259
271
 
260
272
  // Perform bootstrap
261
273
  const performBootstrap = useCallback(async () => {
@@ -296,6 +308,8 @@ export const BaseTypeahead = function BaseTypeahead({
296
308
  searchSource.cancel?.();
297
309
  setResults([]);
298
310
  setHasSearched(false);
311
+ // Clear any lingering result-count / no-results announcement.
312
+ announce('');
299
313
  popover.hide();
300
314
  return;
301
315
  }
@@ -311,7 +325,7 @@ export const BaseTypeahead = function BaseTypeahead({
311
325
  } else {
312
326
  searchTimeoutRef.current = setTimeout(triggerSearch, debounceMs);
313
327
  }
314
- }, [onChangeQuery, hasEntriesOnFocus, performSearch, performBootstrap, popover, debounceMs, searchSource]);
328
+ }, [onChangeQuery, hasEntriesOnFocus, performSearch, performBootstrap, popover, debounceMs, searchSource, announce]);
315
329
 
316
330
  // Handle input change
317
331
  const handleInputChange = useCallback(e => {
@@ -351,6 +365,29 @@ export const BaseTypeahead = function BaseTypeahead({
351
365
  }
352
366
  }, [isDisabled, hasEntriesOnFocus, results.length, query.length, performBootstrap, showLayer]);
353
367
 
368
+ // Handle blur — close the dropdown when focus leaves the input for an
369
+ // element that is neither inside the field wrapper (anchor) nor inside the
370
+ // dropdown popover. The native popover="auto" light-dismiss only fires on
371
+ // outside pointer clicks and Escape; it does not close when focus moves away
372
+ // via the keyboard (Tab) or programmatically, which would otherwise leave an
373
+ // orphaned open menu. Clicking a result moves focus onto the option (it is
374
+ // tabIndex={-1}, so it lives inside the popover) and selection re-focuses the
375
+ // input, so this only closes on a genuine focus-out of the whole field.
376
+ const handleBlur = useCallback(e => {
377
+ if (!popover.isOpen) {
378
+ return;
379
+ }
380
+ const next = e.relatedTarget;
381
+ if (next) {
382
+ const anchorEl = anchorRef?.current ?? fallbackAnchorRef.current;
383
+ const popoverEl = document.getElementById(popover.id);
384
+ if (anchorEl?.contains(next) || popoverEl?.contains(next)) {
385
+ return;
386
+ }
387
+ }
388
+ popover.hide();
389
+ }, [popover, anchorRef]);
390
+
354
391
  // Keyboard navigation
355
392
  const handleKeyDown = useCallback(e => {
356
393
  externalOnKeyDown?.(e);
@@ -451,6 +488,7 @@ export const BaseTypeahead = function BaseTypeahead({
451
488
  });
452
489
  },
453
490
  onFocus: handleFocus,
491
+ onBlur: handleBlur,
454
492
  onKeyDown: handleKeyDown,
455
493
  placeholder: placeholder,
456
494
  disabled: isDisabled,