@astryxdesign/core 0.7.4-canary.3f6ea7d → 0.7.4-canary.88ec018

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ChatMessageList.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatMessageList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAGhF,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,oBAAqB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACrE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAEzB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,OAAoB,EACpB,GAAG,EACH,KAAgB,EAChB,WAAmB,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,oBAAoB,+BAgGtB;yBA9Ge,eAAe"}
1
+ {"version":3,"file":"ChatMessageList.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatMessageList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAGhF,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD,MAAM,WAAW,oBAAqB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACrE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAEzB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,OAAoB,EACpB,GAAG,EACH,KAAgB,EAChB,WAAmB,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,oBAAoB,+BAoGtB;yBAlHe,eAAe"}
@@ -31,6 +31,8 @@ import { ChatListContext, useChatLayoutContext } from "./ChatContext.js";
31
31
  import { isRenderable, mergeProps } from "../utils/index.js";
32
32
  import { Spinner } from "../Spinner/index.js";
33
33
  import { themeProps } from "../utils/themeProps.js";
34
+ import { useTranslator } from "../i18n/index.js";
35
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
34
36
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
35
37
  // =============================================================================
36
38
  // Styles
@@ -161,6 +163,10 @@ export function ChatMessageList({
161
163
  const sentinelRef = useRef(null);
162
164
  const innerRef = useRef(null);
163
165
  const [isLoadingTop, startTransition] = useTransition();
166
+ const t = useTranslator();
167
+ // The top loading Spinner sits inside the log and is silent; the list
168
+ // announces its loading through the channel (spec:AST-086 FR4, FR8).
169
+ useBusyAnnouncement(isLoadingTop, t('@astryx.spinner.loading'), innerRef);
164
170
 
165
171
  // Register inner content element with the layout for height observation
166
172
  useEffect(() => {
@@ -219,7 +225,8 @@ export function ChatMessageList({
219
225
  className: "x78zum5 xl56j7k x8o8v82"
220
226
  },
221
227
  children: /*#__PURE__*/_jsx(Spinner, {
222
- size: "md"
228
+ size: "md",
229
+ "aria-hidden": "true"
223
230
  })
224
231
  }), align === 'bottom' && /*#__PURE__*/_jsx("div", {
225
232
  ...{
@@ -1 +1 @@
1
- {"version":3,"file":"CheckboxInput.d.ts","sourceRoot":"","sources":["../../src/CheckboxInput/CheckboxInput.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAS3C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAG9C,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAmHhD,QAAA,MAAM,iBAAiB;;;;;;;;;EASrB,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,iBAAiB,CAAC;AAE/D,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;IACrE,wDAAwD;IACxD,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,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACxE;;OAEG;IACH,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,OAAO,EAChB,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,KAC7B,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,KAAK,EAAE,OAAO,GAAG,eAAe,CAAC;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACpD;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACnD;;OAEG;IACH,SAAS,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACjC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAOD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,KAAK,EACL,UAAkB,EAClB,QAAQ,EACR,eAAe,EACf,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,IAAW,EACX,OAAO,EACP,MAAM,EACN,SAAS,EACT,MAAM,EACN,KAAK,EACL,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,kBAAkB,EAAE,mBAAmB,EACvC,GAAG,IAAI,EACR,EAAE,kBAAkB,+BA0NpB;yBArPe,aAAa"}
1
+ {"version":3,"file":"CheckboxInput.d.ts","sourceRoot":"","sources":["../../src/CheckboxInput/CheckboxInput.tsx"],"names":[],"mappings":"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"}
@@ -27,7 +27,9 @@ import { mergeProps, rtlStyles } from "../utils/index.js";
27
27
  import "../Indicator/indicator.markers.stylex.js";
28
28
  import { indicatorScope } from "../Indicator/indicator.markers.stylex.js";
29
29
  import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
30
+ import { useTranslator } from "../i18n/index.js";
30
31
  import { useIndicatorFocusRing } from "../hooks/useIndicatorFocusRing.js";
32
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
31
33
  import { usePressFeedback } from "../hooks/usePressFeedback.js";
32
34
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
33
35
  import { useIndicator } from "../Indicator/index.js";
@@ -195,6 +197,10 @@ export function CheckboxInput({
195
197
  // `opacity: 0`, and only the indicator's own element can shape the outline
196
198
  // to match it. See useIndicatorFocusRing.
197
199
  const indicatorRef = useRef(null);
200
+ const t = useTranslator();
201
+ // The busy Spinner sits in the decorative, aria-hidden indicator, so this
202
+ // control announces its busy start itself (spec:AST-086 FR8).
203
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), indicatorRef);
198
204
  const {
199
205
  focusProps
200
206
  } = useIndicatorFocusRing(indicatorRef);
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenuSubMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenuSubMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAE,EASZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAuB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAwC5D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA6N5C,MAAM,MAAM,+BAA+B,GACzC,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAErC,UAAU,4BAA6B,SAAQ,IAAI,CACjD,SAAS,EACT,MAAM,GAAG,UAAU,CACpB;IACC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,8CAA8C;IAC9C,KAAK,EAAE,SAAS,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,8CAA8C;IAC9C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,+BAA+B,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAyB,SAAQ,4BAA4B;IAC5E;;;;;;;;OAQG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AA6CD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,SAAS,CAw4BX;yBA14Be,mBAAmB"}
1
+ {"version":3,"file":"DropdownMenuSubMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenuSubMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAE,EASZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAuB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAyC5D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA6N5C,MAAM,MAAM,+BAA+B,GACzC,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAErC,UAAU,4BAA6B,SAAQ,IAAI,CACjD,SAAS,EACT,MAAM,GAAG,UAAU,CACpB;IACC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,8CAA8C;IAC9C,KAAK,EAAE,SAAS,CAAC;IACjB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,8CAA8C;IAC9C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,+BAA+B,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAyB,SAAQ,4BAA4B;IAC5E;;;;;;;;OAQG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AA6CD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,SAAS,CAg5BX;yBAl5Be,mBAAmB"}
@@ -61,6 +61,7 @@ import { Item } from "../Item/index.js";
61
61
  import { useLayer } from "../Layer/useLayer.js";
62
62
  import { layerAnimations } from "../Layer/layerAnimations.stylex.js";
63
63
  import { useListFocus } from "../hooks/useListFocus.js";
64
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
64
65
  import { useMenuHover } from "../hooks/useMenuHover.js";
65
66
  import { isMenuPressActivation, MENU_PRESS_SURFACE_ATTR, useMenuPress } from "../hooks/useMenuPress.js";
66
67
  import { useTypeahead } from "../hooks/useTypeahead.js";
@@ -461,6 +462,10 @@ export function DropdownMenuSubMenu(props) {
461
462
  // closed from elsewhere is cancelled instead of firing into the hidden
462
463
  // menu (#6893).
463
464
  const isRootMenuOpen = useDropdownMenuRootOpen();
465
+ // The loading Spinner on the trigger is decorative; the submenu announces
466
+ // its busy start instead (spec:AST-086 FR8), once its level is shown: a
467
+ // closed menu stays mounted but hidden, and announces nothing.
468
+ useBusyAnnouncement(hasSpinner && isRootMenuOpen, t('@astryx.spinner.loading'), triggerRef);
464
469
  const {
465
470
  triggerProps,
466
471
  contentProps,
@@ -761,7 +766,8 @@ export function DropdownMenuSubMenu(props) {
761
766
  className: "x78zum5 x6s0dn4"
762
767
  },
763
768
  children: /*#__PURE__*/_jsx(Spinner, {
764
- size: "sm"
769
+ size: "sm",
770
+ "aria-hidden": "true"
765
771
  })
766
772
  }) : /*#__PURE__*/_jsx("span", {
767
773
  ...{
@@ -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;AASlB,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,+BAochB;yBAnee,SAAS"}
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Thumbnail.d.ts","sourceRoot":"","sources":["../../src/Thumbnail/Thumbnail.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAQ5C,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC/D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA6LD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,EACxB,GAAG,EACH,GAAG,EACH,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAiB,EACjB,UAAkB,EAClB,YAAsB,EACtB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,KAAK,EACT,EAAE,cAAc,+BAmJhB;yBAlKe,SAAS"}
1
+ {"version":3,"file":"Thumbnail.d.ts","sourceRoot":"","sources":["../../src/Thumbnail/Thumbnail.tsx"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAQ5C,MAAM,WAAW,cAAe,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC/D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACzC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC;;;OAGG;IACH;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA6LD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,EACxB,GAAG,EACH,GAAG,EACH,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAiB,EACjB,UAAkB,EAClB,YAAsB,EACtB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,KAAK,EACT,EAAE,cAAc,+BA2JhB;yBA1Ke,SAAS"}
@@ -24,7 +24,7 @@
24
24
  * - /apps/storybook/stories/Thumbnail.stories.tsx
25
25
  * - /packages/cli/assets/templates/blocks/components/Thumbnail/ (showcase blocks)
26
26
  */
27
- import { useState } from 'react';
27
+ import { useRef, useState } from 'react';
28
28
  import * as stylex from '@stylexjs/stylex';
29
29
  import "../theme/tokens.stylex.js";
30
30
  import { colorVars, radiusVars, spacingVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
@@ -34,6 +34,7 @@ import { Skeleton } from "../Skeleton/index.js";
34
34
  import { Spinner } from "../Spinner/index.js";
35
35
  import { Tooltip } from "../Tooltip/Tooltip.js";
36
36
  import { useDevWarning } from "../hooks/useDevWarning.js";
37
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
37
38
  import { useContainerReveal } from "../hooks/useContainerReveal.js";
38
39
  import { mergeProps } from "../utils/index.js";
39
40
  import { themeProps } from "../utils/themeProps.js";
@@ -175,6 +176,10 @@ export function Thumbnail({
175
176
  const showSkeleton = isLoading && !hasSrc;
176
177
  const showImage = hasSrc && !showSkeleton && !hasError;
177
178
  const showUploadOverlay = isLoading && hasSrc;
179
+ const uploadOverlayRef = useRef(null);
180
+ // The upload Spinner is decorative; the thumbnail announces its busy start
181
+ // instead (spec:AST-086 FR8).
182
+ useBusyAnnouncement(showUploadOverlay, t('@astryx.spinner.loading'), uploadOverlayRef);
178
183
  const showPlaceholder = !isLoading && !hasSrc || hasError;
179
184
  const isInteractive = onClick != null && !isDisabled && !isLoading;
180
185
  const hasRemove = onRemove != null && !isDisabled;
@@ -273,12 +278,14 @@ export function Thumbnail({
273
278
  className: "x10l6tqk x10a8y8t x1pjcqnp x1er153n x47corl"
274
279
  }
275
280
  }), showUploadOverlay && /*#__PURE__*/_jsx("div", {
281
+ ref: uploadOverlayRef,
276
282
  ...{
277
283
  className: "x10l6tqk x10a8y8t x78zum5 x6s0dn4 xl56j7k x1u4601x x1pjcqnp x1vjfegm x14ju556"
278
284
  },
279
285
  children: /*#__PURE__*/_jsx(Spinner, {
280
286
  size: "sm",
281
- shade: "onMedia"
287
+ shade: "onMedia",
288
+ "aria-hidden": "true"
282
289
  })
283
290
  }), removeButtonEl]
284
291
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.4-canary.3f6ea7d",
3
+ "version": "0.7.4-canary.88ec018",
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.3f6ea7d",
796
- "@astryxdesign/cli": "0.7.4-canary.3f6ea7d",
795
+ "@astryxdesign/a11y-spec": "0.7.4-canary.88ec018",
796
+ "@astryxdesign/cli": "0.7.4-canary.88ec018",
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 | Representative states | Mutation or failure expectation | Audit section |
196
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
197
- | 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` |
198
- | FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` (implementation gap: not yet in the tree) | 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` |
199
- | 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` |
200
- | 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` |
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'}));