@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.
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +11 -5
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +20 -4
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +11 -11
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +4 -2
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +12 -4
- package/dist/ContextMenu/ContextMenu.d.ts +6 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +58 -7
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +17 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +9 -4
- package/dist/DateTimeInput/DateTimeInput.d.ts +7 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +15 -3
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +12 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +25 -4
- package/dist/Field/Field.d.ts +19 -2
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +4 -0
- package/dist/Field/FieldLabel.d.ts +21 -2
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +37 -22
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +27 -6
- package/dist/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/InputGroup/InputGroup.js +4 -1
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +33 -1
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +36 -5
- package/dist/Layout/padding.stylex.d.ts +51 -0
- package/dist/Layout/padding.stylex.d.ts.map +1 -1
- package/dist/Layout/padding.stylex.js +63 -0
- package/dist/Link/Link.d.ts +7 -1
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +12 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +35 -6
- package/dist/MultiSelector/hooks.d.ts +12 -1
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +14 -1
- package/dist/NavMenu/NavHeadingMenu.d.ts.map +1 -1
- package/dist/NavMenu/NavHeadingMenu.js +17 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +7 -1
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +8 -0
- package/dist/ProgressBar/ProgressBar.js +1 -1
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +76 -3
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +6 -2
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +15 -8
- package/dist/Selector/hooks.d.ts +8 -1
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +14 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +8 -2
- package/dist/Stack/Stack.d.ts +28 -1
- package/dist/Stack/Stack.d.ts.map +1 -1
- package/dist/Stack/Stack.js +17 -1
- package/dist/Stack/StackItem.d.ts +12 -1
- package/dist/Stack/StackItem.d.ts.map +1 -1
- package/dist/Stack/StackItem.js +8 -1
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +6 -0
- package/dist/TabList/TabList.d.ts +15 -4
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +58 -4
- package/dist/TabList/TabListContext.d.ts +5 -0
- package/dist/TabList/TabListContext.d.ts.map +1 -1
- package/dist/TabList/TabListContext.js +5 -0
- package/dist/TabList/TabMenu.js +2 -0
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +14 -0
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +132 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +7 -3
- package/dist/TreeList/TreeList.d.ts +1 -1
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +78 -2
- package/dist/TreeList/TreeListItem.d.ts +10 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +44 -10
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +16 -0
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +1 -0
- package/dist/TreeList/treeListItem.markers.stylex.js +22 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +41 -3
- package/dist/astryx.css +3 -0
- package/dist/astryx.umd.js +47 -47
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +10 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -2
- package/dist/hooks/useAnnounce.d.ts +40 -0
- package/dist/hooks/useAnnounce.d.ts.map +1 -0
- package/dist/hooks/useAnnounce.js +142 -0
- package/dist/hooks/useClickableContainer.d.ts +16 -1
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +16 -4
- package/dist/hooks/useFocusTrap.d.ts +18 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +65 -0
- package/dist/hooks/useListFocus.d.ts +79 -8
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +262 -48
- package/dist/hooks/useLongPress.d.ts +21 -0
- package/dist/hooks/useLongPress.d.ts.map +1 -0
- package/dist/hooks/useLongPress.js +89 -0
- package/dist/hooks/useTreeFocus.d.ts +133 -0
- package/dist/hooks/useTreeFocus.d.ts.map +1 -0
- package/dist/hooks/useTreeFocus.js +274 -0
- package/dist/hooks/useTypeahead.d.ts +63 -0
- package/dist/hooks/useTypeahead.d.ts.map +1 -0
- package/dist/hooks/useTypeahead.js +117 -0
- package/package.json +1 -1
- package/src/Avatar/Avatar.test.tsx +37 -0
- package/src/Avatar/Avatar.tsx +10 -5
- package/src/Breadcrumbs/BreadcrumbItem.tsx +20 -5
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +31 -10
- package/src/Calendar/Calendar.test.tsx +25 -0
- package/src/Calendar/Calendar.tsx +10 -12
- package/src/CheckboxInput/CheckboxInput.test.tsx +14 -25
- package/src/CheckboxInput/CheckboxInput.tsx +4 -2
- package/src/CheckboxList/CheckboxList.test.tsx +30 -14
- package/src/CheckboxList/CheckboxList.tsx +18 -3
- package/src/ContextMenu/ContextMenu.doc.mjs +6 -0
- package/src/ContextMenu/ContextMenu.test.tsx +115 -1
- package/src/ContextMenu/ContextMenu.tsx +75 -4
- package/src/DateInput/DateInput.test.tsx +59 -0
- package/src/DateInput/DateInput.tsx +24 -7
- package/src/DateRangeInput/DateRangeInput.test.tsx +69 -18
- package/src/DateRangeInput/DateRangeInput.tsx +7 -3
- package/src/DateTimeInput/DateTimeInput.doc.mjs +7 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +99 -6
- package/src/DateTimeInput/DateTimeInput.tsx +32 -3
- package/src/Dialog/Dialog.tsx +12 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +26 -4
- package/src/DropdownMenu/DropdownMenu.tsx +36 -2
- package/src/Field/Field.test.tsx +32 -0
- package/src/Field/Field.tsx +23 -5
- package/src/Field/FieldLabel.tsx +53 -17
- package/src/FileInput/FileInput.test.tsx +104 -49
- package/src/FileInput/FileInput.tsx +27 -3
- package/src/InputGroup/InputGroup.test.tsx +21 -63
- package/src/InputGroup/InputGroup.tsx +4 -1
- package/src/Kbd/Kbd.test.tsx +22 -4
- package/src/Kbd/Kbd.tsx +35 -3
- package/src/Layer/useLayer.tsx +52 -5
- package/src/Layout/padding.stylex.ts +52 -0
- package/src/Link/Link.doc.mjs +14 -0
- package/src/Link/Link.test.tsx +32 -0
- package/src/Link/Link.tsx +23 -1
- package/src/MultiSelector/MultiSelector.test.tsx +124 -2
- package/src/MultiSelector/MultiSelector.tsx +50 -9
- package/src/MultiSelector/hooks.ts +27 -0
- package/src/NavMenu/NavHeadingMenu.test.tsx +32 -1
- package/src/NavMenu/NavHeadingMenu.tsx +30 -2
- package/src/NumberInput/NumberInput.test.tsx +72 -38
- package/src/NumberInput/NumberInput.tsx +12 -1
- package/src/Pagination/Pagination.test.tsx +58 -5
- package/src/Pagination/Pagination.tsx +12 -0
- package/src/ProgressBar/ProgressBar.test.tsx +34 -26
- package/src/ProgressBar/ProgressBar.tsx +1 -1
- package/src/RadioList/RadioList.test.tsx +129 -14
- package/src/RadioList/RadioList.tsx +103 -8
- package/src/SegmentedControl/SegmentedControl.test.tsx +35 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +4 -1
- package/src/Selector/Selector.test.tsx +53 -0
- package/src/Selector/Selector.tsx +15 -8
- package/src/Selector/hooks.ts +23 -0
- package/src/Skeleton/Skeleton.test.tsx +25 -0
- package/src/Skeleton/Skeleton.tsx +11 -1
- package/src/Stack/Stack.doc.mjs +57 -0
- package/src/Stack/Stack.test.tsx +55 -5
- package/src/Stack/Stack.tsx +53 -1
- package/src/Stack/StackItem.test.tsx +23 -0
- package/src/Stack/StackItem.tsx +20 -0
- package/src/TabList/Tab.tsx +6 -0
- package/src/TabList/TabList.doc.mjs +6 -0
- package/src/TabList/TabList.test.tsx +167 -0
- package/src/TabList/TabList.tsx +72 -9
- package/src/TabList/TabListContext.ts +7 -3
- package/src/TabList/TabMenu.tsx +3 -3
- package/src/Toast/Toast.tsx +14 -0
- package/src/Toast/ToastViewport.test.tsx +204 -0
- package/src/Toast/ToastViewport.tsx +167 -21
- package/src/Toolbar/Toolbar.test.tsx +45 -0
- package/src/Toolbar/Toolbar.tsx +5 -2
- package/src/TreeList/TreeList.test.tsx +273 -0
- package/src/TreeList/TreeList.tsx +84 -1
- package/src/TreeList/TreeListItem.tsx +42 -2
- package/src/TreeList/treeListItem.markers.stylex.ts +20 -0
- package/src/Typeahead/BaseTypeahead.tsx +48 -2
- package/src/Typeahead/Typeahead.test.tsx +125 -0
- package/src/hooks/index.ts +21 -2
- package/src/hooks/useAnnounce.test.tsx +101 -0
- package/src/hooks/useAnnounce.ts +168 -0
- package/src/hooks/useClickableContainer.ts +16 -4
- package/src/hooks/useFocusTrap.test.tsx +67 -3
- package/src/hooks/useFocusTrap.ts +75 -0
- package/src/hooks/useListFocus.doc.mjs +49 -10
- package/src/hooks/useListFocus.test.tsx +269 -0
- package/src/hooks/useListFocus.ts +339 -54
- package/src/hooks/useLongPress.test.tsx +134 -0
- package/src/hooks/useLongPress.ts +119 -0
- package/src/hooks/useTreeFocus.doc.mjs +126 -0
- package/src/hooks/useTreeFocus.test.tsx +214 -0
- package/src/hooks/useTreeFocus.ts +464 -0
- package/src/hooks/useTypeahead.test.tsx +109 -0
- 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,+
|
|
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"}
|
package/dist/Toolbar/Toolbar.js
CHANGED
|
@@ -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
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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,
|