@astryxdesign/core 0.7.3-canary.948e75d → 0.7.3-canary.97dda3c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/Button/Button.d.ts.map +1 -1
  2. package/dist/Button/Button.js +15 -2
  3. package/dist/Chat/ChatComposer.d.ts.map +1 -1
  4. package/dist/Chat/ChatComposer.js +25 -2
  5. package/dist/Chat/ChatSystemMessage.d.ts +4 -0
  6. package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
  7. package/dist/Chat/ChatSystemMessage.js +4 -2
  8. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  9. package/dist/ContextMenu/ContextMenu.js +12 -0
  10. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +9 -5
  11. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  12. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +2 -0
  13. package/dist/hooks/useMenuPress.d.ts +2 -2
  14. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  15. package/dist/hooks/useMenuPress.js +5 -1
  16. package/package.json +3 -3
  17. package/src/Button/Button.test.tsx +13 -2
  18. package/src/Button/Button.tsx +23 -2
  19. package/src/Button/__tests__/Button.a11y.states.ts +4 -3
  20. package/src/Carousel/Carousel.spec.md +146 -0
  21. package/src/Chat/ChatComposer.spec.md +26 -26
  22. package/src/Chat/ChatComposer.test.tsx +58 -2
  23. package/src/Chat/ChatComposer.tsx +25 -1
  24. package/src/Chat/ChatSystemMessage.doc.mjs +1 -1
  25. package/src/Chat/ChatSystemMessage.test.tsx +25 -3
  26. package/src/Chat/ChatSystemMessage.tsx +6 -4
  27. package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +8 -9
  28. package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +7 -6
  29. package/src/ContextMenu/ContextMenu.test.tsx +34 -0
  30. package/src/ContextMenu/ContextMenu.tsx +13 -1
  31. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +62 -7
  32. package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +190 -0
  33. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -4
  34. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +93 -0
  35. package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +327 -0
  36. package/src/FieldStatus/FieldStatus.spec.md +4 -4
  37. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -18
  38. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +3 -18
  39. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +1 -2
  40. package/src/Markdown/Markdown.helpers.perf.test.ts +25 -19
  41. package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +11 -11
  42. package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +4 -3
  43. package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +50 -0
  44. package/src/hooks/useMenuPress.test.tsx +27 -1
  45. package/src/hooks/useMenuPress.ts +12 -3
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/Button/Button.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/Button/Button.tsx"],"names":[],"mappings":"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"}
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Last synced props: label, variant, size, isDisabled, isLoading, isInterruptible, clickAction, icon, isIconOnly, width, children, tooltip, endContent, href, as, target, rel
19
19
  */
20
- import { useRef, useTransition } from 'react';
20
+ import { useEffect, useRef, useState, useTransition } from 'react';
21
21
  import * as stylex from '@stylexjs/stylex';
22
22
  import { useTooltip } from "../Tooltip/useTooltip.js";
23
23
  import "../theme/tokens.stylex.js";
@@ -357,6 +357,19 @@ export function Button({
357
357
  // re-click can land and interrupt the in-flight action with a fresh one.
358
358
  const actionInFlightRef = useRef(false);
359
359
  const isLoadingState = isLoading || isPending;
360
+ // The loading live region must exist empty before it receives "Loading":
361
+ // many screen readers skip a live region that mounts already holding its
362
+ // text, which is what a Button first rendered loading did (spec:AST-086
363
+ // FR3). Only that case holds the text back, until the frame after the
364
+ // region mounts; a Button mounted idle never re-renders for this.
365
+ const [isLiveRegionReady, setIsLiveRegionReady] = useState(() => !isLoadingState);
366
+ useEffect(() => {
367
+ if (isLiveRegionReady) {
368
+ return;
369
+ }
370
+ const frame = requestAnimationFrame(() => setIsLiveRegionReady(true));
371
+ return () => cancelAnimationFrame(frame);
372
+ }, [isLiveRegionReady]);
360
373
  // Delay the spinner reveal for action-driven loading (clickAction's own
361
374
  // transition) so a fast action that settles within the delay does not flash
362
375
  // a spinner. Interruptible loading is delayed too, so rapid re-clicks settle
@@ -506,7 +519,7 @@ export function Button({
506
519
  }), /*#__PURE__*/_jsx(VisuallyHidden, {
507
520
  role: "status",
508
521
  "aria-live": "polite",
509
- children: isLoadingState ? t('@astryx.button.loading') : ''
522
+ children: isLoadingState && isLiveRegionReady ? t('@astryx.button.loading') : ''
510
523
  })]
511
524
  });
