@astryxdesign/core 0.7.0 → 0.7.1-canary.30b6e1b
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/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +12 -2
- package/dist/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +22 -8
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -26
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +10 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +12 -3
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
- package/dist/utils/focusReturn.d.ts +12 -8
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +44 -3
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.test.tsx +114 -0
- package/src/BottomSheet/BottomSheet.tsx +12 -2
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
- package/src/CodeBlock/CodeBlock.spec.md +54 -44
- package/src/ContextMenu/ContextMenu.spec.md +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +36 -3
- package/src/ContextMenu/ContextMenu.tsx +27 -11
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
- package/src/Dialog/Dialog.tsx +2 -30
- package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
- package/src/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
- package/src/Markdown/Markdown.spec.md +37 -23
- package/src/PowerSearch/PowerSearch.test.tsx +45 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
- package/src/Typeahead/BaseTypeahead.tsx +17 -3
- package/src/utils/focusReturn.test.ts +79 -2
- package/src/utils/focusReturn.ts +48 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file BottomSheet.tsx
|
|
3
|
-
* @input Uses React, StyleX, core hooks, shared layer dismissal, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
3
|
+
* @input Uses React, StyleX, core hooks, shared layer dismissal, the shared interaction modality, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
4
4
|
* @output Exports BottomSheet component and BottomSheetProps
|
|
5
5
|
* @position Public BottomSheet router plus private standalone/switcher hosts
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../src/BottomSheet/BottomSheet.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAmBhD,YAAY,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AAChF,OAAO,KAAK,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,MAAM,oBAAoB,CAAC;AA0EhF,UAAU,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,qDAAqD;IACrD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IAEd,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB,wGAAwG;IACxG,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAC;IAE7C;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAEjD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,UAAU,0BAA2B,SAAQ,sBAAsB;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,aAAa,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,UAAU,wBAAyB,SAAQ,sBAAsB;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,MAAM,gBAAgB,GAC1B,0BAA0B,GAAG,wBAAwB,CAAC;AAsYxD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BA6BlD;yBA7Be,WAAW"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file BottomSheet.tsx
|
|
7
|
-
* @input Uses React, StyleX, core hooks, shared layer dismissal, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
7
|
+
* @input Uses React, StyleX, core hooks, shared layer dismissal, the shared interaction modality, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
8
8
|
* @output Exports BottomSheet component and BottomSheetProps
|
|
9
9
|
* @position Public BottomSheet router plus private standalone/switcher hosts
|
|
10
10
|
*
|
|
@@ -34,6 +34,8 @@ import { colorVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
|
|
|
34
34
|
import { useDevWarning, useScrollLock } from "../hooks/index.js";
|
|
35
35
|
import { LayerDepthProvider } from "../Layer/LayerDepthContext.js";
|
|
36
36
|
import { useLayerDismissal } from "../Layer/useLayerDismissal.js";
|
|
37
|
+
import { focusForLastInput } from "../utils/focusReturn.js";
|
|
38
|
+
import { useInteractionModalityTracking } from "../utils/interactionModality.js";
|
|
37
39
|
import { BottomSheetPanel } from "./BottomSheetPanel.js";
|
|
38
40
|
import { BottomSheetEdgeTint } from "./BottomSheetEdgeTint.js";
|
|
39
41
|
import { BottomSheetSwitcherContext } from "./BottomSheetSwitcherContext.js";
|
|
@@ -168,9 +170,17 @@ function StandaloneBottomSheet({
|
|
|
168
170
|
dialog.close();
|
|
169
171
|
}
|
|
170
172
|
setIsPresented(false);
|
|
171
|
-
|
|
173
|
+
// The returned focus shows the focus-visible ring only after keyboard
|
|
174
|
+
// input (`architecture:interaction-modality` INV1). The native close
|
|
175
|
+
// may already have restored focus to the target with a ring; the helper
|
|
176
|
+
// clears it first.
|
|
177
|
+
const returnTarget = finalFocusRef?.current ?? triggerRef.current;
|
|
178
|
+
if (returnTarget != null) {
|
|
179
|
+
focusForLastInput(returnTarget);
|
|
180
|
+
}
|
|
172
181
|
triggerRef.current = null;
|
|
173
182
|
}, [finalFocusRef, isOpen]);
|
|
183
|
+
useInteractionModalityTracking();
|
|
174
184
|
useScrollLock(shouldPresent && hasScrim);
|
|
175
185
|
useDevWarning('BottomSheet', 'requires a non-empty `label` for an accessible name; the open sheet ' + 'has no built-in heading to derive one from.', isOpen && !label);
|
|
176
186
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @position Core implementation; consumed by index.ts
|
|
7
7
|
*
|
|
8
8
|
* Right-click context menu positioned at the cursor. The cursor point is
|
|
9
|
-
* captured as an offset *inside the trigger* and materialized as a
|
|
9
|
+
* captured as an offset *inside the trigger* and materialized as a 1px
|
|
10
10
|
* anchor element, so the menu is positioned relative to the trigger's context
|
|
11
11
|
* (via CSS anchor positioning) rather than the viewport. It therefore follows
|
|
12
12
|
* the content on scroll and auto-flips at viewport edges, while still appearing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../src/ContextMenu/ContextMenu.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KASN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAkCrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAGjD,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../src/ContextMenu/ContextMenu.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KASN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAkCrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAGjD,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,kCAAkC,CAAC;AA6G1C,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AAEvD,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAMnD,UAAU,oBAAqB,SAAQ,SAAS;IAC9C,oDAAoD;IACpD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,EAAE,SAAS,CAAC;IACpB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,wCAAwC;IACxC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,oBAAqB,SAAQ,oBAAoB;IACzD,gDAAgD;IAChD,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,SAAS,CAAC;CACzB;AAED,UAAU,wBAAyB,SAAQ,oBAAoB;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+CAA+C;IAC/C,WAAW,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,wBAAwB,CAAC;AAM/E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,IAAW,EACX,KAAK,EAAE,cAAc,EACrB,UAAkB,EAClB,YAAY,EACZ,YAAwB,EACxB,SAAiB,EACjB,GAAG,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,aAAa,EACb,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBA6clB;yBA7de,WAAW"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @position Core implementation; consumed by index.ts
|
|
10
10
|
*
|
|
11
11
|
* Right-click context menu positioned at the cursor. The cursor point is
|
|
12
|
-
* captured as an offset *inside the trigger* and materialized as a
|
|
12
|
+
* captured as an offset *inside the trigger* and materialized as a 1px
|
|
13
13
|
* anchor element, so the menu is positioned relative to the trigger's context
|
|
14
14
|
* (via CSS anchor positioning) rather than the viewport. It therefore follows
|
|
15
15
|
* the content on scroll and auto-flips at viewport edges, while still appearing
|
|
@@ -134,6 +134,17 @@ const styles = {
|
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Keeps the 1px cursor anchor inside the trigger along one axis. A point in
|
|
139
|
+
* the trigger's last pixel (Shift+F10 opens on its bottom edge) steps back by
|
|
140
|
+
* that pixel: past the edge the anchor would add scrollable overflow, and a
|
|
141
|
+
* scroller whose edge meets the trigger's would clip it and hide the menu.
|
|
142
|
+
* Any other point, inside the trigger or not, stays where it is.
|
|
143
|
+
*/
|
|
144
|
+
function keepAnchorInside(offset, size) {
|
|
145
|
+
return offset > size - 1 && offset <= size ? Math.max(size - 1, 0) : offset;
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
// =============================================================================
|
|
138
149
|
// Types
|
|
139
150
|
// =============================================================================
|
|
@@ -203,7 +214,7 @@ export function ContextMenu({
|
|
|
203
214
|
const menuContent = menuContentProp;
|
|
204
215
|
const menuId = useId();
|
|
205
216
|
// Cursor point in the trigger's local coordinate space (offset from the
|
|
206
|
-
// trigger's top-left, in-flow). Stored here and written to the
|
|
217
|
+
// trigger's top-left, in-flow). Stored here and written to the cursor
|
|
207
218
|
// anchor element so the menu is positioned relative to the trigger context
|
|
208
219
|
// — it scrolls with the content instead of sitting at a fixed viewport point.
|
|
209
220
|
const positionRef = useRef({
|
|
@@ -380,20 +391,23 @@ export function ContextMenu({
|
|
|
380
391
|
listNavKeyDown(e);
|
|
381
392
|
}, [listNavKeyDown, closeMenu, typeahead, ownsEvent]);
|
|
382
393
|
|
|
383
|
-
// Place the
|
|
394
|
+
// Place the 1px cursor anchor at a point in the trigger's local
|
|
384
395
|
// coordinate space and open the menu. Positioning the anchor inside the
|
|
385
396
|
// trigger (rather than storing viewport coordinates on the menu itself) is
|
|
386
397
|
// what makes the menu context-relative: it scrolls with the content and the
|
|
387
398
|
// browser auto-flips it against the viewport edges via CSS anchor positioning.
|
|
388
399
|
const openAtLocalPoint = useCallback((localX, localY, focusEl) => {
|
|
400
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
401
|
+
const x = rect ? keepAnchorInside(localX, rect.width) : localX;
|
|
402
|
+
const y = rect ? keepAnchorInside(localY, rect.height) : localY;
|
|
389
403
|
positionRef.current = {
|
|
390
|
-
x
|
|
391
|
-
y
|
|
404
|
+
x,
|
|
405
|
+
y
|
|
392
406
|
};
|
|
393
407
|
const anchorEl = cursorAnchorRef.current;
|
|
394
408
|
if (anchorEl) {
|
|
395
|
-
anchorEl.style.left = `${
|
|
396
|
-
anchorEl.style.top = `${
|
|
409
|
+
anchorEl.style.left = `${x}px`;
|
|
410
|
+
anchorEl.style.top = `${y}px`;
|
|
397
411
|
}
|
|
398
412
|
// Remember the element focused before opening so we can restore it on
|
|
399
413
|
// close (Escape or outside-click), instead of dropping focus to <body>.
|
|
@@ -534,7 +548,7 @@ export function ContextMenu({
|
|
|
534
548
|
ref: useMergedRefs(cursorAnchorRef, layer.ref),
|
|
535
549
|
"aria-hidden": "true",
|
|
536
550
|
...mergeProps({
|
|
537
|
-
className: "x10l6tqk
|
|
551
|
+
className: "x10l6tqk x1i1rx1s xjm9jq1 x47corl"
|
|
538
552
|
}, {
|
|
539
553
|
style: {
|
|
540
554
|
left: `${positionRef.current.x}px`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2B5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2B5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAyB9C;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEzD,4EAA4E;AAC5E,UAAU,mBAAmB;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAe,SAAQ,mBAAmB;IACzD,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAuBD,2FAA2F;AAC3F,eAAO,MAAM,uCAAuC;;;;;;;;;;;CAW1C,CAAC;AAoJX;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;EAUpE;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC/D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;OAOG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAEpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,QAAgB,EAChB,YAAY,EACZ,KAAW,EACX,SAAmB,EACnB,QAAQ,EACR,OAAoB,EACpB,OAAgB,EAChB,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,sCAiUb;yBAjVe,MAAM"}
|
package/dist/Dialog/Dialog.js
CHANGED
|
@@ -45,38 +45,15 @@ import { devWarn } from "../utils/devWarning.js";
|
|
|
45
45
|
import { DialogContext } from "./DialogContext.js";
|
|
46
46
|
import { themeProps } from "../utils/themeProps.js";
|
|
47
47
|
import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
|
|
48
|
-
import {
|
|
48
|
+
import { useInteractionModalityTracking } from "../utils/interactionModality.js";
|
|
49
|
+
import { focusForLastInput } from "../utils/focusReturn.js";
|
|
49
50
|
import { useMergedRefs } from "../hooks/useMergedRefs.js";
|
|
50
|
-
/**
|
|
51
|
-
* Move focus programmatically so its ring follows the last input. Dialog does
|
|
52
|
-
* not render the element that opened it or the consumer's autofocus target, so
|
|
53
|
-
* the browser's focus option is the one way to reach them.
|
|
54
|
-
*/
|
|
55
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
56
|
-
function focusForLastInput(element) {
|
|
57
|
-
// A text-entry field paints its own focus treatment to show where typing
|
|
58
|
-
// goes, and an extra blur there can run validation or mark it touched.
|
|
59
|
-
const isTextEntry = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable;
|
|
60
|
-
if (isTextEntry || getInteractionModality() !== 'pointer') {
|
|
61
|
-
element.focus();
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
// Focusing the element that already has focus does nothing, so a ring the
|
|
65
|
-
// browser already drew would stay. WebKit draws one when showModal() or its
|
|
66
|
-
// native close moves focus itself, so clear that focus first.
|
|
67
|
-
if (element.matches(':focus-visible')) {
|
|
68
|
-
element.blur();
|
|
69
|
-
}
|
|
70
|
-
element.focus({
|
|
71
|
-
focusVisible: false
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
51
|
/**
|
|
76
52
|
* Calculate a directional translate offset for dialog entry animation.
|
|
77
53
|
* Returns a normalized vector from the trigger element toward the viewport
|
|
78
54
|
* center, scaled to the given distance.
|
|
79
55
|
*/
|
|
56
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
80
57
|
function getDialogDirection(triggerEl, distance = 16) {
|
|
81
58
|
const rect = triggerEl.getBoundingClientRect();
|
|
82
59
|
const dx = rect.left + rect.width / 2 - window.innerWidth / 2;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAS,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAKnD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"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"}
|
|
@@ -48,6 +48,7 @@ import { useListFocus } from "../hooks/useListFocus.js";
|
|
|
48
48
|
import { useMenuPress } from "../hooks/useMenuPress.js";
|
|
49
49
|
import { useTypeahead } from "../hooks/useTypeahead.js";
|
|
50
50
|
import { useFocusReturnVisibility } from "../hooks/useFocusReturnVisibility.js";
|
|
51
|
+
import { focusForLastInput } from "../utils/focusReturn.js";
|
|
51
52
|
import { useMenuOverflow } from "./useMenuOverflow.js";
|
|
52
53
|
import { MenuRegionsContext } from "./menuRegionsContext.js";
|
|
53
54
|
import { getFocusableElements } from "../hooks/useFocusTrap.js";
|
|
@@ -654,7 +655,12 @@ function DropdownMenuPopover({
|
|
|
654
655
|
return;
|
|
655
656
|
}
|
|
656
657
|
prepareFocusReturn();
|
|
657
|
-
trigger
|
|
658
|
+
// After pointer input the trigger takes focus without the focus-visible
|
|
659
|
+
// ring (`component:DropdownMenu/DEC-1`). The style the helper above adds
|
|
660
|
+
// reaches only the design system's own Button; the browser's focus option
|
|
661
|
+
// also reaches a control the caller renders, which the browser would
|
|
662
|
+
// otherwise ring (Chromium after a mouse pick, WebKit after a touch).
|
|
663
|
+
focusForLastInput(trigger, {
|
|
658
664
|
preventScroll: true
|
|
659
665
|
});
|
|
660
666
|
}, [isControlled, onOpenChange, prepareFocusReturn]);
|
|
@@ -1072,8 +1078,9 @@ function DropdownMenuPopover({
|
|
|
1072
1078
|
onPointerDown: menuPress.triggerProps.onPointerDown,
|
|
1073
1079
|
onContextMenu: menuPress.triggerProps.onContextMenu,
|
|
1074
1080
|
onKeyDown: handleButtonKeyDown,
|
|
1075
|
-
// The
|
|
1076
|
-
// the
|
|
1081
|
+
// The caller's control paints its own ring, and the menu's focus return
|
|
1082
|
+
// keeps the browser from drawing one after pointer input (see
|
|
1083
|
+
// handleLayerHide), so its focus needs no bookkeeping here.
|
|
1077
1084
|
onFocus: () => {},
|
|
1078
1085
|
'aria-haspopup': 'menu',
|
|
1079
1086
|
'aria-expanded': isOpen,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file BaseTypeahead.tsx
|
|
3
3
|
* @input Uses React, StyleX, usePopover, TypeaheadItem
|
|
4
|
-
* @output Exports BaseTypeahead
|
|
4
|
+
* @output Exports BaseTypeahead; focus bootstraps a replaced search source
|
|
5
5
|
* @position Core implementation; used by Typeahead and Tokenizer
|
|
6
6
|
*
|
|
7
7
|
* Pure combobox engine: input, search, keyboard navigation, dropdown.
|
|
@@ -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;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAW1D,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,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;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;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;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;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,
|
|
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;AAIf,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AA2BnD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,cAAc,EAAE,YAAY,EAAC,MAAM,SAAS,CAAC;AAW1D,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,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;OASG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;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;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAEtD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;IAE3B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;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;AAoKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,EAo1BrB,CAAC,CAAC,SAAS,cAAc,EAC5B,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KACzB,KAAK,CAAC,YAAY,CAAC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file BaseTypeahead.tsx
|
|
7
7
|
* @input Uses React, StyleX, usePopover, TypeaheadItem
|
|
8
|
-
* @output Exports BaseTypeahead
|
|
8
|
+
* @output Exports BaseTypeahead; focus bootstraps a replaced search source
|
|
9
9
|
* @position Core implementation; used by Typeahead and Tokenizer
|
|
10
10
|
*
|
|
11
11
|
* Pure combobox engine: input, search, keyboard navigation, dropdown.
|
|
@@ -326,6 +326,10 @@ export const BaseTypeahead = function BaseTypeahead({
|
|
|
326
326
|
// in the closure are stale (a selection cleared them) and shouldn't
|
|
327
327
|
// be re-shown.
|
|
328
328
|
const resultsGenRef = useRef(0);
|
|
329
|
+
// The generation at which the current searchSource took over. Results
|
|
330
|
+
// populated before it (resultsGenRef below this) came from a replaced
|
|
331
|
+
// source: they can never be re-shown, so focus bootstraps the new source.
|
|
332
|
+
const sourceGenRef = useRef(0);
|
|
329
333
|
|
|
330
334
|
// Results still arriving from a replaced source must not land in the new
|
|
331
335
|
// one's menu.
|
|
@@ -333,7 +337,7 @@ export const BaseTypeahead = function BaseTypeahead({
|
|
|
333
337
|
if (prevSearchSourceRef.current !== searchSource) {
|
|
334
338
|
prevSearchSourceRef.current.cancel?.();
|
|
335
339
|
prevSearchSourceRef.current = searchSource;
|
|
336
|
-
searchGenRef.current
|
|
340
|
+
sourceGenRef.current = ++searchGenRef.current;
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
// Layer for dropdown
|
|
@@ -551,11 +555,16 @@ export const BaseTypeahead = function BaseTypeahead({
|
|
|
551
555
|
// Shared by handleFocus and handleClick: bootstrap entries if none are
|
|
552
556
|
// loaded yet, or re-show cached results that haven't been invalidated by
|
|
553
557
|
// a selection since (comparing the two generation refs catches that).
|
|
558
|
+
// Results cached from a replaced search source count as none, so focusing
|
|
559
|
+
// an empty field bootstraps the new source instead of opening nothing.
|
|
560
|
+
// Results a selection invalidated still keep the menu closed when the
|
|
561
|
+
// selection returns focus to the input.
|
|
554
562
|
const openIfEligible = useCallback(() => {
|
|
555
563
|
if (isDisabled) {
|
|
556
564
|
return;
|
|
557
565
|
}
|
|
558
|
-
|
|
566
|
+
const hasResultsFromCurrentSource = results.length > 0 && resultsGenRef.current >= sourceGenRef.current;
|
|
567
|
+
if (hasEntriesOnFocus && !hasResultsFromCurrentSource && query.length === 0) {
|
|
559
568
|
void performBootstrap();
|
|
560
569
|
} else if (results.length > 0 && (query.length > 0 || hasEntriesOnFocus) && resultsGenRef.current === searchGenRef.current) {
|
|
561
570
|
showLayer();
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
* identical — neither hook fires during server rendering — but React only
|
|
12
12
|
* warns about useLayoutEffect.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
export declare const useIsomorphicLayoutEffect: typeof
|
|
14
|
+
import { useEffect } from 'react';
|
|
15
|
+
export declare const useIsomorphicLayoutEffect: typeof useEffect;
|
|
16
16
|
//# sourceMappingURL=useIsomorphicLayoutEffect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIsomorphicLayoutEffect.d.ts","sourceRoot":"","sources":["../../src/hooks/useIsomorphicLayoutEffect.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"useIsomorphicLayoutEffect.d.ts","sourceRoot":"","sources":["../../src/hooks/useIsomorphicLayoutEffect.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,SAAS,EAAkB,MAAM,OAAO,CAAC;AAEjD,eAAO,MAAM,yBAAyB,kBACuB,CAAC"}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file focusReturn.ts
|
|
3
|
-
* @input The document's current active element.
|
|
4
|
-
* @output Whether focus is currently "detached" — on nothing in particular.
|
|
5
|
-
* @position Utility; used by overlay triggers (e.g. DateInput, DateTimeInput)
|
|
6
|
-
* that return focus to their control when the overlay closes, but only when
|
|
7
|
-
* the dismiss did not already move focus somewhere the user chose.
|
|
8
|
-
*/
|
|
9
1
|
/**
|
|
10
2
|
* True when focus rests on nothing in particular — no active element, or the
|
|
11
3
|
* document body / root element.
|
|
@@ -21,4 +13,16 @@
|
|
|
21
13
|
* when the trigger should reclaim it.
|
|
22
14
|
*/
|
|
23
15
|
export declare function isFocusDetached(doc?: Document): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Move focus programmatically so its ring follows the last input
|
|
18
|
+
* (`architecture:interaction-modality` INV1). After pointer input the browser's
|
|
19
|
+
* focus option keeps the element from matching `:focus-visible`, which is the
|
|
20
|
+
* one way to reach an element the overlay does not render: the consumer's
|
|
21
|
+
* autofocus target, the control that opened a Dialog, or a menu trigger the
|
|
22
|
+
* caller renders. Pass `options` (for example `preventScroll`) as to `focus()`.
|
|
23
|
+
*
|
|
24
|
+
* Make sure modality tracking is active (`useInteractionModalityTracking()`)
|
|
25
|
+
* in the component that calls this.
|
|
26
|
+
*/
|
|
27
|
+
export declare function focusForLastInput(element: HTMLElement, options?: FocusOptions): void;
|
|
24
28
|
//# sourceMappingURL=focusReturn.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focusReturn.d.ts","sourceRoot":"","sources":["../../src/utils/focusReturn.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"focusReturn.d.ts","sourceRoot":"","sources":["../../src/utils/focusReturn.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,QAAmB,GAAG,OAAO,CAKjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,YAAY,GACrB,IAAI,CAwBN"}
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file focusReturn.ts
|
|
5
|
-
* @input The document's current active element
|
|
6
|
-
*
|
|
5
|
+
* @input The document's current active element and the shared interaction
|
|
6
|
+
* modality.
|
|
7
|
+
* @output Whether focus is currently "detached" — on nothing in particular —
|
|
8
|
+
* and a focus move whose ring follows the last input.
|
|
7
9
|
* @position Utility; used by overlay triggers (e.g. DateInput, DateTimeInput)
|
|
8
10
|
* that return focus to their control when the overlay closes, but only when
|
|
9
|
-
* the dismiss did not already move focus somewhere the user chose
|
|
11
|
+
* the dismiss did not already move focus somewhere the user chose, and by
|
|
12
|
+
* overlays (Dialog, DropdownMenu) that move focus programmatically.
|
|
10
13
|
*/
|
|
11
14
|
|
|
15
|
+
import { getInteractionModality } from "./interactionModality.js";
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* True when focus rests on nothing in particular — no active element, or the
|
|
14
19
|
* document body / root element.
|
|
@@ -26,4 +31,40 @@
|
|
|
26
31
|
export function isFocusDetached(doc = document) {
|
|
27
32
|
const active = doc.activeElement;
|
|
28
33
|
return active == null || active === doc.body || active === doc.documentElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Move focus programmatically so its ring follows the last input
|
|
38
|
+
* (`architecture:interaction-modality` INV1). After pointer input the browser's
|
|
39
|
+
* focus option keeps the element from matching `:focus-visible`, which is the
|
|
40
|
+
* one way to reach an element the overlay does not render: the consumer's
|
|
41
|
+
* autofocus target, the control that opened a Dialog, or a menu trigger the
|
|
42
|
+
* caller renders. Pass `options` (for example `preventScroll`) as to `focus()`.
|
|
43
|
+
*
|
|
44
|
+
* Make sure modality tracking is active (`useInteractionModalityTracking()`)
|
|
45
|
+
* in the component that calls this.
|
|
46
|
+
*/
|
|
47
|
+
export function focusForLastInput(element, options) {
|
|
48
|
+
// A text-entry field paints its own focus treatment to show where typing
|
|
49
|
+
// goes, and an extra blur there can run validation or mark it touched.
|
|
50
|
+
const isTextEntry = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable;
|
|
51
|
+
if (isTextEntry || getInteractionModality() !== 'pointer') {
|
|
52
|
+
if (options === undefined) {
|
|
53
|
+
element.focus();
|
|
54
|
+
} else {
|
|
55
|
+
element.focus(options);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Focusing the element that already has focus does nothing, so a ring the
|
|
60
|
+
// browser already drew would stay. WebKit draws one when its own focus
|
|
61
|
+
// restoration (a dialog's close, a popover's hide) moves focus itself, so
|
|
62
|
+
// clear that focus first.
|
|
63
|
+
if (element.matches(':focus-visible')) {
|
|
64
|
+
element.blur();
|
|
65
|
+
}
|
|
66
|
+
element.focus({
|
|
67
|
+
...options,
|
|
68
|
+
focusVisible: false
|
|
69
|
+
});
|
|
29
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1-canary.30b6e1b",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -787,6 +787,8 @@
|
|
|
787
787
|
"react-dom": ">=19.0.0"
|
|
788
788
|
},
|
|
789
789
|
"devDependencies": {
|
|
790
|
+
"@astryxdesign/a11y-spec": "0.7.1-canary.30b6e1b",
|
|
791
|
+
"@astryxdesign/cli": "0.7.1-canary.30b6e1b",
|
|
790
792
|
"@babel/cli": "^7.29.7",
|
|
791
793
|
"@babel/core": "^7.29.7",
|
|
792
794
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -797,9 +799,7 @@
|
|
|
797
799
|
"@testing-library/jest-dom": "^6.6.0",
|
|
798
800
|
"@testing-library/react": "^16.3.2",
|
|
799
801
|
"@types/babel__core": "^7.20.5",
|
|
800
|
-
"tailwindcss": "^4.3.2"
|
|
801
|
-
"@astryxdesign/a11y-spec": "0.0.0",
|
|
802
|
-
"@astryxdesign/cli": "0.7.0"
|
|
802
|
+
"tailwindcss": "^4.3.2"
|
|
803
803
|
},
|
|
804
804
|
"dependencies": {
|
|
805
805
|
"intl-messageformat": "^11.2.9"
|
|
@@ -14,6 +14,7 @@ import {act, render, screen, fireEvent} from '@testing-library/react';
|
|
|
14
14
|
import {createRef, useState} from 'react';
|
|
15
15
|
import {BottomSheet} from './BottomSheet';
|
|
16
16
|
import {BottomSheetSwitcher} from './BottomSheetSwitcher';
|
|
17
|
+
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
17
18
|
|
|
18
19
|
// A sheet has no stops unless its host asks for them, so the tests that
|
|
19
20
|
// exercise detents pass their own. These three — a 14% peek, a half-height
|
|
@@ -2351,6 +2352,119 @@ describe('BottomSheet', () => {
|
|
|
2351
2352
|
|
|
2352
2353
|
expect(document.activeElement).toBe(finalFocusRef.current);
|
|
2353
2354
|
});
|
|
2355
|
+
|
|
2356
|
+
// The returned focus shows the focus-visible ring only after keyboard
|
|
2357
|
+
// input (`architecture:interaction-modality` INV1). A plain focus() leaves
|
|
2358
|
+
// that to the browser, which rings it after a pointer close: WebKit after
|
|
2359
|
+
// a tap, and Chromium when the sheet was opened from the keyboard.
|
|
2360
|
+
describe('ring on the returned focus', () => {
|
|
2361
|
+
beforeEach(() => {
|
|
2362
|
+
__resetInteractionModalityForTest();
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
afterEach(() => {
|
|
2366
|
+
vi.restoreAllMocks();
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
function openThenClose(closeWith: 'pointer' | 'keyboard') {
|
|
2370
|
+
render(<Harness />);
|
|
2371
|
+
const opener = screen.getByRole('button', {name: 'Open sheet'});
|
|
2372
|
+
opener.focus();
|
|
2373
|
+
fireEvent.click(opener);
|
|
2374
|
+
const done = screen.getByRole('button', {name: 'Done'});
|
|
2375
|
+
if (closeWith === 'pointer') {
|
|
2376
|
+
fireEvent.pointerDown(done);
|
|
2377
|
+
} else {
|
|
2378
|
+
fireEvent.keyDown(done, {key: 'Enter'});
|
|
2379
|
+
}
|
|
2380
|
+
fireEvent.click(done);
|
|
2381
|
+
return opener;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
it('asks the browser for no ring after a pointer close', () => {
|
|
2385
|
+
const opener = openThenClose('pointer');
|
|
2386
|
+
const focus = vi.spyOn(opener, 'focus');
|
|
2387
|
+
finishSheetExit();
|
|
2388
|
+
|
|
2389
|
+
expect(opener).toHaveFocus();
|
|
2390
|
+
expect(focus.mock.calls).toEqual([[{focusVisible: false}]]);
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
it('leaves the ring to the browser after a keyboard close', () => {
|
|
2394
|
+
const opener = openThenClose('keyboard');
|
|
2395
|
+
const focus = vi.spyOn(opener, 'focus');
|
|
2396
|
+
finishSheetExit();
|
|
2397
|
+
|
|
2398
|
+
expect(opener).toHaveFocus();
|
|
2399
|
+
expect(focus.mock.calls).toEqual([[]]);
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
it('clears a ring the native close already drew before refocusing', () => {
|
|
2403
|
+
const opener = openThenClose('pointer');
|
|
2404
|
+
// The browser's own dialog close restores focus to the opener, with a
|
|
2405
|
+
// ring, before the sheet moves focus, and focusing the focused element
|
|
2406
|
+
// changes nothing.
|
|
2407
|
+
HTMLDialogElement.prototype.close = vi.fn(function (
|
|
2408
|
+
this: HTMLDialogElement,
|
|
2409
|
+
) {
|
|
2410
|
+
this.removeAttribute('open');
|
|
2411
|
+
opener.focus();
|
|
2412
|
+
});
|
|
2413
|
+
const matches = HTMLElement.prototype.matches;
|
|
2414
|
+
vi.spyOn(HTMLElement.prototype, 'matches').mockImplementation(function (
|
|
2415
|
+
this: HTMLElement,
|
|
2416
|
+
selector: string,
|
|
2417
|
+
) {
|
|
2418
|
+
return selector === ':focus-visible'
|
|
2419
|
+
? this === opener
|
|
2420
|
+
: matches.call(this, selector);
|
|
2421
|
+
});
|
|
2422
|
+
const blur = vi.spyOn(opener, 'blur');
|
|
2423
|
+
const focus = vi.spyOn(opener, 'focus');
|
|
2424
|
+
finishSheetExit();
|
|
2425
|
+
|
|
2426
|
+
expect(blur).toHaveBeenCalledTimes(1);
|
|
2427
|
+
expect(focus.mock.calls.at(-1)).toEqual([{focusVisible: false}]);
|
|
2428
|
+
expect(blur.mock.invocationCallOrder[0]).toBeLessThan(
|
|
2429
|
+
focus.mock.invocationCallOrder.at(-1) ?? 0,
|
|
2430
|
+
);
|
|
2431
|
+
expect(opener).toHaveFocus();
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
it('asks for no ring on an explicit final focus target too', () => {
|
|
2435
|
+
const finalFocusRef = createRef<HTMLButtonElement>();
|
|
2436
|
+
function ExplicitFocusHarness() {
|
|
2437
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
2438
|
+
return (
|
|
2439
|
+
<>
|
|
2440
|
+
<button ref={finalFocusRef} type="button">
|
|
2441
|
+
Adaptive trigger
|
|
2442
|
+
</button>
|
|
2443
|
+
<BottomSheet
|
|
2444
|
+
isOpen={isOpen}
|
|
2445
|
+
onOpenChange={setIsOpen}
|
|
2446
|
+
finalFocusRef={finalFocusRef}
|
|
2447
|
+
label="Filters">
|
|
2448
|
+
<button type="button" onClick={() => setIsOpen(false)}>
|
|
2449
|
+
Done
|
|
2450
|
+
</button>
|
|
2451
|
+
</BottomSheet>
|
|
2452
|
+
</>
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
render(<ExplicitFocusHarness />);
|
|
2457
|
+
const done = screen.getByRole('button', {name: 'Done'});
|
|
2458
|
+
fireEvent.pointerDown(done);
|
|
2459
|
+
fireEvent.click(done);
|
|
2460
|
+
const target = finalFocusRef.current!;
|
|
2461
|
+
const focus = vi.spyOn(target, 'focus');
|
|
2462
|
+
finishSheetExit();
|
|
2463
|
+
|
|
2464
|
+
expect(target).toHaveFocus();
|
|
2465
|
+
expect(focus.mock.calls).toEqual([[{focusVisible: false}]]);
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2354
2468
|
});
|
|
2355
2469
|
|
|
2356
2470
|
describe('initial focus', () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file BottomSheet.tsx
|
|
7
|
-
* @input Uses React, StyleX, core hooks, shared layer dismissal, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
7
|
+
* @input Uses React, StyleX, core hooks, shared layer dismissal, the shared interaction modality, named BottomSheetPanel, BottomSheetSwitcherContext
|
|
8
8
|
* @output Exports BottomSheet component and BottomSheetProps
|
|
9
9
|
* @position Public BottomSheet router plus private standalone/switcher hosts
|
|
10
10
|
*
|
|
@@ -46,6 +46,8 @@ import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
|
|
|
46
46
|
import {useDevWarning, useScrollLock} from '../hooks';
|
|
47
47
|
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
|
|
48
48
|
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
49
|
+
import {focusForLastInput} from '../utils/focusReturn';
|
|
50
|
+
import {useInteractionModalityTracking} from '../utils/interactionModality';
|
|
49
51
|
import {
|
|
50
52
|
BottomSheetPanel,
|
|
51
53
|
type BottomSheetPanelMotion,
|
|
@@ -320,12 +322,20 @@ function StandaloneBottomSheet({
|
|
|
320
322
|
dialog.close();
|
|
321
323
|
}
|
|
322
324
|
setIsPresented(false);
|
|
323
|
-
|
|
325
|
+
// The returned focus shows the focus-visible ring only after keyboard
|
|
326
|
+
// input (`architecture:interaction-modality` INV1). The native close
|
|
327
|
+
// may already have restored focus to the target with a ring; the helper
|
|
328
|
+
// clears it first.
|
|
329
|
+
const returnTarget = finalFocusRef?.current ?? triggerRef.current;
|
|
330
|
+
if (returnTarget != null) {
|
|
331
|
+
focusForLastInput(returnTarget);
|
|
332
|
+
}
|
|
324
333
|
triggerRef.current = null;
|
|
325
334
|
},
|
|
326
335
|
[finalFocusRef, isOpen],
|
|
327
336
|
);
|
|
328
337
|
|
|
338
|
+
useInteractionModalityTracking();
|
|
329
339
|
useScrollLock(shouldPresent && hasScrim);
|
|
330
340
|
useDevWarning(
|
|
331
341
|
'BottomSheet',
|