@astryxdesign/core 0.7.4-canary.10b832e → 0.7.4-canary.1a552f2
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/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +6 -11
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +7 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +16 -2
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.spec.md +1 -0
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +25 -2
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +7 -6
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +221 -1
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
- package/src/CommandPalette/CommandPalette.test.tsx +4 -2
- package/src/CommandPalette/CommandPalette.tsx +6 -11
- package/src/CommandPalette/CommandPaletteInput.test.tsx +3 -1
- package/src/CommandPalette/CommandPaletteInput.tsx +10 -1
- package/src/FileInput/FileInput.tsx +18 -2
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +217 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommandPalette.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalette.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,cAAc,CAAC;AAU/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,mBAAmB,CAClC,CAAC,SAAS,cAAc,GAAG,cAAc,CACzC,SAAQ,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IACtD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,0DAA0D;IAC1D,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;;OAIG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,OAAO,KAAK,SAAS,CAAC;IAEzD;;;OAGG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,SAAS,CAAC;IAE/B,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,8CAA8C;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAiID;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAAE,EACxE,GAAG,EACH,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,MAAM,EACN,UAAU,EACV,eAAe,EAAE,wBAAwB,EACzC,kBAAkB,EAAE,2BAA2B,EAC/C,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,KAAK,EAAE,cAAc,EACrB,KAAW,EACX,SAAe,EACf,GAAG,IAAI,EACR,EAAE,mBAAmB,CAAC,CAAC,CAAC,+
|
|
1
|
+
{"version":3,"file":"CommandPalette.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalette.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,cAAc,CAAC;AAU/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,mBAAmB,CAClC,CAAC,SAAS,cAAc,GAAG,cAAc,CACzC,SAAQ,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IACtD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,2CAA2C;IAC3C,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,0DAA0D;IAC1D,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;;OAIG;IACH,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAE9B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,OAAO,KAAK,SAAS,CAAC;IAEzD;;;OAGG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAE5B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,SAAS,CAAC;IAE/B,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,8CAA8C;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAiID;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAAE,EACxE,GAAG,EACH,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,MAAM,EACN,UAAU,EACV,eAAe,EAAE,wBAAwB,EACzC,kBAAkB,EAAE,2BAA2B,EAC/C,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,KAAK,EAAE,cAAc,EACrB,KAAW,EACX,SAAe,EACf,GAAG,IAAI,EACR,EAAE,mBAAmB,CAAC,CAAC,CAAC,+BAsYxB;yBAvZe,cAAc"}
|
|
@@ -205,11 +205,11 @@ export function CommandPalette({
|
|
|
205
205
|
const searchVersionRef = useRef(0);
|
|
206
206
|
const emptyStateRef = useRef(null);
|
|
207
207
|
|
|
208
|
-
// Announce search
|
|
208
|
+
// Announce search results to screen readers through the shared polite live
|
|
209
209
|
// region (comboboxes-7 announce path, mirroring Selector / BaseTypeahead).
|
|
210
|
-
// The
|
|
211
|
-
//
|
|
212
|
-
//
|
|
210
|
+
// The empty states are otherwise purely visual, so a screen-reader user
|
|
211
|
+
// typing a query would hear nothing when results disappear. The busy start
|
|
212
|
+
// is announced by CommandPaletteInput.
|
|
213
213
|
const announce = useAnnounce();
|
|
214
214
|
const value = controlledValue ?? internalValue;
|
|
215
215
|
const setValue = useCallback(newValue => {
|
|
@@ -278,13 +278,8 @@ export function CommandPalette({
|
|
|
278
278
|
startTransition(async () => {
|
|
279
279
|
const isBootstrap = query === '';
|
|
280
280
|
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// fast sources the result-count announcement below simply replaces
|
|
284
|
-
// this instead of stacking one "Loading" per keystroke.
|
|
285
|
-
if (!isBootstrap) {
|
|
286
|
-
announce(t('@astryx.commandPalette.loading'));
|
|
287
|
-
}
|
|
281
|
+
// The busy start ("Loading") is announced by CommandPaletteInput,
|
|
282
|
+
// from a region inside the palette's dialog (spec:AST-086 FR8, FR3).
|
|
288
283
|
|
|
289
284
|
// Client-filter previous results for instant narrowing while fetch is in flight
|
|
290
285
|
if (!isBootstrap && searchResults.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommandPaletteInput.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteInput.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAa/E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"CommandPaletteInput.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteInput.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAa/E,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAuE5C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CACpD,SAAS,CAAC,gBAAgB,CAAC,EAC3B,UAAU,CACX;IACC,iEAAiE;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAElC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,WAAW,EAAE,oBAAoB,EACjC,KAAK,EACL,YAAmB,EACnB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,OAAO,EACP,MAAM,EACN,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,wBAAwB,+BA2K1B;yBA3Le,mBAAmB"}
|
|
@@ -27,6 +27,7 @@ import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
|
|
|
27
27
|
import { getInteractionModality, useInteractionModalityTracking } from "../utils/interactionModality.js";
|
|
28
28
|
import { useTranslator } from "../i18n/index.js";
|
|
29
29
|
import { useMergedRefs } from "../hooks/useMergedRefs.js";
|
|
30
|
+
import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
|
|
30
31
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
31
32
|
const styles = {
|
|
32
33
|
wrapper: {
|
|
@@ -97,6 +98,10 @@ export function CommandPaletteInput({
|
|
|
97
98
|
const hasListbox = ctx ? ctx.selectableItems.length > 0 : true;
|
|
98
99
|
const dialogContext = useDialogContext();
|
|
99
100
|
const inputRef = useRef(null);
|
|
101
|
+
// The search Spinner is decorative; the input announces each busy start,
|
|
102
|
+
// including the load on opening or clearing the query. Inside the palette's
|
|
103
|
+
// native modal it does so from a region in the dialog (spec:AST-086 FR8, FR3).
|
|
104
|
+
useBusyAnnouncement(ctx?.isBusy === true, t('@astryx.commandPalette.loading'), inputRef);
|
|
100
105
|
const [isKeyboardFocus, setIsKeyboardFocus] = useState(false);
|
|
101
106
|
|
|
102
107
|
// Text-entry controls match :focus-visible after pointer focus in Chromium.
|
|
@@ -195,7 +200,8 @@ export function CommandPaletteInput({
|
|
|
195
200
|
className: "x78zum5 x6s0dn4 x2lah0s xv1l7n4 x1hc1fzr x19991ni xjd9b36 x5h36tt x4itv7f"
|
|
196
201
|
},
|
|
197
202
|
children: /*#__PURE__*/_jsx(Spinner, {
|
|
198
|
-
size: "sm"
|
|
203
|
+
size: "sm",
|
|
204
|
+
"aria-hidden": "true"
|
|
199
205
|
})
|
|
200
206
|
}), hasEndContent && endContent]
|
|
201
207
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileInput.d.ts","sourceRoot":"","sources":["../../src/FileInput/FileInput.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"FileInput.d.ts","sourceRoot":"","sources":["../../src/FileInput/FileInput.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAUlB,YAAY,EACV,WAAW,IAAI,eAAe,EAC9B,eAAe,IAAI,mBAAmB,GACvC,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAmP9C,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,mBAAmB,EACnB,UAAU,GAAG,cAAc,GAAG,OAAO,CACtC;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;OAEG;IACH,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,aAAqB,EACrB,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,UAAkB,EAClB,OAAO,EACP,QAAQ,EACR,UAAkB,EAClB,eAAe,EACf,UAAkB,EAClB,SAAiB,EACjB,MAAM,EAAE,UAAU,EAClB,aAA0B,EAC1B,WAAW,EACX,WAAW,EACX,IAAc,EACd,UAAkB,EAClB,YAAY,EACZ,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,EAAE,EAAE,MAAM,EACV,GAAG,SAAS,EACb,EAAE,cAAc,+BAmdhB;yBAlfe,SAAS"}
|
|
@@ -24,6 +24,7 @@ import { Icon } from "../Icon/index.js";
|
|
|
24
24
|
import { Spinner } from "../Spinner/index.js";
|
|
25
25
|
import { VisuallyHidden } from "../VisuallyHidden/index.js";
|
|
26
26
|
import { useAnnounce } from "../hooks/useAnnounce.js";
|
|
27
|
+
import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
|
|
27
28
|
import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
|
|
28
29
|
import { useClickableContainer } from "../hooks/useClickableContainer.js";
|
|
29
30
|
import { useTooltip } from "../Tooltip/index.js";
|
|
@@ -232,6 +233,16 @@ export function FileInput({
|
|
|
232
233
|
const [, startTransition] = useTransition();
|
|
233
234
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
234
235
|
const isBusy = isLoading || optimisticValue !== value;
|
|
236
|
+
// A pick is one user action, already announced as "file selected"; a busy
|
|
237
|
+
// period it starts (changeAction, or a caller setting isLoading from
|
|
238
|
+
// onChange) adds no second message (spec:AST-086 FR6). The mark lasts until
|
|
239
|
+
// the field is idle again. Other busy starts announce; the Spinner stays
|
|
240
|
+
// decorative (FR8).
|
|
241
|
+
const [isPickAnnounced, setIsPickAnnounced] = useState(false);
|
|
242
|
+
if (isPickAnnounced && !isBusy) {
|
|
243
|
+
setIsPickAnnounced(false);
|
|
244
|
+
}
|
|
245
|
+
useBusyAnnouncement(isBusy && !isPickAnnounced, t('@astryx.spinner.loading'), containerRef);
|
|
235
246
|
|
|
236
247
|
// Announce successful file selection to screen readers via a persistent
|
|
237
248
|
// live region (forms-17). Validation errors are announced (assertively) by
|
|
@@ -299,6 +310,7 @@ export function FileInput({
|
|
|
299
310
|
// status mounts, so only announce the attach here (do not
|
|
300
311
|
// double-announce errors).
|
|
301
312
|
if (errors.length === 0) {
|
|
313
|
+
setIsPickAnnounced(true);
|
|
302
314
|
announce(valid.length === 1 ? t('@astryx.fileInput.fileSelected', {
|
|
303
315
|
fileName: valid[0].name
|
|
304
316
|
}) : t('@astryx.fileInput.filesSelected', {
|
|
@@ -414,7 +426,8 @@ export function FileInput({
|
|
|
414
426
|
const renderDropzoneContent = () => {
|
|
415
427
|
if (isBusy) {
|
|
416
428
|
return /*#__PURE__*/_jsx(Spinner, {
|
|
417
|
-
size: "md"
|
|
429
|
+
size: "md",
|
|
430
|
+
"aria-hidden": "true"
|
|
418
431
|
});
|
|
419
432
|
}
|
|
420
433
|
if (hasFiles) {
|
|
@@ -450,7 +463,8 @@ export function FileInput({
|
|
|
450
463
|
},
|
|
451
464
|
children: fileNames ?? displayPlaceholder
|
|
452
465
|
}), /*#__PURE__*/_jsx(Spinner, {
|
|
453
|
-
size: "sm"
|
|
466
|
+
size: "sm",
|
|
467
|
+
"aria-hidden": "true"
|
|
454
468
|
})]
|
|
455
469
|
});
|
|
456
470
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.4-canary.
|
|
3
|
+
"version": "0.7.4-canary.1a552f2",
|
|
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",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.4-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.4-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.4-canary.1a552f2",
|
|
796
|
+
"@astryxdesign/cli": "0.7.4-canary.1a552f2",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -15,6 +15,7 @@ verified_by:
|
|
|
15
15
|
packages/core/src/BottomSheet/BottomSheet.test.tsx,
|
|
16
16
|
packages/core/src/BottomSheet/BottomSheetPanel.test.tsx,
|
|
17
17
|
packages/core/src/BottomSheet/BottomSheetKeyboard.test.tsx,
|
|
18
|
+
packages/core/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts,
|
|
18
19
|
packages/core/src/BottomSheet/__tests__/BottomSheetKeyboard.a11y.browser.spec.ts,
|
|
19
20
|
apps/storybook/stories/BottomSheet.stories.tsx,
|
|
20
21
|
scripts/check-knowledge.mjs,
|
|
@@ -148,15 +148,38 @@ describe('BottomSheetEdgeTint', () => {
|
|
|
148
148
|
expect(tints()).toHaveLength(1);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it('gives a modal switcher flow exactly one tint', () => {
|
|
152
|
-
render(
|
|
151
|
+
it('gives a modal switcher flow exactly one tint at rest, and one per presented dialog through a handoff', () => {
|
|
152
|
+
const {rerender} = render(
|
|
153
153
|
<BottomSheetSwitcher activeSheet="comment" onActiveSheetChange={() => {}}>
|
|
154
154
|
<BottomSheet sheetId="comment" label="Add a comment">
|
|
155
155
|
Comment
|
|
156
156
|
</BottomSheet>
|
|
157
|
+
<BottomSheet sheetId="confirmation" label="Confirmation">
|
|
158
|
+
Confirmation
|
|
159
|
+
</BottomSheet>
|
|
157
160
|
</BottomSheetSwitcher>,
|
|
158
161
|
);
|
|
159
162
|
expect(tints()).toHaveLength(1);
|
|
163
|
+
expect(tints()[0]?.closest('dialog')?.getAttribute('aria-label')).toBe(
|
|
164
|
+
'Add a comment',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
rerender(
|
|
168
|
+
<BottomSheetSwitcher
|
|
169
|
+
activeSheet="confirmation"
|
|
170
|
+
onActiveSheetChange={() => {}}>
|
|
171
|
+
<BottomSheet sheetId="comment" label="Add a comment">
|
|
172
|
+
Comment
|
|
173
|
+
</BottomSheet>
|
|
174
|
+
<BottomSheet sheetId="confirmation" label="Confirmation">
|
|
175
|
+
Confirmation
|
|
176
|
+
</BottomSheet>
|
|
177
|
+
</BottomSheetSwitcher>,
|
|
178
|
+
);
|
|
179
|
+
// Mid-handoff both dialogs are presented; each carries its own tint.
|
|
180
|
+
expect(
|
|
181
|
+
tints().map(tint => tint.closest('dialog')?.getAttribute('aria-label')),
|
|
182
|
+
).toEqual(['Add a comment', 'Confirmation']);
|
|
160
183
|
});
|
|
161
184
|
|
|
162
185
|
it('keeps the tint out of the accessibility tree and out of hit testing', async () => {
|
|
@@ -13,6 +13,7 @@ review_triggers: [public-api, behavior, layout, theming, accessibility]
|
|
|
13
13
|
verified_by:
|
|
14
14
|
[
|
|
15
15
|
packages/core/src/BottomSheet/BottomSheetSwitcher.test.tsx,
|
|
16
|
+
packages/core/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts,
|
|
16
17
|
apps/storybook/stories/BottomSheetSwitcher.stories.tsx,
|
|
17
18
|
scripts/check-knowledge.mjs,
|
|
18
19
|
]
|
|
@@ -192,12 +193,12 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
192
193
|
|
|
193
194
|
## Verification map
|
|
194
195
|
|
|
195
|
-
| Contract | Verification
|
|
196
|
-
| ---------------- |
|
|
197
|
-
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx`
|
|
198
|
-
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts`
|
|
199
|
-
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks
|
|
200
|
-
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests
|
|
196
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
197
|
+
| ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
|
198
|
+
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close | A child becomes interactive at the wrong time, a retained child disappears early, a second backdrop paints, or a sheet dialog's lifecycle breaks. | `audit:BottomSheetSwitcher/behavior` |
|
|
199
|
+
| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
|
|
200
|
+
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref across open/handoff/close/unmount, callback-ref cleanup, `id` once, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or duplicated, an object ref is null while a sheet is mounted or skips a cleanup, or an input overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
|
|
201
|
+
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
|
|
201
202
|
|
|
202
203
|
## Decision log
|
|
203
204
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* update these tests to match the public behavior.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {fireEvent, render, screen} from '@testing-library/react';
|
|
13
|
+
import {act, fireEvent, render, screen} from '@testing-library/react';
|
|
14
14
|
import {
|
|
15
15
|
StrictMode,
|
|
16
16
|
createRef,
|
|
@@ -22,6 +22,9 @@ import {
|
|
|
22
22
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
23
23
|
import {hasActiveFocusTrapEscape, useFocusTrap} from '../hooks';
|
|
24
24
|
import {useLayerDismissal} from '../Layer/useLayerDismissal';
|
|
25
|
+
import {Dialog} from '../Dialog';
|
|
26
|
+
import {hydrateRoot} from 'react-dom/client';
|
|
27
|
+
import {renderToString} from 'react-dom/server';
|
|
25
28
|
import {BottomSheet} from './BottomSheet';
|
|
26
29
|
import {BottomSheetSwitcher} from './BottomSheetSwitcher';
|
|
27
30
|
|
|
@@ -260,6 +263,12 @@ function RequiredConfirmFlow() {
|
|
|
260
263
|
);
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
/**
|
|
267
|
+
* A three-step flow that mounts closed, for the host-dialog contract: the
|
|
268
|
+
* switcher's ref, `id`, and `data-testid`. `onMountLayout` and
|
|
269
|
+
* `onMountPassive` run in the consumer's own mount effects, where a parent
|
|
270
|
+
* would first read the ref.
|
|
271
|
+
*/
|
|
263
272
|
function HostFlow({
|
|
264
273
|
dialogRef,
|
|
265
274
|
dialogProps,
|
|
@@ -344,6 +353,50 @@ function isScrollLockEngaged(): boolean {
|
|
|
344
353
|
return document.body.style.position === 'fixed';
|
|
345
354
|
}
|
|
346
355
|
|
|
356
|
+
/** A two-step flow whose second step opens a Core Dialog above itself. */
|
|
357
|
+
function DialogInStepFlow() {
|
|
358
|
+
const [activeSheet, setActiveSheet] = useState<string | null>(null);
|
|
359
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
360
|
+
return (
|
|
361
|
+
<>
|
|
362
|
+
<button type="button" onClick={() => setActiveSheet('details')}>
|
|
363
|
+
Start flow
|
|
364
|
+
</button>
|
|
365
|
+
<BottomSheetSwitcher
|
|
366
|
+
activeSheet={activeSheet}
|
|
367
|
+
onActiveSheetChange={setActiveSheet}>
|
|
368
|
+
<BottomSheet
|
|
369
|
+
sheetId="details"
|
|
370
|
+
label="Details"
|
|
371
|
+
data-testid="details-sheet">
|
|
372
|
+
<button type="button" onClick={() => setActiveSheet('confirm')}>
|
|
373
|
+
Continue
|
|
374
|
+
</button>
|
|
375
|
+
</BottomSheet>
|
|
376
|
+
<BottomSheet
|
|
377
|
+
sheetId="confirm"
|
|
378
|
+
label="Confirm"
|
|
379
|
+
data-testid="confirm-sheet">
|
|
380
|
+
<button type="button" onClick={() => setIsDialogOpen(true)}>
|
|
381
|
+
Open dialog
|
|
382
|
+
</button>
|
|
383
|
+
<Dialog
|
|
384
|
+
isOpen={isDialogOpen}
|
|
385
|
+
onOpenChange={setIsDialogOpen}
|
|
386
|
+
aria-label="Are you sure">
|
|
387
|
+
<button type="button" onClick={() => setIsDialogOpen(false)}>
|
|
388
|
+
Close dialog
|
|
389
|
+
</button>
|
|
390
|
+
</Dialog>
|
|
391
|
+
<button type="button" onClick={() => setActiveSheet(null)}>
|
|
392
|
+
Done
|
|
393
|
+
</button>
|
|
394
|
+
</BottomSheet>
|
|
395
|
+
</BottomSheetSwitcher>
|
|
396
|
+
</>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
347
400
|
/**
|
|
348
401
|
* Records every native dialog call with the dialog's accessible label, and
|
|
349
402
|
* returns focus the way Chromium and WebKit do: close() on a dialog presented
|
|
@@ -1352,6 +1405,42 @@ describe('BottomSheetSwitcher', () => {
|
|
|
1352
1405
|
expect(window.scrollTo).toHaveBeenCalledWith(0, 120);
|
|
1353
1406
|
});
|
|
1354
1407
|
|
|
1408
|
+
it('keeps the lock engaged under a Core Dialog opened from a step, and the Dialog paints above the step', () => {
|
|
1409
|
+
render(<DialogInStepFlow />);
|
|
1410
|
+
fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
|
|
1411
|
+
fireEvent.click(screen.getByRole('button', {name: 'Continue'}));
|
|
1412
|
+
finishSheetTransition(getSheetLayer('confirm-sheet'), 'transform');
|
|
1413
|
+
finishSheetTransition(getSheetLayer('details-sheet'), 'opacity');
|
|
1414
|
+
const confirmDialog = getSheetDialog('confirm-sheet');
|
|
1415
|
+
|
|
1416
|
+
fireEvent.click(screen.getByRole('button', {name: 'Open dialog'}));
|
|
1417
|
+
const dialog = screen.getByRole('dialog', {name: 'Are you sure'});
|
|
1418
|
+
expect(dialog).toHaveAttribute('open');
|
|
1419
|
+
// The Dialog was presented after the step's dialog, so the top layer
|
|
1420
|
+
// paints it above; the Dialog and the flow share the lock's counter,
|
|
1421
|
+
// so the step beneath keeps the pin when the Dialog gives its hold back.
|
|
1422
|
+
expect(HTMLDialogElement.prototype.showModal).toHaveBeenLastCalledWith();
|
|
1423
|
+
expect(
|
|
1424
|
+
vi.mocked(HTMLDialogElement.prototype.showModal).mock.contexts.at(-1),
|
|
1425
|
+
).toBe(dialog);
|
|
1426
|
+
expect(confirmDialog).toHaveAttribute('open');
|
|
1427
|
+
expect(isScrollLockEngaged()).toBe(true);
|
|
1428
|
+
|
|
1429
|
+
expect(window.scrollTo).not.toHaveBeenCalled();
|
|
1430
|
+
|
|
1431
|
+
// Escape reaches the Dialog first, not the step beneath it.
|
|
1432
|
+
fireEvent.keyDown(dialog, {key: 'Escape'});
|
|
1433
|
+
expect(dialog).not.toHaveAttribute('open');
|
|
1434
|
+
expect(confirmDialog).toHaveAttribute('open');
|
|
1435
|
+
expect(isScrollLockEngaged()).toBe(true);
|
|
1436
|
+
expect(window.scrollTo).not.toHaveBeenCalled();
|
|
1437
|
+
|
|
1438
|
+
fireEvent.click(screen.getByRole('button', {name: 'Done'}));
|
|
1439
|
+
finishSheetTransition(getSheetLayer('confirm-sheet'), 'transform');
|
|
1440
|
+
expect(isScrollLockEngaged()).toBe(false);
|
|
1441
|
+
expect(window.scrollTo).toHaveBeenCalledTimes(1);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1355
1444
|
it("resolves the switcher ref to the host sheet's dialog and never to null while a sheet is mounted, from the first commit on", () => {
|
|
1356
1445
|
const dialogRef = createRef<HTMLDialogElement>();
|
|
1357
1446
|
const seenAtMount: {layout?: Element | null; passive?: Element | null} =
|
|
@@ -1473,6 +1562,88 @@ describe('BottomSheetSwitcher', () => {
|
|
|
1473
1562
|
expect(withId()).toEqual([dialogs[1]]);
|
|
1474
1563
|
});
|
|
1475
1564
|
|
|
1565
|
+
it("renders the switcher id and data-testid once, on the active sheet's dialog, in the server markup of a flow that mounts open", () => {
|
|
1566
|
+
// The active sheet is known from props, so the host needs no commit:
|
|
1567
|
+
// server markup carries both attributes on that sheet's dialog, once.
|
|
1568
|
+
const markup = renderToString(
|
|
1569
|
+
<BottomSheetSwitcher
|
|
1570
|
+
id="checkout"
|
|
1571
|
+
data-testid="checkout-flow"
|
|
1572
|
+
activeSheet="confirm"
|
|
1573
|
+
onActiveSheetChange={() => {}}>
|
|
1574
|
+
<BottomSheet sheetId="details" label="Details">
|
|
1575
|
+
Content
|
|
1576
|
+
</BottomSheet>
|
|
1577
|
+
<BottomSheet sheetId="confirm" label="Confirm">
|
|
1578
|
+
Content
|
|
1579
|
+
</BottomSheet>
|
|
1580
|
+
</BottomSheetSwitcher>,
|
|
1581
|
+
);
|
|
1582
|
+
const dialogs = markup.match(/<dialog\b[^>]*>/g) ?? [];
|
|
1583
|
+
expect(dialogs).toHaveLength(2);
|
|
1584
|
+
expect(markup.match(/id="checkout"/g)).toHaveLength(1);
|
|
1585
|
+
expect(markup.match(/data-testid="checkout-flow"/g)).toHaveLength(1);
|
|
1586
|
+
const host = dialogs.find(tag => tag.includes('id="checkout"'));
|
|
1587
|
+
expect(host).toContain('aria-label="Confirm"');
|
|
1588
|
+
expect(host).toContain('data-testid="checkout-flow"');
|
|
1589
|
+
expect(
|
|
1590
|
+
dialogs.find(tag => tag.includes('aria-label="Details"')),
|
|
1591
|
+
).not.toContain('id=');
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
it('renders every sheet dialog closed in server markup, with the switcher id on no element until the first commit', () => {
|
|
1595
|
+
// Which sheet mounts first is known once its dialog registers during
|
|
1596
|
+
// the first commit; server rendering has no commit, so the id waits for
|
|
1597
|
+
// the client. What server markup must still get right: one closed
|
|
1598
|
+
// dialog per sheet, none `open`, and the id never duplicated.
|
|
1599
|
+
const markup = renderToString(
|
|
1600
|
+
<BottomSheetSwitcher
|
|
1601
|
+
id="checkout"
|
|
1602
|
+
data-testid="checkout-flow"
|
|
1603
|
+
activeSheet={null}
|
|
1604
|
+
onActiveSheetChange={() => {}}>
|
|
1605
|
+
<BottomSheet sheetId="details" label="Details">
|
|
1606
|
+
Content
|
|
1607
|
+
</BottomSheet>
|
|
1608
|
+
<BottomSheet sheetId="confirm" label="Confirm">
|
|
1609
|
+
Content
|
|
1610
|
+
</BottomSheet>
|
|
1611
|
+
</BottomSheetSwitcher>,
|
|
1612
|
+
);
|
|
1613
|
+
expect(markup.match(/<dialog\b/g)).toHaveLength(2);
|
|
1614
|
+
expect(markup).not.toMatch(/<dialog[^>]*\sopen/);
|
|
1615
|
+
expect(markup.match(/id="checkout"/g) ?? []).toHaveLength(0);
|
|
1616
|
+
|
|
1617
|
+
// Hydrating the same tree on the client: the first commit's synchronous
|
|
1618
|
+
// re-render puts the id on the first sheet's dialog, exactly once.
|
|
1619
|
+
const container = document.createElement('div');
|
|
1620
|
+
container.innerHTML = markup;
|
|
1621
|
+
document.body.appendChild(container);
|
|
1622
|
+
act(() => {
|
|
1623
|
+
hydrateRoot(
|
|
1624
|
+
container,
|
|
1625
|
+
<BottomSheetSwitcher
|
|
1626
|
+
id="checkout"
|
|
1627
|
+
data-testid="checkout-flow"
|
|
1628
|
+
activeSheet={null}
|
|
1629
|
+
onActiveSheetChange={() => {}}>
|
|
1630
|
+
<BottomSheet sheetId="details" label="Details">
|
|
1631
|
+
Content
|
|
1632
|
+
</BottomSheet>
|
|
1633
|
+
<BottomSheet sheetId="confirm" label="Confirm">
|
|
1634
|
+
Content
|
|
1635
|
+
</BottomSheet>
|
|
1636
|
+
</BottomSheetSwitcher>,
|
|
1637
|
+
);
|
|
1638
|
+
});
|
|
1639
|
+
const withId = container.querySelectorAll('[id="checkout"]');
|
|
1640
|
+
expect(withId).toHaveLength(1);
|
|
1641
|
+
expect(withId[0]).toBe(container.querySelector('dialog'));
|
|
1642
|
+
expect(withId[0]).toHaveAttribute('data-testid', 'checkout-flow');
|
|
1643
|
+
expect(withId[0]).toHaveAttribute('aria-label', 'Details');
|
|
1644
|
+
container.remove();
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1476
1647
|
it('orders a non-modal handoff inside the switcher-owned isolation surface, with no page-wide z-index on a sheet dialog', () => {
|
|
1477
1648
|
render(<HostFlow hasScrim={false} initialSheet="details" />);
|
|
1478
1649
|
const detailsDialog = getSheetDialog('details-sheet');
|
|
@@ -1544,6 +1715,55 @@ describe('BottomSheetSwitcher', () => {
|
|
|
1544
1715
|
expect(getSheetDialog('b')).toHaveAttribute('aria-label', 'Confirm');
|
|
1545
1716
|
});
|
|
1546
1717
|
|
|
1718
|
+
it('forces alertdialog on a required step and leaves the other sheet dialogs with no role when the switcher sets none', () => {
|
|
1719
|
+
render(
|
|
1720
|
+
<BottomSheetSwitcher
|
|
1721
|
+
activeSheet="details"
|
|
1722
|
+
onActiveSheetChange={() => {}}>
|
|
1723
|
+
<BottomSheet sheetId="details" label="Details" data-testid="a">
|
|
1724
|
+
Content
|
|
1725
|
+
</BottomSheet>
|
|
1726
|
+
<BottomSheet
|
|
1727
|
+
sheetId="danger"
|
|
1728
|
+
label="Danger"
|
|
1729
|
+
purpose="required"
|
|
1730
|
+
data-testid="c">
|
|
1731
|
+
Content
|
|
1732
|
+
</BottomSheet>
|
|
1733
|
+
</BottomSheetSwitcher>,
|
|
1734
|
+
);
|
|
1735
|
+
expect(getSheetDialog('a')).not.toHaveAttribute('role');
|
|
1736
|
+
expect(getSheetDialog('c')).toHaveAttribute('role', 'alertdialog');
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
it('keeps the scrim takeover pin under prefers-reduced-motion: the reduced duration reads the pinned variable', () => {
|
|
1740
|
+
render(<HostFlow initialSheet="details" />);
|
|
1741
|
+
// jsdom evaluates neither @media nor ::backdrop; read the compiled
|
|
1742
|
+
// rules the dialog's scrim class produced. The reduced-motion rule for
|
|
1743
|
+
// the backdrop's transition-duration must defer to the takeover pin
|
|
1744
|
+
// (`--_sheet-scrim-duration: 0s` for one style flush); a literal 0.01s
|
|
1745
|
+
// there would win the cascade and paint a frame with no scrim.
|
|
1746
|
+
const rules: string[] = [];
|
|
1747
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
1748
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
1749
|
+
rules.push(rule.cssText);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const reducedBackdropDurations = rules.filter(
|
|
1753
|
+
text =>
|
|
1754
|
+
text.includes('prefers-reduced-motion: reduce') &&
|
|
1755
|
+
text.includes('::backdrop') &&
|
|
1756
|
+
text.includes('transition-duration'),
|
|
1757
|
+
);
|
|
1758
|
+
expect(reducedBackdropDurations.length).toBeGreaterThan(0);
|
|
1759
|
+
for (const text of reducedBackdropDurations) {
|
|
1760
|
+
expect(text).toMatch(
|
|
1761
|
+
/transition-duration:\s*var\(--_sheet-scrim-duration,\s*0?\.01s\)/,
|
|
1762
|
+
);
|
|
1763
|
+
expect(text).not.toMatch(/transition-duration:\s*0?\.01s/);
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1547
1767
|
it('paints one scrim through a handoff and hands it over without a fade', () => {
|
|
1548
1768
|
render(<Flow />);
|
|
1549
1769
|
fireEvent.click(screen.getByRole('button', {name: 'Start flow'}));
|