512
525
 
@@ -1 +1 @@
1
- {"version":3,"file":"ChatComposer.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatComposer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EAGf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA+B5C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAEtE,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,GAAG,UAAU,CACxB;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sBAAsB;IACtB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAI3B,6GAA6G;IAC7G,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yHAAyH;IACzH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,yGAAyG;IACzG,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,gHAAgH;IAChH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gHAAgH;IAChH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,gDAAgD;IAChD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,iEAAiE;IACjE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,oDAAoD;IACpD,cAAc,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;CACnC;AAkND;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,qBAkNpD;yBAlNe,YAAY"}
1
+ {"version":3,"file":"ChatComposer.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatComposer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EAGf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAgC5C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAEtE,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,GAAG,UAAU,CACxB;IACC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,+CAA+C;IAC/C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sBAAsB;IACtB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAI3B,6GAA6G;IAC7G,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yHAAyH;IACzH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,yGAAyG;IACzG,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,gHAAgH;IAChH,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gHAAgH;IAChH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,gDAAgD;IAChD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,iEAAiE;IACjE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,oDAAoD;IACpD,cAAc,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;CACnC;AAkND;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,qBAwOpD;yBAxOe,YAAY"}
@@ -22,7 +22,7 @@
22
22
  * - /apps/storybook/stories/ChatComposer.stories.tsx
23
23
  * - /packages/cli/assets/templates/blocks/components/ChatComposer/ (block examples)
24
24
  */
25
- import React, { useState, useCallback, useRef, useMemo } from 'react';
25
+ import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
26
26
  import * as stylex from '@stylexjs/stylex';
27
27
  import "../theme/tokens.stylex.js";
28
28
  import { colorVars, spacingVars, radiusVars, shadowVars, durationVars, easeVars, borderVars, typeScaleVars, typographyVars } from "../theme/tokens.stylex.js";
@@ -33,6 +33,7 @@ import { ChatComposerContext } from "./ChatContext.js";
33
33
  import { ChatSendButton } from "./ChatSendButton.js";
34
34
  import { themeProps } from "../utils/themeProps.js";
35
35
  import { useTranslator } from "../i18n/index.js";
36
+ import { useAnnounce } from "../hooks/useAnnounce.js";
36
37
  import { focusOutlineStyles } from "../utils/focusOutline.stylex.js";
37
38
  import { getInteractionModality, useInteractionModalityTracking } from "../utils/interactionModality.js";
38
39
 
@@ -191,8 +192,30 @@ export function ChatComposer(props) {
191
192
  setHasKeyboardEditorFocus(false);
192
193
  }
193
194
  }, []);
