@astryxdesign/core 0.7.4-canary.e5c5a87 → 0.7.4-canary.faa934f

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 (33) hide show
  1. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  2. package/dist/Chat/ChatMessageList.js +8 -1
  3. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  4. package/dist/CheckboxInput/CheckboxInput.js +6 -0
  5. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  6. package/dist/DateTimeInput/DateTimeInput.js +6 -1
  7. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  8. package/dist/DateTimeInput/TouchDateTimeField.js +9 -1
  9. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  10. package/dist/DropdownMenu/DropdownMenuSubMenu.js +7 -1
  11. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  12. package/dist/Thumbnail/Thumbnail.js +9 -2
  13. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  14. package/dist/TimeInput/TimeInput.js +6 -1
  15. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  16. package/dist/TimeInput/TouchTimeField.js +9 -1
  17. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  18. package/dist/hooks/useTypeahead.js +7 -2
  19. package/package.json +3 -3
  20. package/src/Chat/ChatMessageList.test.tsx +6 -2
  21. package/src/Chat/ChatMessageList.tsx +7 -1
  22. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +20 -12
  23. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  24. package/src/DateTimeInput/DateTimeInput.tsx +5 -1
  25. package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
  26. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
  27. package/src/Thumbnail/Thumbnail.test.tsx +6 -1
  28. package/src/Thumbnail/Thumbnail.tsx +12 -3
  29. package/src/TimeInput/TimeInput.tsx +5 -1
  30. package/src/TimeInput/TouchTimeField.tsx +12 -2
  31. package/src/__tests__/BusyOwnerAnnouncement.test.tsx +209 -2
  32. package/src/hooks/useTypeahead.test.tsx +42 -1
  33. package/src/hooks/useTypeahead.ts +7 -2
@@ -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":"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"}
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":"DateTimeInput.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/DateTimeInput.tsx"],"names":[],"mappings":"AA4CA,OAAO,EAGL,KAAK,WAAW,EAKjB,MAAM,UAAU,CAAC;AAIlB,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AA6BrB,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAc9C,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG;IACvC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;CACvC,CAAC;AAEF,qFAAqF;AACrF,MAAM,MAAM,yBAAyB,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE,uGAAuG;AACvG,MAAM,MAAM,yBAAyB,GAAG,kBAAkB,CAAC;AAE3D,MAAM,MAAM,uBAAuB,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,MAAM,0BAA0B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9D;;;GAGG;AACH,MAAM,MAAM,+BAA+B,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEpE,YAAY,EACV,WAAW,IAAI,mBAAmB,EAClC,eAAe,IAAI,uBAAuB,GAC3C,MAAM,UAAU,CAAC;AAiKlB,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAC9C,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,UAAU,GAAG,cAAc,CAC5B;IACC,8CAA8C;IAC9C,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAElC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAE1B;;;OAGG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,GAAG,SAAS,KAAK,IAAI,CAAC;IAEzD;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,GAAG,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;IAExB;;;OAGG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;IAExB;;;OAGG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC;IAEzD;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAC;IAErC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,0BAA0B,CAAC;IAE3C;;;;;;;;;;;;;;;OAeG;IACH,kBAAkB,CAAC,EAAE,+BAA+B,CAAC;IAErD;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IAEzB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC;IAEzC;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,YAAY,CAAC,EAAE,yBAAyB,CAAC;IAEzC;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,yBAAyB,CAAC;CAC1C;AA06CD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,+BAqCtD;yBArCe,aAAa"}
1
+ {"version":3,"file":"DateTimeInput.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/DateTimeInput.tsx"],"names":[],"mappings":"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"}
@@ -32,6 +32,7 @@ import { useCalendarConstraints } from "../Calendar/hooks/index.js";
32
32
  import { usePopover } from "../Popover/index.js";
33
33
  import { useTooltip } from "../Tooltip/index.js";
34
34
  import { useInputContainer } from "../hooks/useInputContainer.js";
35
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
35
36
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
36
37
  import { parseDateInput, parseISOTime, parseTimeInput, formatDisplayTime12h, formatDisplayTime24h, formatISOTime, isTimeInRange, adjustTime, isImeKeyEvent, mergeProps, isFocusDetached } from "../utils/index.js";
37
38
  import { chainEventHandlers } from "../utils/chainEventHandlers.js";
@@ -317,6 +318,9 @@ function PointerDateTimeField({
317
318
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
318
319
  const isPendingChange = optimisticValue !== value;
319
320
  const isBusy = isLoading || isPendingChange;
321
+ // The busy Spinner is decorative; this control announces its busy start
322
+ // (spec:AST-086 FR8).
323
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), dateInputRef);
320
324
  // A native wheel emits several edits while it remains open. Disabling the
321
325
  // control after the first optimistic edit detaches the iOS picker, so pending
322
326
  // changeAction work stays interactive just like TouchDateTimeField. Explicit
@@ -1130,7 +1134,8 @@ function PointerDateTimeField({
1130
1134
  }),
1131
1135
  onClick: handleClear
1132
1136
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
1133
- size: "sm"
1137
+ size: "sm",
1138
+ "aria-hidden": "true"
1134
1139
  }), statusIcon]
