@astryxdesign/core 0.7.4-canary.f32572e → 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.
@@ -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":"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
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.7.4-canary.f32572e",
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.f32572e",
796
- "@astryxdesign/cli": "0.7.4-canary.f32572e",
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';
@@ -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}
@@ -29,6 +29,14 @@ import {DateInput} from '../DateInput';
29
29
  import {DateRangeInput} from '../DateRangeInput';
30
30
  import {DateTimeInput} from '../DateTimeInput';
31
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';
32
40
  import {BaseTypeahead, Typeahead} from '../Typeahead';
33
41
  import type {SearchableItem, SearchSource} from '../Typeahead';
34
42
  import type {ISOTimeString} from '../utils';
@@ -131,6 +139,23 @@ const OWNERS: ReadonlyArray<
131
139
  ),
132
140
  ] as const,
133
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
+ ],
134
159
  [
135
160
  'DateRangeInput',
136
161
  isLoading => (
@@ -242,7 +267,8 @@ describe.each(OWNERS)('%s busy state (spec:AST-086)', (_name, renderOwner) => {
242
267
  const {container} = render(renderOwner(true));
243
268
  const spinner = container.querySelector('.astryx-spinner');
244
269
  expect(spinner).not.toBeNull();
245
- 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();
246
272
  expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
247
273
  });
248
274
 
@@ -468,3 +494,83 @@ describe.each(SHEET_OWNERS)(
468
494
  });
469
495
  },
470
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
+ });