195
+
196
+ // The status bar mounts together with its message, and a live region born
197
+ // holding its text is not reliably announced, so the message goes through
198
+ // the shared channel instead: errors assertively, warnings politely
199
+ // (spec:AST-086 FR2, FR5). The ref keeps a rerender or a StrictMode effect
200
+ // replay from repeating it; clearing the status re-arms it, so the same
201
+ // message shown again is a new occasion (FR6).
202
+ const announce = useAnnounce();
203
+ const statusType = status?.type;
204
+ const statusMessage = status?.message;
205
+ const announcedStatusRef = useRef(null);
206
+ useEffect(() => {
207
+ if (!statusMessage) {
208
+ announcedStatusRef.current = null;
209
+ return;
210
+ }
211
+ const occasion = `${statusType}\u0000${statusMessage}`;
212
+ if (announcedStatusRef.current === occasion) {
213
+ return;
214
+ }
215
+ announcedStatusRef.current = occasion;
216
+ announce(statusMessage, statusType === 'error' ? 'assertive' : 'polite');
217
+ }, [announce, statusType, statusMessage]);
194
218
  const statusEl = status ? /*#__PURE__*/_jsxs("div", {
195
- role: status.type === 'error' ? 'alert' : 'status',
196
219
  ...{
197
220
  0: {
198
221
  className: "x1n2onr6 x1ja2u2z x78zum5 x6s0dn4 x1txdalj x1pzlopt x141an7d x1ltkj2j x9ynric x1htvmrp xvpcij2 xckg3cf xkvxy4d x6k0y1c"
@@ -44,6 +44,10 @@ export interface ChatSystemMessageProps extends BaseProps<HTMLDivElement> {
44
44
  * "user joined", status changes. Supports a divider variant with horizontal
45
45
  * lines on each side of the text.
46
46
  *
47
+ * It carries no live role: a notice added to a `ChatMessageList` is announced
48
+ * by the list's log, which owns content added inside it (`spec:AST-086` FR4),
49
+ * and a date or divider row is ordinary transcript content.
50
+ *
47
51
  * @example
48
52
  * ```
49
53
  * <ChatSystemMessage>Conversation started</ChatSystemMessage>
@@ -1 +1 @@
1
- {"version":3,"file":"ChatSystemMessage.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatSystemMessage.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAWrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACvE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,wBAAwB,CAAC;IAEnC;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAwCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,OAAmB,EACnB,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EAAE,SAAS,EAChB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,sBAAsB,+BAmCxB;yBA5Ce,iBAAiB"}
1
+ {"version":3,"file":"ChatSystemMessage.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatSystemMessage.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAWrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAG5C,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,sBAAuB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACvE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,wBAAwB,CAAC;IAEnC;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAwCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,OAAmB,EACnB,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EAAE,SAAS,EAChB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,sBAAsB,+BAiCxB;yBA1Ce,iBAAiB"}
@@ -60,6 +60,10 @@ const styles = {
60
60
  * "user joined", status changes. Supports a divider variant with horizontal
61
61
  * lines on each side of the text.
62
62
  *
63
+ * It carries no live role: a notice added to a `ChatMessageList` is announced
64
+ * by the list's log, which owns content added inside it (`spec:AST-086` FR4),
65
+ * and a date or divider row is ordinary transcript content.
66
+ *
63
67
  * @example
64
68
  * ```
65
69
  * <ChatSystemMessage>Conversation started</ChatSystemMessage>
@@ -84,7 +88,6 @@ export function ChatSystemMessage({
84
88
  variant
85
89
  }), stylex.props(styles.dividerWrap, xstyle), className, styleProp),
86
90
  ...rest,
87
- role: "status",
88
91
  children: /*#__PURE__*/_jsx(Divider, {
89
92
  label: children
90
93
  })
@@ -96,7 +99,6 @@ export function ChatSystemMessage({
96
99
  variant
97
100
  }), stylex.props(styles.root, xstyle), className, styleProp),
98
101
  ...rest,
99
- role: "status",
100
102
  children: /*#__PURE__*/_jsxs("span", {
101
103
  ...{
102
104
  className: "x3nfvp2 x6s0dn4 x1s4dlld"
@@ -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;AAqCrC,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,iBAAiB,EAChC,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,iBAAiB,EAChC,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBAselB;yBA3fe,WAAW"}
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;AAqCrC,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,iBAAiB,EAChC,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,cAAc,EAC1B,aAAa,EAAE,iBAAiB,EAChC,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBAkflB;yBAvgBe,WAAW"}
@@ -327,15 +327,27 @@ export function ContextMenu({
327
327
  if (!isOpen || usesBottomSheet) {
328
328
  return;
329
329
  }
330
+ // Inside a shadow root, even a closed or nested one, the document sees a
331
+ // press on the menu as a press on an outer shadow host, so the menu's own
332
+ // tree judges its presses first and the document skips those.
333
+ const tree = listRef.current?.getRootNode();
334
+ const shadow = tree instanceof ShadowRoot ? tree : null;
335
+ const judged = new WeakSet();
330
336
  const handleClickOutside = e => {
331
337
  const menu = listRef.current;
338
+ if (judged.has(e)) {
339
+ return;
340
+ }
341
+ judged.add(e);
332
342
  if (menu && !menu.contains(e.target)) {
333
343
  closeMenu();
334
344
  }
335
345
  };
336
346
  document.addEventListener('mousedown', handleClickOutside);
347
+ shadow?.addEventListener('mousedown', handleClickOutside);
337
348
  return () => {
338
349
  document.removeEventListener('mousedown', handleClickOutside);
350
+ shadow?.removeEventListener('mousedown', handleClickOutside);
339
351
  };
340
352
  }, [isOpen, closeMenu, listRef, usesBottomSheet]);
341
353
  const listKeyDown = useCallback(e => {
@@ -9,7 +9,7 @@
9
9
  * comes from the parent DropdownMenu's useListFocus, which matches
10
10
  * menuitemradio rows.
11
11
  */
12
- import { type ReactNode } from 'react';
12
+ import { type ReactNode, type Ref } from 'react';
13
13
  import type { BaseProps } from '../BaseProps';
14
14
  export interface DropdownMenuRadioGroupProps extends Omit<BaseProps, 'onChange'> {
15
15
  /**
@@ -24,9 +24,9 @@ export interface DropdownMenuRadioGroupProps extends Omit<BaseProps, 'onChange'>
24
24
  /**
25
25
  * Accessible name for the group, announced by screen readers so the radios
26
26
  * read as a named set (e.g. "Sort by"). Applied as the group's `aria-label`.
27
- * Required -- an unnamed radio group is an accessibility defect. Pass
28
- * `aria-labelledby` (via base props) instead if the name already exists as a
29
- * visible element on the page.
27
+ * Required even when `aria-labelledby` (via base props) points to visible
28
+ * text; that visible reference becomes the computed name while `label`
29
+ * remains the fallback.
30
30
  */
31
31
  label: string;
32
32
  /**
@@ -36,6 +36,10 @@ export interface DropdownMenuRadioGroupProps extends Omit<BaseProps, 'onChange'>
36
36
  * @default true
37
37
  */
38
38
  hasCloseOnSelect?: boolean;
39
+ /**
40
+ * Ref forwarded to the role="group" root.
41
+ */
42
+ ref?: Ref<HTMLDivElement>;
39
43
  /**
40
44
  * The `DropdownMenuRadioItem`s that make up the group.
41
45
  */
@@ -58,7 +62,7 @@ export interface DropdownMenuRadioGroupProps extends Omit<BaseProps, 'onChange'>
58
62
  * </DropdownMenu>
59
63
  * ```
60
64
  */
61
- export declare function DropdownMenuRadioGroup({ value, onChange, label, hasCloseOnSelect, children, xstyle, className, style, ...rest }: DropdownMenuRadioGroupProps): import("react").JSX.Element;
65
+ export declare function DropdownMenuRadioGroup({ value, onChange, label, hasCloseOnSelect, children, ref, xstyle, className, style, ...rest }: DropdownMenuRadioGroupProps): import("react").JSX.Element;
62
66
  export declare namespace DropdownMenuRadioGroup {
63
67
  var displayName: string;
64
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenuRadioGroup.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenuRadioGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAI9C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAiB5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,EACT,UAAU,CACX;IACC;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,KAAK,EACL,gBAAuB,EACvB,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,2BAA2B,+BAiB7B;yBA3Be,sBAAsB"}
1
+ {"version":3,"file":"DropdownMenuRadioGroup.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenuRadioGroup.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAU,KAAK,SAAS,EAAE,KAAK,GAAG,EAAC,MAAM,OAAO,CAAC;AAIxD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAiB5C,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CACvD,SAAS,EACT,UAAU,CACX;IACC;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,QAAQ,EACR,KAAK,EACL,gBAAuB,EACvB,QAAQ,EACR,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,2BAA2B,+BAkB7B;yBA7Be,sBAAsB"}
@@ -51,6 +51,7 @@ export function DropdownMenuRadioGroup({
51
51
  label,
52
52
  hasCloseOnSelect = true,
53
53
  children,
54
+ ref,
54
55
  xstyle,
55
56
  className,
56
57
  style,
@@ -63,6 +64,7 @@ export function DropdownMenuRadioGroup({
63
64
  }), [value, onChange, hasCloseOnSelect]);
64
65
  return /*#__PURE__*/_jsx("div", {
65
66
  ...rest,
67
+ ref: ref,
66
68
  role: "group",
67
69
  "aria-label": label,
68
70
  ...mergeProps(stylex.props(styles.group, xstyle), {
@@ -110,8 +110,8 @@ export interface UseMenuPressOptions {
110
110
  getScroller?: () => HTMLElement | null;
111
111
  /**
112
112
  * Resolve the element under a viewport point. Defaults to
113
- * `document.elementFromPoint`, falling back to the event target where the
114
- * document lacks it (jsdom). A test seam.
113
+ * `elementFromPoint` on the menu's own document or shadow root, falling back
114
+ * to the event target where the document lacks it (jsdom). A test seam.
115
115
  */
116
116
  hitTest?: (x: number, y: number) => Element | null;
117
117
  /** @default 500 */
@@ -1 +1 @@
1
- {"version":3,"file":"useMenuPress.d.ts","sourceRoot":"","sources":["../../src/hooks/useMenuPress.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAA0C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE9E,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAG5B,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,mCAAmC,CAAC;AAExE,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAchD;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AA6DD,0DAA0D;AAC1D,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAMD,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACvC,uEAAuE;IACvE,UAAU,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,oBAAoB,KAAK,OAAO,CAAC;IAChE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC/D,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACnD,mBAAmB;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE;QACT,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;QAChE,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC;KACzB,CAAC;IACF,yEAAyE;IACzE,YAAY,EAAE;QACZ,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;QAChE,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;KAC/D,CAAC;IACF;;;;OAIG;IACH,uBAAuB,EAAE,MAAM,OAAO,CAAC;IACvC,uEAAuE;IACvE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAkED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CA2iB7E"}
1
+ {"version":3,"file":"useMenuPress.d.ts","sourceRoot":"","sources":["../../src/hooks/useMenuPress.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAA0C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE9E,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAG5B,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,mCAAmC,CAAC;AAExE,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAC5C,oFAAoF;AACpF,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAchD;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AA6DD,0DAA0D;AAC1D,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAMD,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACvC,uEAAuE;IACvE,UAAU,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC3C;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,oBAAoB,KAAK,OAAO,CAAC;IAChE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC/D,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACnD,mBAAmB;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,SAAS,EAAE;QACT,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;QAChE,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAAC;KACzB,CAAC;IACF,yEAAyE;IACzE,YAAY,EAAE;QACZ,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;QAChE,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;KAC/D,CAAC;IACF;;;;OAIG;IACH,uBAAuB,EAAE,MAAM,OAAO,CAAC;IACvC,uEAAuE;IACvE,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAkED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAojB7E"}
@@ -256,7 +256,11 @@ export function useMenuPress(options) {
256
256
  if (hitTest != null) {
257
257
  element = hitTest(event.clientX, event.clientY);
258
258
  } else if (typeof doc.elementFromPoint === 'function') {
259
- element = doc.elementFromPoint(event.clientX, event.clientY);
259
+ // Hit-test in the menu's own tree: inside a shadow root the document
260
+ // answers with the shadow host, never the row under the pointer.
261
+ const tree = menu?.getRootNode();
262
+ const scope = tree != null && tree !== doc && typeof tree.elementFromPoint === 'function' ? tree : doc;
263
+ element = scope.elementFromPoint(event.clientX, event.clientY);
260
264
  } else {
261
265
  element = event.target instanceof Element ? event.target : null;
262
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.3-canary.948e75d",
3
+ "version": "0.7.3-canary.97dda3c",
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.3-canary.948e75d",
796
- "@astryxdesign/cli": "0.7.3-canary.948e75d",
795
+ "@astryxdesign/a11y-spec": "0.7.3-canary.97dda3c",
796
+ "@astryxdesign/cli": "0.7.3-canary.97dda3c",
797
797
  "@babel/cli": "^7.29.7",
798
798
  "@babel/core": "^7.29.7",
799
799
  "@babel/preset-react": "^7.29.7",
@@ -17,6 +17,7 @@ import {
17
17
  fireEvent,
18
18
  createEvent,
19
19
  act,
20
+ waitFor,
20
21
  } from '@testing-library/react';
21
22
  import userEvent from '@testing-library/user-event';
22
23
  import {Button} from './Button';
@@ -643,7 +644,17 @@ describe('Button', () => {
643
644
  expect(liveRegion).toHaveTextContent('Loading');
644
645
  });
645
646
 
646
- it('localizes the loading announcement through the i18n catalog', () => {
647
+ it('mounts the loading live region empty when first rendered loading, then fills it (spec:AST-086 FR3)', async () => {
648
+ render(<Button label="Submit" isLoading />);
649
+ const button = screen.getByRole('button');
650
+ // The Spinner also has role="status", so grab the live region explicitly.
651
+ const regions = button.querySelectorAll('[role="status"]');
652
+ const liveRegion = regions[regions.length - 1];
653
+ expect(liveRegion).toHaveTextContent('');
654
+ await waitFor(() => expect(liveRegion).toHaveTextContent('Loading'));
655
+ });
656
+
657
+ it('localizes the loading announcement through the i18n catalog', async () => {
647
658
  render(
648
659
  <InternationalizationProvider
649
660
  locale="fr"
@@ -655,7 +666,7 @@ describe('Button', () => {
655
666
  // The Spinner also has role="status", so grab the live region explicitly.
656
667
  const regions = button.querySelectorAll('[role="status"]');
657
668
  const liveRegion = regions[regions.length - 1];
658
- expect(liveRegion).toHaveTextContent('Chargement');
669
+ await waitFor(() => expect(liveRegion).toHaveTextContent('Chargement'));
659
670
  });
660
671
 
661
672
  describe('in a narrow row', () => {
@@ -18,7 +18,13 @@
18
18
  * Last synced props: label, variant, size, isDisabled, isLoading, isInterruptible, clickAction, icon, isIconOnly, width, children, tooltip, endContent, href, as, target, rel
19
19
  */
20
20
 
21
- import {useRef, useTransition, type ReactNode} from 'react';
21
+ import {
22
+ useEffect,
23
+ useRef,
24
+ useState,
25
+ useTransition,
26
+ type ReactNode,
27
+ } from 'react';
22
28
  import type {BaseProps} from '../BaseProps';
23
29
  import type {Elevation, SizeValue} from '../utils/types';
24
30
  import * as stylex from '@stylexjs/stylex';
@@ -587,6 +593,21 @@ export function Button({
587
593
  // re-click can land and interrupt the in-flight action with a fresh one.
588
594
  const actionInFlightRef = useRef(false);
589
595
  const isLoadingState = isLoading || isPending;
596
+ // The loading live region must exist empty before it receives "Loading":
597
+ // many screen readers skip a live region that mounts already holding its
598
+ // text, which is what a Button first rendered loading did (spec:AST-086
599
+ // FR3). Only that case holds the text back, until the frame after the
600
+ // region mounts; a Button mounted idle never re-renders for this.
601
+ const [isLiveRegionReady, setIsLiveRegionReady] = useState(
602
+ () => !isLoadingState,
603
+ );
604
+ useEffect(() => {
605
+ if (isLiveRegionReady) {
606
+ return;
607
+ }
608
+ const frame = requestAnimationFrame(() => setIsLiveRegionReady(true));
609
+ return () => cancelAnimationFrame(frame);
610
+ }, [isLiveRegionReady]);
590
611
  // Delay the spinner reveal for action-driven loading (clickAction's own
591
612
  // transition) so a fast action that settles within the delay does not flash
592
613
  // a spinner. Interruptible loading is delayed too, so rapid re-clicks settle
@@ -767,7 +788,7 @@ export function Button({
767
788
  </span>
768
789
  {/* Live region for loading state announcements */}
769
790
  <VisuallyHidden role="status" aria-live="polite">
770
- {isLoadingState ? t('@astryx.button.loading') : ''}
791
+ {isLoadingState && isLiveRegionReady ? t('@astryx.button.loading') : ''}
771
792
  </VisuallyHidden>
772
793
  </>
773
794
  );
@@ -8,7 +8,8 @@
8
8
  * BUTTON_PATTERN_EXCLUSIONS, the parts deliberately left to another pattern.
9
9
  * @position The binding inventory required before any assertion moves
10
10
  * (`docs/specs/AST-021/spec.md` FR2, FR4), and the single definition of what
11
- * each state IS.
11
+ * each state IS. Disabled ClickableCard remains focusable and inert under
12
+ * `docs/specs/AST-085-focusable-disabled/spec.md` FR1 and FR2.
12
13
  *
13
14
  * A state earns a row when it can change what the button pattern promises: a
14
15
  * different name source, a different operability, a different exposure of
@@ -223,8 +224,8 @@ export const BUTTON_BINDING_STATES = [
223
224
  {
224
225
  id: 'clickable-card-disabled',
225
226
  binding: 'ClickableCard',
226
- summary: 'a disabled action card',
227
- facts: facts({operable: false, focusable: false, unavailable: true}),
227
+ summary: 'a disabled action card: focusable, unavailable, and inert',
228
+ facts: facts({operable: false, focusable: true, unavailable: true}),
228
229
  visibleLabel: null,
229
230
  pointerTargetSelector: '[data-a11y-pointer-target]',
230
231
  storyId: 'a11y-button-pattern--clickable-card-disabled',
@@ -0,0 +1,146 @@
1
+ ---
2
+ schema_version: 3
3
+ template_version: 7
4
+ kind: component
5
+ id: component:Carousel
6
+ authority: current
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: cixzhang
10
+ approved_at: 2026-10-22
11
+ owners: [cixzhang]
12
+ review_triggers: [behavior, accessibility]
13
+ verified_by:
14
+ [
15
+ packages/core/src/Carousel/Carousel.test.tsx,
16
+ packages/core/src/Carousel/__tests__/Carousel.a11y.chromium.spec.ts,
17
+ ]
18
+ modules: []
19
+ families: [family:buttons]
20
+ design_specs: []
21
+ architecture: [architecture:interaction-modality]
22
+ contributing: []
23
+ system_specs: [spec:AST-085, spec:AST-025]
24
+ ---
25
+
26
+ # Carousel component contract
27
+
28
+ ## Contract at a glance
29
+
30
+ | Area | Contract |
31
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | Public contract | None changes. |
33
+ | Behavior | An edge scroll button whose direction has run out is hidden and takes no tab stop. When the focused edge button runs out, focus moves to the scroll area. |
34
+ | End-user impact | A keyboard user who scrolls to the first or last item stays inside the carousel instead of landing on the page body or on an invisible control. The scroll area has no role or name yet, so screen-reader users hear little at the destination until it meets `spec:AST-025` FR12. |
35
+ | Builder impact | None. |
36
+ | Compatibility/readiness | No API change. Shipped Carousel and its current tests still use native `disabled` and hand focus to the opposite button; they change with conformance work. |
37
+ | Review checks | Reject focus left on a hidden edge button, focus dropped to the page body, or an edge handoff that moves focus to anything but the scroll area. |
38
+ | Governing rules | `spec:AST-085` FR1, FR4; `spec:AST-025` FR12; `architecture:interaction-modality` INV1. |
39
+
40
+ This table is a review projection; the body below is authoritative.
41
+
42
+ ## Intent
43
+
44
+ Carousel shows a horizontally scrolling row of items inside a labelled region,
45
+ with optional scroll buttons at each edge. This record covers only what the edge
46
+ scroll buttons do when their direction runs out. Other Carousel behavior is
47
+ uncontracted until this record grows.
48
+
49
+ ## Compatibility and migration
50
+
51
+ - Released default preserved: yes, except where focus lands at an edge.
52
+ - Compatibility class: behavior correction; no API change.
53
+ - Controlled/uncontrolled behavior: unchanged.
54
+ - Migration decision: DEC-1.
55
+
56
+ Consumer migration instructions belong in consumer docs and release notes.
57
+
58
+ ## Ownership boundary
59
+
60
+ **Owns**
61
+
62
+ - Edge scroll button visibility, tab order, and the focus destination when one
63
+ runs out.
64
+
65
+ **Does not own / non-goals**
66
+
67
+ - Scroll area keyboard access and naming — `spec:AST-025`.
68
+ - Disabled control semantics in general — `spec:AST-085`.
69
+ - Button naming, sizing, and activation — `family:buttons`.
70
+ - Looping, snapping, edge fades, and slide semantics — uncontracted.
71
+
72
+ ## Public concepts
73
+
74
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
75
+ | ------------------ | ----------------------- | -------------------------------------------------- | ----------------------------------------- | ------- | -------------------- | --------- | ---------------------- |
76
+ | Edge scroll button | available, run out | Whether content remains in that button's direction | Both edges when the carousel overflows | — | `component:Carousel` | stable | not applicable |
77
+
78
+ ## Behavioral and layout contract
79
+
80
+ | ID | Candidate invariant | Basis | Draft review state |
81
+ | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------ |
82
+ | FR1 | An edge scroll button whose direction has run out is hidden. While hidden it takes no tab stop and is not exposed to assistive technology. It stays mounted so layout does not shift. This is removal under `spec:AST-085` FR4, not a disabled control. | DEC-1 | settled |
83
+ | FR2 | When the focused edge scroll button runs out, focus moves to the scroll area without scrolling it. This holds when the content no longer overflows and the scroll area is out of the tab order (`spec:AST-025` FR12): it still takes focus programmatically. Focus never lands on the page body or on a hidden button. | DEC-1; `spec:AST-085` FR4 | settled |
84
+ | FR3 | The handoff happens once per edge transition, and only while the edge button held focus. An edge reached by swipe, resize, or wheel while focus is elsewhere moves nothing. | `architecture:interaction-modality` INV8 | settled |
85
+ | FR4 | An edge scroll button that is available behaves as an ordinary Button. | `family:buttons` | settled |
86
+
87
+ ### Representative states
88
+
89
+ | State | Required invariant | Allowed variation |
90
+ | -------------------------------------- | --------------------------------------------------------------- | ----------------------- |
91
+ | Mid-scroll | Both edge buttons available | Loop keeps both enabled |
92
+ | Focused "Scroll right" reaches the end | Button hides; focus is on the scroll area; no extra scroll | Reduced motion |
93
+ | Edge reached by swipe, focus elsewhere | Button hides; focus does not move | — |
94
+ | Content shrinks to fit | Both buttons hidden; a focused one hands off to the scroll area | — |
95
+
96
+ ## Accessibility contract
97
+
98
+ - **AR1 — No invisible tab stops.** A hidden edge button is not focusable and is
99
+ not exposed to assistive technology (FR1).
100
+ - **AR2 — Focus stays in the region.** The edge handoff keeps focus inside the
101
+ labelled carousel region (FR2), and the keyboard focus indicator follows
102
+ `architecture:interaction-modality` INV1.
103
+
104
+ ## Design relationships
105
+
106
+ No design record governs Carousel yet. Edge button visuals stay in code and
107
+ visual evidence.
108
+
109
+ ## Family and system relationships
110
+
111
+ - `spec:AST-085` FR4 lets a component name a focus destination for a control it
112
+ removes; FR2 names the scroll area.
113
+ - `spec:AST-025` FR12 owns the scroll area's tab stop and naming.
114
+ - `family:buttons` owns the edge buttons' behavior while available.
115
+
116
+ ## Verification map
117
+
118
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
119
+ | -------- | --------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------ |
120
+ | FR1, AR1 | Unit and accessibility-tree tests | at rest at each edge; no overflow | A hidden button that is focusable or exposed fails | `audit:Carousel/accessibility` |
121
+ | FR2, AR2 | Real-browser keyboard test | Enter on "Scroll right" until the end; same on the start | Focus on the page body, a hidden button, or the opposite button fails | `audit:Carousel/accessibility` |
122
+ | FR3 | Real-browser test with swipe and resize | edge reached with focus outside the carousel | Focus moving without an edge-button press, or moving twice for one edge, fails | `audit:Carousel/accessibility` |
123
+
124
+ ## Decision log
125
+
126
+ ### DEC-1 — The scroll area receives focus when an edge button runs out
127
+
128
+ **Reference:** `component:Carousel/DEC-1`
129
+ **Decider:** cixzhang, 2026-10-22
130
+
131
+ Carousel hides an edge button whose direction has run out, so keeping focus on
132
+ it would leave a keyboard user on an invisible control. The scroll area is always
133
+ mounted inside the labelled region and can take focus whether or not it is in the
134
+ tab order, so focus moving there keeps the user in place inside the carousel.
135
+
136
+ Rejected: the opposite edge button — it may be hidden too, and focus would jump
137
+ across the carousel.
138
+
139
+ ## Open questions
140
+
141
+ None.
142
+
143
+ ## Content boundary
144
+
145
+ This file does not duplicate consumer prop tables/examples, current audit
146
+ results, implementation steps, or family/system rules. It links to their owners.