1135
1140
  }), /*#__PURE__*/_jsx("div", {
1136
1141
  ref: timeContainerRef,
@@ -1 +1 @@
1
- {"version":3,"file":"TouchDateTimeField.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/TouchDateTimeField.tsx"],"names":[],"mappings":"AA6GA,OAAO,KAAK,EAAC,kBAAkB,EAAoB,MAAM,iBAAiB,CAAC;AAuV3E,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,UAAkB,EAClB,UAAkB,EAElB,aAAa,EAAE,cAAc,EAC7B,kBAAkB,EAAE,mBAAmB,EACvC,QAAgB,EAChB,WAAW,EAAE,oBAAoB,EACjC,eAAe,EAAE,wBAAwB,EACzC,SAAS,EACT,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EAEZ,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,kBAAkB,+BAu/BpB;yBA9hCe,kBAAkB"}
1
+ {"version":3,"file":"TouchDateTimeField.d.ts","sourceRoot":"","sources":["../../src/DateTimeInput/TouchDateTimeField.tsx"],"names":[],"mappings":"AA8GA,OAAO,KAAK,EAAC,kBAAkB,EAAoB,MAAM,iBAAiB,CAAC;AAuV3E,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,eAAe,EACf,UAAkB,EAClB,UAAkB,EAElB,aAAa,EAAE,cAAc,EAC7B,kBAAkB,EAAE,mBAAmB,EACvC,QAAgB,EAChB,WAAW,EAAE,oBAAoB,EACjC,eAAe,EAAE,wBAAwB,EACzC,SAAS,EACT,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,YAAY,EAEZ,cAAc,EAAE,eAAe,EAC/B,YAAY,EAAE,gBAAoB,EAClC,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,kBAAkB,+BAkgCpB;yBAziCe,kBAAkB"}
@@ -29,6 +29,7 @@ import { useCalendarConstraints } from "../Calendar/index.js";
29
29
  import { Field, InputClearButton, inputWrapperStyles, inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles } from "../Field/index.js";
30
30
  import { useInputStatusIcon, useMergedRefs } from "../hooks/index.js";
31
31
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
32
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
32
33
  import { Icon } from "../Icon/index.js";
33
34
  import { IconButton } from "../IconButton/index.js";
34
35
  import { useTranslator, InternationalizationContext } from "../i18n/index.js";
@@ -338,6 +339,11 @@ export function TouchDateTimeField({
338
339
  dateConstraints
339
340
  });
340
341
  const [isSheetOpen, setIsSheetOpen] = useState(false);
342
+ const sheetBodyRef = useRef(null);
343
+ // The busy Spinner is decorative; this control announces its busy start
344
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
345
+ // the sheet is open, so it is announced from inside the sheet.
346
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), isSheetOpen ? sheetBodyRef : dateInputRef);
341
347
  const [activePanel, setActivePanel] = useState('date');
342
348
  const [isWheelOpen, setIsWheelOpen] = useState(false);
343
349
  const [draftTime, setDraftTime] = useState(() => normalizeISOTime(splitDateTime(value).time, hasSeconds));
@@ -990,7 +996,8 @@ export function TouchDateTimeField({
990
996
  }),
991
997
  onClick: handleClear
992
998
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
993
- size: "sm"
999
+ size: "sm",
1000
+ "aria-hidden": "true"
994
1001
  }), isRenderable(statusIcon) && statusIcon]
995
1002
  }), /*#__PURE__*/_jsxs("div", {
996
1003
  onClick: event => {
@@ -1058,6 +1065,7 @@ export function TouchDateTimeField({
1058
1065
  ,
1059
1066
  padding: 0,
1060
1067
  children: /*#__PURE__*/_jsx("div", {
1068
+ ref: sheetBodyRef,
1061
1069
  ...{
1062
1070
  className: "x9f619 xh8yej3 xeuugli x1pzlopt x1adxfkp x1awphl8"
1063
1071
  },
@@ -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":"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
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TimeInput.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TimeInput.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TimeInput.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TimeInput.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAO3C,OAAO,EAGL,KAAK,WAAW,EAKhB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAIlB,OAAO,EACL,KAAK,aAAa,EAUnB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAiB9C,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAC;AAyDpC,QAAA,MAAM,UAAU;;;;;;;;;;;;;EAad,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,UAAU,CAAC;AAEpD,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,KAAK,CAAC;AAEhD,iFAAiF;AACjF,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjE,4EAA4E;AAC5E,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC;AAItD,YAAY,EACV,WAAW,IAAI,eAAe,EAC9B,eAAe,IAAI,mBAAmB,GACvC,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,UAAU,GAAG,cAAc,CAC5B;IACC,qEAAqE;IACrE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAClC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,CAAC;IAEtD;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1E;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;IAEpB;;OAEG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,mBAAmB,CAAC;IAEjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAErC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IAErB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;IAEnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA8hBD;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAyC9C;yBAzCe,SAAS"}
@@ -30,6 +30,7 @@ import { joinAriaIDs } from "../utils/inputAria.js";
30
30
  import { useFieldInputStyling } from "../Field/fieldParts.js";
31
31
  import { useSize } from "../SizeContext/SizeContext.js";
32
32
  import { useAnnounce } from "../hooks/useAnnounce.js";
33
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
33
34
  import { useInputContainer } from "../hooks/useInputContainer.js";
34
35
  import { useMediaQuery } from "../hooks/useMediaQuery.js";
35
36
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
@@ -169,6 +170,9 @@ function TimeField({
169
170
  const [, startTransition] = useTransition();
170
171
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
171
172
  const isBusy = isLoading || optimisticValue !== value;
173
+ // The busy Spinner is decorative; this control announces its busy start
174
+ // (spec:AST-086 FR8).
175
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), containerRef);
172
176
 
173
177
  // In grouped mode the status message renders as a visually-hidden node that
174
178
  // exists only for aria-describedby. Announce it through the persistent
@@ -504,7 +508,8 @@ function TimeField({
504
508
  children: !isInputValid ? t('@astryx.timeInput.invalidTime') : ''
505
509
  })]
506
510
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
507
- size: "sm"
511
+ size: "sm",
512
+ "aria-hidden": "true"
508
513
  }), hasClear && optimisticValue && !isDisabled && /*#__PURE__*/_jsx(InputClearButton, {
509
514
  label: t('@astryx.timeInput.clearLabel', {
510
515
  label
@@ -1 +1 @@
1
- {"version":3,"file":"TouchTimeField.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TouchTimeField.tsx"],"names":[],"mappings":"AAoEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAqHhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAGlB,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,aAAa,EAC3B,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,cAAc,+BAiZhB;yBApbe,cAAc"}
1
+ {"version":3,"file":"TouchTimeField.d.ts","sourceRoot":"","sources":["../../src/TimeInput/TouchTimeField.tsx"],"names":[],"mappings":"AAqEA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,aAAa,CAAC;AAqHhD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,UAAkB,EAClB,eAAe,EACf,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,SAAiB,EACjB,GAAG,EACH,GAAG,EACH,UAAkB,EAClB,QAAgB,EAChB,UAAkB,EAGlB,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,aAAa,EAC3B,WAAW,EAAE,oBAAoB,EACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EACN,aAA0B,EAC1B,YAAY,EACZ,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,GAAG,EACH,GAAG,SAAS,EACb,EAAE,cAAc,+BA0ZhB;yBA7be,cAAc"}
@@ -25,6 +25,7 @@ import { useFieldInputStyling } from "../Field/fieldParts.js";
25
25
  import { joinAriaIDs } from "../utils/inputAria.js";
26
26
  import { groupStyles } from "../InputGroup/groupStyles.js";
27
27
  import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
28
+ import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
28
29
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
29
30
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
30
31
  import { Icon } from "../Icon/index.js";
@@ -180,6 +181,11 @@ export function TouchTimeField({
180
181
  const [, startTransition] = useTransition();
181
182
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
182
183
  const isBusy = isLoading || optimisticValue !== value;
184
+ const sheetBodyRef = useRef(null);
185
+ // The busy Spinner is decorative; this control announces its busy start
186
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
187
+ // the sheet is open, so it is announced from inside the sheet.
188
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), isSheetOpen ? sheetBodyRef : inputRef);
183
189
  const isEffectivelyDisabled = isDisabled || isLoading;
184
190
  const showsDisabledMessage = isDisabled && !!disabledMessage;
185
191
  const disabledMessageTooltip = useTooltip({
@@ -362,7 +368,8 @@ export function TouchTimeField({
362
368
  className: "x1lliihq x98rzlu xeuugli xc342km xng3xce x1717udv xjbqb8w x1a2a7pz x1ypdohk x16khyan"
363
369
  }
364
370
  }), isBusy && /*#__PURE__*/_jsx(Spinner, {
365
- size: "sm"
371
+ size: "sm",
372
+ "aria-hidden": "true"
366
373
  }), hasClear && optimisticValue && !isEffectivelyDisabled && /*#__PURE__*/_jsx(InputClearButton, {
367
374
  label: t('@astryx.timeInput.clearLabel', {
368
375
  label
@@ -377,6 +384,7 @@ export function TouchTimeField({
377
384
  ,
378
385
  padding: 0,
379
386
  children: /*#__PURE__*/_jsxs("div", {
387
+ ref: sheetBodyRef,
380
388
  ...{
381
389
  className: "x9f619 x78zum5 xdt5ytf xh8yej3 xeuugli x1txdalj x1pzlopt x1adxfkp x1awphl8"
382
390
  },
@@ -1 +1 @@
1
- {"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../src/hooks/useTypeahead.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAE9D;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAE/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,KAAK,OAAO,CAAC;IAC/D,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA+BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CA8F7E"}
1
+ {"version":3,"file":"useTypeahead.d.ts","sourceRoot":"","sources":["../../src/hooks/useTypeahead.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,aAAa,EAAE,MAAM,aAAa,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAE9D;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAE/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,aAAa,KAAK,OAAO,CAAC;IAC/D,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AA+BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAmG7E"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTypeahead.ts
7
- * @input Uses React useCallback/useRef
7
+ * @input Uses React useCallback/useEffect/useRef
8
8
  * @output Exports useTypeahead hook for first-character (type-to-focus) search
9
9
  * @position Core hook; adds APG typeahead to menus, listboxes, and other
10
10
  * collections. Composes with useListFocus/useGridFocus (or any collection)
@@ -13,7 +13,7 @@
13
13
  * SYNC: When modified, update:
14
14
  * - /packages/core/src/hooks/index.ts
15
15
  */
16
- import { useCallback, useRef } from 'react';
16
+ import { useCallback, useEffect, useRef } from 'react';
17
17
  /**
18
18
  * Whether a key event represents a single printable character (a type-to-focus
19
19
  * candidate) rather than a control/navigation key or a shortcut chord.
@@ -79,6 +79,11 @@ export function useTypeahead(options) {
79
79
  timeoutRef.current = undefined;
80
80
  }
81
81
  }, []);
82
+
83
+ // A collection that unmounts, or that an `<Activity>` boundary hides,
84
+ // mid-typing leaves no reset timer running and comes back with an empty
85
+ // buffer, as the reset would have left it.
86
+ useEffect(() => reset, [reset]);
82
87
  const scheduleReset = useCallback(() => {
83
88
  if (timeoutRef.current) {
84
89
  clearTimeout(timeoutRef.current);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.4-canary.e5c5a87",
3
+ "version": "0.7.4-canary.faa934f",
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.e5c5a87",
796
- "@astryxdesign/cli": "0.7.4-canary.e5c5a87",
795
+ "@astryxdesign/a11y-spec": "0.7.4-canary.faa934f",
796
+ "@astryxdesign/cli": "0.7.4-canary.faa934f",
797
797
  "@babel/cli": "^7.29.7",
798
798
  "@babel/core": "^7.29.7",
799
799
  "@babel/preset-react": "^7.29.7",
@@ -182,9 +182,13 @@ describe('ChatMessageList', () => {
182
182
  );
183
183
  });
184
184
  expect(action).toHaveBeenCalledTimes(1);
185
- expect(screen.getByRole('status', {name: 'Loading'})).toBeTruthy();
185
+ // The loader Spinner inside the log is silent (spec:AST-086 FR4, FR8).
186
+ expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
187
+ 'aria-hidden',
188
+ 'true',
189
+ );
186
190
  await act(async () => complete());
187
- expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
191
+ expect(document.querySelector('.astryx-spinner')).toBeNull();
188
192
  unmount();
189
193
  expect(disconnect).toHaveBeenCalledTimes(1);
190
194
  } finally {
@@ -37,6 +37,8 @@ import {Spinner} from '../Spinner';
37
37
  import type {BaseProps} from '../BaseProps';
38
38
  import type {SpacingStep} from '../utils/types';
39
39
  import {themeProps} from '../utils/themeProps';
40
+ import {useTranslator} from '../i18n';
41
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
40
42
 
41
43
  export interface ChatMessageListProps extends BaseProps<HTMLDivElement> {
42
44
  /** Ref forwarded to the root element */
@@ -238,6 +240,10 @@ export function ChatMessageList({
238
240
  const sentinelRef = useRef<HTMLDivElement>(null);
239
241
  const innerRef = useRef<HTMLDivElement>(null);
240
242
  const [isLoadingTop, startTransition] = useTransition();
243
+ const t = useTranslator();
244
+ // The top loading Spinner sits inside the log and is silent; the list
245
+ // announces its loading through the channel (spec:AST-086 FR4, FR8).
246
+ useBusyAnnouncement(isLoadingTop, t('@astryx.spinner.loading'), innerRef);
241
247
 
242
248
  // Register inner content element with the layout for height observation
243
249
  useEffect(() => {
@@ -309,7 +315,7 @@ export function ChatMessageList({
309
315
  {/* Loading spinner at top */}
310
316
  {isLoadingTop && (
311
317
  <div {...stylex.props(styles.loadingTop)}>
312
- <Spinner size="md" />
318
+ <Spinner size="md" aria-hidden="true" />
313
319
  </div>
314
320
  )}
315
321
 
@@ -42,7 +42,8 @@ interface Case {
42
42
  direction?: 'ltr' | 'rtl';
43
43
  coarsePointer?: boolean;
44
44
  requiresOverflow?: boolean;
45
- statusName?: string;
45
+ /** The top loader is shown, silent inside the log (spec:AST-086 FR4, FR8). */
46
+ hiddenLoader?: boolean;
46
47
  }
47
48
 
48
49
  // Expectations are authored from the public fixture and component source, not
@@ -153,7 +154,7 @@ const CASES: Case[] = [
153
154
  density: 'balanced',
154
155
  busy: false,
155
156
  text: 'Earlier message remains readable',
156
- statusName: 'Loading',
157
+ hiddenLoader: true,
157
158
  },
158
159
  ];
159
160
 
@@ -228,8 +229,10 @@ async function capture(page: Page, scenario: Case, mode: 'light' | 'dark') {
228
229
  );
229
230
  const logs = page.getByRole('log');
230
231
  await logs.first().waitFor();
231
- if (scenario.statusName) {
232
- await page.getByRole('status', {name: scenario.statusName}).waitFor();
232
+ if (scenario.hiddenLoader) {
233
+ await page
234
+ .locator('[role="log"] .astryx-spinner[aria-hidden="true"]')
235
+ .waitFor();
233
236
  }
234
237
  await holdMotionStill(page);
235
238
  await page.evaluate(async () => document.fonts.ready);
@@ -260,17 +263,21 @@ async function capture(page: Page, scenario: Case, mode: 'light' | 'dark') {
260
263
  parentClientHeight: node.parentElement?.clientHeight ?? 0,
261
264
  }
262
265
  : {}),
263
- ...(measures.status
266
+ ...(measures.loader
264
267
  ? {
265
- statusName: node
266
- .querySelector('[role="status"]')
267
- ?.getAttribute('aria-label'),
268
+ hiddenLoader:
269
+ node
270
+ .querySelector('.astryx-spinner')
271
+ ?.closest('[aria-hidden="true"]') != null,
272
+ exposedStatus: [...node.querySelectorAll('[role="status"]')].some(
273
+ status => status.closest('[aria-hidden="true"]') == null,
274
+ ),
268
275
  }
269
276
  : {}),
270
277
  }),
271
278
  {
272
279
  overflow: Boolean(scenario.requiresOverflow),
273
- status: Boolean(scenario.statusName),
280
+ loader: Boolean(scenario.hiddenLoader),
274
281
  },
275
282
  );
276
283
  const environment = await page.evaluate(() => ({
@@ -323,8 +330,9 @@ async function capture(page: Page, scenario: Case, mode: 'light' | 'dark') {
323
330
  if (scenario.text != null) {
324
331
  expect(observed.text).toContain(scenario.text);
325
332
  }
326
- if (scenario.statusName != null) {
327
- expect(observed.statusName).toBe(scenario.statusName);
333
+ if (scenario.hiddenLoader) {
334
+ expect(observed.hiddenLoader).toBe(true);
335
+ expect(observed.exposedStatus).toBe(false);
328
336
  }
329
337
  expect(observed.width).toBeGreaterThan(0);
330
338
  expect(observed.height).toBeGreaterThan(0);
@@ -371,7 +379,7 @@ async function capture(page: Page, scenario: Case, mode: 'light' | 'dark') {
371
379
  text: scenario.text,
372
380
  coarsePointer: scenario.coarsePointer ?? false,
373
381
  ...(scenario.requiresOverflow ? {requiresOverflow: true} : {}),
374
- ...(scenario.statusName ? {statusName: scenario.statusName} : {}),
382
+ ...(scenario.hiddenLoader ? {hiddenLoader: true} : {}),
375
383
  },
376
384
  observed: {
377
385
  build: storybookSha,
@@ -46,7 +46,9 @@ import {useTooltip} from '../Tooltip';
46
46
  import {mergeProps, rtlStyles} from '../utils';
47
47
  import {indicatorScope} from '../Indicator/indicator.markers.stylex';
48
48
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
49
+ import {useTranslator} from '../i18n';
49
50
  import {useIndicatorFocusRing} from '../hooks/useIndicatorFocusRing';
51
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
50
52
  import {usePressFeedback} from '../hooks/usePressFeedback';
51
53
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
52
54
  import {useIndicator} from '../Indicator';
@@ -371,6 +373,10 @@ export function CheckboxInput({
371
373
  // `opacity: 0`, and only the indicator's own element can shape the outline
372
374
  // to match it. See useIndicatorFocusRing.
373
375
  const indicatorRef = useRef<HTMLSpanElement>(null);
376
+ const t = useTranslator();
377
+ // The busy Spinner sits in the decorative, aria-hidden indicator, so this
378
+ // control announces its busy start itself (spec:AST-086 FR8).
379
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), indicatorRef);
374
380
  const {focusProps} = useIndicatorFocusRing(indicatorRef);
375
381
 
376
382
  const isIndeterminate = optimisticValue === 'indeterminate';
@@ -65,6 +65,7 @@ import {useCalendarConstraints} from '../Calendar/hooks';
65
65
  import {usePopover} from '../Popover';
66
66
  import {useTooltip} from '../Tooltip';
67
67
  import {useInputContainer} from '../hooks/useInputContainer';
68
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
68
69
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
69
70
  import {
70
71
  type ISOTimeString,
@@ -709,6 +710,9 @@ function PointerDateTimeField({
709
710
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
710
711
  const isPendingChange = optimisticValue !== value;
711
712
  const isBusy = isLoading || isPendingChange;
713
+ // The busy Spinner is decorative; this control announces its busy start
714
+ // (spec:AST-086 FR8).
715
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), dateInputRef);
712
716
  // A native wheel emits several edits while it remains open. Disabling the
713
717
  // control after the first optimistic edit detaches the iOS picker, so pending
714
718
  // changeAction work stays interactive just like TouchDateTimeField. Explicit
@@ -1801,7 +1805,7 @@ function PointerDateTimeField({
1801
1805
  onClick={handleClear}
1802
1806
  />
1803
1807
  )}
1804
- {isBusy && <Spinner size="sm" />}
1808
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
1805
1809
  {statusIcon}
1806
1810
  </div>
1807
1811
 
@@ -47,6 +47,7 @@ import {
47
47
  } from '../Field';
48
48
  import {useInputStatusIcon, useMergedRefs} from '../hooks';
49
49
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
50
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
50
51
  import {Icon} from '../Icon';
51
52
  import {IconButton} from '../IconButton';
52
53
  import {useTranslator, InternationalizationContext} from '../i18n';
@@ -576,6 +577,15 @@ export function TouchDateTimeField({
576
577
  });
577
578
 
578
579
  const [isSheetOpen, setIsSheetOpen] = useState(false);
580
+ const sheetBodyRef = useRef<HTMLDivElement | null>(null);
581
+ // The busy Spinner is decorative; this control announces its busy start
582
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
583
+ // the sheet is open, so it is announced from inside the sheet.
584
+ useBusyAnnouncement(
585
+ isBusy,
586
+ t('@astryx.spinner.loading'),
587
+ isSheetOpen ? sheetBodyRef : dateInputRef,
588
+ );
579
589
  const [activePanel, setActivePanel] = useState<TouchDateTimePanel>('date');
580
590
  const [isWheelOpen, setIsWheelOpen] = useState(false);
581
591
  const [draftTime, setDraftTime] = useState<ISOTimeString | undefined>(() =>
@@ -1412,7 +1422,7 @@ export function TouchDateTimeField({
1412
1422
  onClick={handleClear}
1413
1423
  />
1414
1424
  )}
1415
- {isBusy && <Spinner size="sm" />}
1425
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
1416
1426
  {isRenderable(statusIcon) && statusIcon}
1417
1427
  </div>
1418
1428
 
@@ -1497,7 +1507,9 @@ export function TouchDateTimeField({
1497
1507
  // touchSheetBody owns the inset, including the band under the grab
1498
1508
  // handle.
1499
1509
  padding={0}>
1500
- <div {...stylex.props(styles.touchSheetBody)}>{surface}</div>
1510
+ <div ref={sheetBodyRef} {...stylex.props(styles.touchSheetBody)}>
1511
+ {surface}
1512
+ </div>
1501
1513
  </BottomSheet>
1502
1514
  {showsDisabledMessage &&
1503
1515
  disabledMessageTooltip.renderTooltip(disabledMessage)}
@@ -72,6 +72,7 @@ import {Item} from '../Item';
72
72
  import {useLayer} from '../Layer/useLayer';
73
73
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
74
74
  import {useListFocus} from '../hooks/useListFocus';
75
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
75
76
  import {useMenuHover} from '../hooks/useMenuHover';
76
77
  import {
77
78
  isMenuPressActivation,
@@ -682,6 +683,14 @@ export function DropdownMenuSubMenu(
682
683
  // closed from elsewhere is cancelled instead of firing into the hidden
683
684
  // menu (#6893).
684
685
  const isRootMenuOpen = useDropdownMenuRootOpen();
686
+ // The loading Spinner on the trigger is decorative; the submenu announces
687
+ // its busy start instead (spec:AST-086 FR8), once its level is shown: a
688
+ // closed menu stays mounted but hidden, and announces nothing.
689
+ useBusyAnnouncement(
690
+ hasSpinner && isRootMenuOpen,
691
+ t('@astryx.spinner.loading'),
692
+ triggerRef,
693
+ );
685
694
  const {triggerProps, contentProps, confirmHoverOpen} =
686
695
  useMenuHover<HTMLDivElement>({
687
696
  show: showLayer,
@@ -1033,7 +1042,7 @@ export function DropdownMenuSubMenu(
1033
1042
 
1034
1043
  const endAffordance = hasSpinner ? (
1035
1044
  <span {...stylex.props(triggerStyles.caret)}>
1036
- <Spinner size="sm" />
1045
+ <Spinner size="sm" aria-hidden="true" />
1037
1046
  </span>
1038
1047
  ) : (
1039
1048
  <span {...stylex.props(triggerStyles.caret)}>
@@ -38,7 +38,12 @@ describe('Thumbnail', () => {
38
38
  );
39
39
  const img = screen.getByRole('img');
40
40
  expect(img).toHaveAttribute('src', '/local.jpg');
41
- expect(screen.getByRole('status')).toBeInTheDocument();
41
+ // The upload Spinner is decorative; the thumbnail announces its busy
42
+ // start instead (spec:AST-086 FR8).
43
+ expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
44
+ 'aria-hidden',
45
+ 'true',
46
+ );
42
47
  });
43
48
 
44
49
  it('exposes the label as an accessible name via a valid group role', () => {
@@ -25,7 +25,7 @@
25
25
  * - /packages/cli/assets/templates/blocks/components/Thumbnail/ (showcase blocks)
26
26
  */
27
27
 
28
- import {useState} from 'react';
28
+ import {useRef, useState} from 'react';
29
29
  import * as stylex from '@stylexjs/stylex';
30
30
  import {
31
31
  colorVars,
@@ -40,6 +40,7 @@ import {Skeleton} from '../Skeleton';
40
40
  import {Spinner} from '../Spinner';
41
41
  import {Tooltip} from '../Tooltip/Tooltip';
42
42
  import {useDevWarning} from '../hooks/useDevWarning';
43
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
43
44
  import {useContainerReveal} from '../hooks/useContainerReveal';
44
45
  import type {BaseProps} from '../BaseProps';
45
46
  import {mergeProps} from '../utils';
@@ -352,6 +353,14 @@ export function Thumbnail({
352
353
  const showSkeleton = isLoading && !hasSrc;
353
354
  const showImage = hasSrc && !showSkeleton && !hasError;
354
355
  const showUploadOverlay = isLoading && hasSrc;
356
+ const uploadOverlayRef = useRef<HTMLDivElement>(null);
357
+ // The upload Spinner is decorative; the thumbnail announces its busy start
358
+ // instead (spec:AST-086 FR8).
359
+ useBusyAnnouncement(
360
+ showUploadOverlay,
361
+ t('@astryx.spinner.loading'),
362
+ uploadOverlayRef,
363
+ );
355
364
  const showPlaceholder = (!isLoading && !hasSrc) || hasError;
356
365
  const isInteractive = onClick != null && !isDisabled && !isLoading;
357
366
  const hasRemove = onRemove != null && !isDisabled;
@@ -472,8 +481,8 @@ export function Thumbnail({
472
481
  )}
473
482
  {showImage && <div {...stylex.props(styles.insetBorder)} />}
474
483
  {showUploadOverlay && (
475
- <div {...stylex.props(styles.uploadOverlay)}>
476
- <Spinner size="sm" shade="onMedia" />
484
+ <div ref={uploadOverlayRef} {...stylex.props(styles.uploadOverlay)}>
485
+ <Spinner size="sm" shade="onMedia" aria-hidden="true" />
477
486
  </div>
478
487
  )}
479
488
  {removeButtonEl}
@@ -70,6 +70,7 @@ import {
70
70
  import type {SizeValue} from '../utils/types';
71
71
  import {useSize} from '../SizeContext/SizeContext';
72
72
  import {useAnnounce} from '../hooks/useAnnounce';
73
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
73
74
  import {useInputContainer} from '../hooks/useInputContainer';
74
75
  import {useMediaQuery} from '../hooks/useMediaQuery';
75
76
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
@@ -475,6 +476,9 @@ function TimeField({
475
476
  const [, startTransition] = useTransition();
476
477
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
477
478
  const isBusy = isLoading || optimisticValue !== value;
479
+ // The busy Spinner is decorative; this control announces its busy start
480
+ // (spec:AST-086 FR8).
481
+ useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), containerRef);
478
482
 
479
483
  // In grouped mode the status message renders as a visually-hidden node that
480
484
  // exists only for aria-describedby. Announce it through the persistent
@@ -865,7 +869,7 @@ function TimeField({
865
869
  </VisuallyHidden>
866
870
  </>
867
871
  )}
868
- {isBusy && <Spinner size="sm" />}
872
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
869
873
  {hasClear && optimisticValue && !isDisabled && (
870
874
  <InputClearButton
871
875
  label={t('@astryx.timeInput.clearLabel', {label})}
@@ -42,6 +42,7 @@ import {useFieldInputStyling} from '../Field/fieldParts';
42
42
  import {joinAriaIDs} from '../utils/inputAria';
43
43
  import {groupStyles} from '../InputGroup/groupStyles';
44
44
  import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
45
+ import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
45
46
  import {useMergedRefs} from '../hooks/useMergedRefs';
46
47
  import {useResolvedRequired} from '../hooks/useResolvedRequired';
47
48
  import {Icon} from '../Icon';
@@ -254,6 +255,15 @@ export function TouchTimeField({
254
255
  const [, startTransition] = useTransition();
255
256
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
256
257
  const isBusy = isLoading || optimisticValue !== value;
258
+ const sheetBodyRef = useRef<HTMLDivElement | null>(null);
259
+ // The busy Spinner is decorative; this control announces its busy start
260
+ // (spec:AST-086 FR8). A change made in the open modal sheet starts it while
261
+ // the sheet is open, so it is announced from inside the sheet.
262
+ useBusyAnnouncement(
263
+ isBusy,
264
+ t('@astryx.spinner.loading'),
265
+ isSheetOpen ? sheetBodyRef : inputRef,
266
+ );
257
267
  const isEffectivelyDisabled = isDisabled || isLoading;
258
268
 
259
269
  const showsDisabledMessage = isDisabled && !!disabledMessage;
@@ -516,7 +526,7 @@ export function TouchTimeField({
516
526
  }
517
527
  {...stylex.props(styles.input)}
518
528
  />
519
- {isBusy && <Spinner size="sm" />}
529
+ {isBusy && <Spinner size="sm" aria-hidden="true" />}
520
530
  {hasClear && optimisticValue && !isEffectivelyDisabled && (
521
531
  <InputClearButton
522
532
  label={t('@astryx.timeInput.clearLabel', {label})}
@@ -531,7 +541,7 @@ export function TouchTimeField({
531
541
  height="hug"
532
542
  // sheetBody owns the inset, including the band under the grab handle.
533
543
  padding={0}>
534
- <div {...stylex.props(styles.sheetBody)}>
544
+ <div ref={sheetBodyRef} {...stylex.props(styles.sheetBody)}>
535
545
  <div {...stylex.props(styles.wheels)}>
536
546
  <Wheel
537
547
  label={t('@astryx.dateTimeInput.hourWheel')}
@@ -19,7 +19,7 @@ import {
19
19
  it,
20
20
  vi,
21
21
  } from 'vitest';
22
- import {act, fireEvent, render, screen} from '@testing-library/react';
22
+ import {act, fireEvent, render, screen, within} from '@testing-library/react';
23
23
  import {TextInput} from '../TextInput';
24
24
  import {TextArea} from '../TextArea';
25
25
  import {Selector} from '../Selector';
@@ -27,8 +27,19 @@ import {MultiSelector} from '../MultiSelector';
27
27
  import {ComplexSelector} from '../ComplexSelector';
28
28
  import {DateInput} from '../DateInput';
29
29
  import {DateRangeInput} from '../DateRangeInput';
30
+ import {DateTimeInput} from '../DateTimeInput';
31
+ import {TimeInput} from '../TimeInput';
32
+ import {CheckboxInput} from '../CheckboxInput';
33
+ import {Thumbnail} from '../Thumbnail';
34
+ import {
35
+ DropdownMenu,
36
+ DropdownMenuItem,
37
+ DropdownMenuSubMenu,
38
+ } from '../DropdownMenu';
39
+ import {ChatMessageList} from '../Chat';
30
40
  import {BaseTypeahead, Typeahead} from '../Typeahead';
31
41
  import type {SearchableItem, SearchSource} from '../Typeahead';
42
+ import type {ISOTimeString} from '../utils';
32
43
  import {Tokenizer} from '../Tokenizer';
33
44
 
34
45
  const announce = vi.fn();
@@ -104,6 +115,47 @@ const OWNERS: ReadonlyArray<
104
115
  ),
105
116
  ] as const,
106
117
  ),
118
+ ...(['popover', 'native', 'bottom-sheet'] as const).flatMap(presentation => [
119
+ [
120
+ `DateTimeInput (${presentation})`,
121
+ (isLoading: boolean) => (
122
+ <DateTimeInput
123
+ label="When"
124
+ presentation={presentation}
125
+ onChange={() => {}}
126
+ isLoading={isLoading}
127
+ />
128
+ ),
129
+ ] as const,
130
+ [
131
+ `TimeInput (${presentation})`,
132
+ (isLoading: boolean) => (
133
+ <TimeInput
134
+ label="Time"
135
+ presentation={presentation}
136
+ onChange={() => {}}
137
+ isLoading={isLoading}
138
+ />
139
+ ),
140
+ ] as const,
141
+ ]),
142
+ [
143
+ 'CheckboxInput',
144
+ isLoading => (
145
+ <CheckboxInput
146
+ label="Subscribe"
147
+ value={false}
148
+ onChange={() => {}}
149
+ isLoading={isLoading}
150
+ />
151
+ ),
152
+ ],
153
+ [
154
+ 'Thumbnail (uploading)',
155
+ isLoading => (
156
+ <Thumbnail src="/local.jpg" alt="Upload" isLoading={isLoading} />
157
+ ),
158
+ ],
107
159
  [
108
160
  'DateRangeInput',
109
161
  isLoading => (
@@ -215,7 +267,8 @@ describe.each(OWNERS)('%s busy state (spec:AST-086)', (_name, renderOwner) => {
215
267
  const {container} = render(renderOwner(true));
216
268
  const spinner = container.querySelector('.astryx-spinner');
217
269
  expect(spinner).not.toBeNull();
218
- expect(spinner).toHaveAttribute('aria-hidden', 'true');
270
+ // Hidden itself, or inside a decorative hidden indicator (CheckboxInput).
271
+ expect(spinner!.closest('[aria-hidden="true"]')).not.toBeNull();
219
272
  expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
220
273
  });
221
274
 
@@ -367,3 +420,157 @@ describe.each(SEARCH_OWNERS)(
367
420
  });
368
421
  },
369
422
  );
423
+
424
+ /**
425
+ * Bottom-sheet fields whose sheet stays open while a changeAction is pending:
426
+ * the sheet is a modal dialog, so the busy start must be announced inside it.
427
+ */
428
+ const SHEET_OWNERS: ReadonlyArray<
429
+ readonly [
430
+ string,
431
+ (changeAction: () => Promise<void>) => ReactElement,
432
+ () => void,
433
+ ]
434
+ > = [
435
+ [
436
+ 'TimeInput',
437
+ changeAction => (
438
+ <TimeInput
439
+ label="Time"
440
+ presentation="bottom-sheet"
441
+ value={'09:00' as ISOTimeString}
442
+ onChange={() => {}}
443
+ changeAction={changeAction}
444
+ />
445
+ ),
446
+ () => {
447
+ fireEvent.click(screen.getByRole('button', {name: /Time/}));
448
+ },
449
+ ],
450
+ ];
451
+
452
+ describe.each(SHEET_OWNERS)(
453
+ '%s busy start from inside its open sheet (spec:AST-086 Platform support)',
454
+ (_name, renderOwner, openSheet) => {
455
+ beforeEach(() => {
456
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
457
+ this: HTMLDialogElement,
458
+ ) {
459
+ this.setAttribute('open', '');
460
+ });
461
+ HTMLDialogElement.prototype.close = vi.fn(function (
462
+ this: HTMLDialogElement,
463
+ ) {
464
+ this.removeAttribute('open');
465
+ });
466
+ const matches = Element.prototype.matches;
467
+ vi.spyOn(Element.prototype, 'matches').mockImplementation(function (
468
+ this: Element,
469
+ selector: string,
470
+ ) {
471
+ return selector === ':modal'
472
+ ? this instanceof HTMLDialogElement && this.open
473
+ : matches.call(this, selector);
474
+ });
475
+ });
476
+ afterEach(() => {
477
+ vi.restoreAllMocks();
478
+ });
479
+
480
+ it('announces the busy start in a region inside the open sheet, not behind it', async () => {
481
+ render(renderOwner(async () => new Promise<void>(() => {})));
482
+ openSheet();
483
+ const sheet = document.querySelector('dialog[open]');
484
+ expect(sheet).not.toBeNull();
485
+ act(() => {
486
+ fireEvent.click(within(sheet as HTMLElement).getAllByRole('option')[1]);
487
+ });
488
+ await nextFrame();
489
+ await nextFrame();
490
+ expect(
491
+ sheet!.querySelector('[data-astryx-modal-busy-region]'),
492
+ ).toHaveTextContent('Loading');
493
+ expect(busyAnnouncements()).toEqual([]);
494
+ });
495
+ },
496
+ );
497
+
498
+ describe('DropdownMenuSubMenu loading state (spec:AST-086 FR8)', () => {
499
+ function Menu({isLoading}: {isLoading: boolean}) {
500
+ return (
501
+ <DropdownMenu button={{label: 'Actions'}}>
502
+ <DropdownMenuSubMenu label="Move to" hasSpinner={isLoading}>
503
+ <DropdownMenuItem label="Inbox" onClick={() => {}} />
504
+ </DropdownMenuSubMenu>
505
+ </DropdownMenu>
506
+ );
507
+ }
508
+
509
+ it('announces nothing while its menu is closed', async () => {
510
+ render(strict(<Menu isLoading />));
511
+ await nextFrame();
512
+ expect(busyAnnouncements()).toEqual([]);
513
+ });
514
+
515
+ it('announces the busy start when its menu opens while loading', async () => {
516
+ render(strict(<Menu isLoading />));
517
+ fireEvent.click(screen.getByRole('button', {name: /Actions/}));
518
+ await nextFrame();
519
+ expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
520
+ });
521
+
522
+ it('keeps the trigger Spinner silent and announces each busy start once', async () => {
523
+ const {rerender} = render(strict(<Menu isLoading={false} />));
524
+ fireEvent.click(screen.getByRole('button', {name: /Actions/}));
525
+ await nextFrame();
526
+ expect(busyAnnouncements()).toEqual([]);
527
+ rerender(strict(<Menu isLoading />));
528
+ await nextFrame();
529
+ const spinner = document.querySelector('.astryx-spinner');
530
+ expect(spinner).toHaveAttribute('aria-hidden', 'true');
531
+ expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
532
+ rerender(strict(<Menu isLoading={false} />));
533
+ rerender(strict(<Menu isLoading />));
534
+ await nextFrame();
535
+ expect(busyAnnouncements()).toEqual([
536
+ ['Loading', 'polite'],
537
+ ['Loading', 'polite'],
538
+ ]);
539
+ });
540
+ });
541
+
542
+ describe('ChatMessageList top loader (spec:AST-086 FR4, FR8)', () => {
543
+ it('keeps the loader inside the log silent and announces loading once through the channel', async () => {
544
+ let notify!: IntersectionObserverCallback;
545
+ vi.stubGlobal(
546
+ 'IntersectionObserver',
547
+ class {
548
+ constructor(callback: IntersectionObserverCallback) {
549
+ notify = callback;
550
+ }
551
+ observe() {}
552
+ disconnect() {}
553
+ },
554
+ );
555
+ render(
556
+ strict(
557
+ <ChatMessageList
558
+ scrollToTopAction={async () => new Promise<void>(() => {})}>
559
+ <div>Earlier message</div>
560
+ </ChatMessageList>,
561
+ ),
562
+ );
563
+ act(() => {
564
+ notify(
565
+ [{isIntersecting: true} as IntersectionObserverEntry],
566
+ {} as IntersectionObserver,
567
+ );
568
+ });
569
+ await nextFrame();
570
+ expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
571
+ 'aria-hidden',
572
+ 'true',
573
+ );
574
+ expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
575
+ });
576
+ });
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
- import {renderHook} from '@testing-library/react';
13
+ import {Activity, type ReactNode} from 'react';
14
+ import {render, renderHook} from '@testing-library/react';
14
15
  import {useTypeahead} from './useTypeahead';
15
16
 
16
17
  const LABELS = ['Apple', 'Apricot', 'Banana', 'Cherry'] as const;
@@ -86,6 +87,46 @@ describe('useTypeahead', () => {
86
87
  expect(onMatch).toHaveBeenLastCalledWith(2);
87
88
  });
88
89
 
90
+ it('clears the pending buffer reset when the component unmounts', () => {
91
+ const {result, unmount} = renderHook(() =>
92
+ useTypeahead({getItemLabels: () => LABELS, onMatch: vi.fn()}),
93
+ );
94
+ result.current.onKeyDown(key('a'));
95
+ expect(vi.getTimerCount()).toBe(1);
96
+ unmount();
97
+ expect(vi.getTimerCount()).toBe(0);
98
+ });
99
+
100
+ it('empties the buffer when an Activity boundary hides the collection', () => {
101
+ const latest: {api: ReturnType<typeof useTypeahead> | null} = {api: null};
102
+ function Probe(): ReactNode {
103
+ latest.api = useTypeahead({
104
+ getItemLabels: () => LABELS,
105
+ onMatch: vi.fn(),
106
+ });
107
+ return null;
108
+ }
109
+ const {rerender} = render(
110
+ <Activity mode="visible">
111
+ <Probe />
112
+ </Activity>,
113
+ );
114
+ latest.api?.onKeyDown(key('a'));
115
+ rerender(
116
+ <Activity mode="hidden">
117
+ <Probe />
118
+ </Activity>,
119
+ );
120
+ expect(vi.getTimerCount()).toBe(0);
121
+ rerender(
122
+ <Activity mode="visible">
123
+ <Probe />
124
+ </Activity>,
125
+ );
126
+ // The typed prefix went with the hide, so a bare Space is not typeahead.
127
+ expect(latest.api?.onKeyDown(key(' '))).toBe(false);
128
+ });
129
+
89
130
  it('skips disabled items', () => {
90
131
  const {onMatch, api} = setup({disabledIndices: [0]}); // Apple disabled
91
132
  api.onKeyDown(key('a'));
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTypeahead.ts
7
- * @input Uses React useCallback/useRef
7
+ * @input Uses React useCallback/useEffect/useRef
8
8
  * @output Exports useTypeahead hook for first-character (type-to-focus) search
9
9
  * @position Core hook; adds APG typeahead to menus, listboxes, and other
10
10
  * collections. Composes with useListFocus/useGridFocus (or any collection)
@@ -14,7 +14,7 @@
14
14
  * - /packages/core/src/hooks/index.ts
15
15
  */
16
16
 
17
- import {useCallback, useRef} from 'react';
17
+ import {useCallback, useEffect, useRef} from 'react';
18
18
 
19
19
  export interface UseTypeaheadOptions {
20
20
  /**
@@ -135,6 +135,11 @@ export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn {
135
135
  }
136
136
  }, []);
137
137
 
138
+ // A collection that unmounts, or that an `<Activity>` boundary hides,
139
+ // mid-typing leaves no reset timer running and comes back with an empty
140
+ // buffer, as the reset would have left it.
141
+ useEffect(() => reset, [reset]);
142
+
138
143
  const scheduleReset = useCallback(() => {
139
144
  if (timeoutRef.current) {
140
145
  clearTimeout(timeoutRef.current);