@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.
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +8 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +7 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +9 -2
- package/package.json +3 -3
- package/src/Chat/ChatMessageList.test.tsx +6 -2
- package/src/Chat/ChatMessageList.tsx +7 -1
- package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +20 -12
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
- package/src/Thumbnail/Thumbnail.test.tsx +6 -1
- package/src/Thumbnail/Thumbnail.tsx +12 -3
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +107 -1
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"ChatMessageList.d.ts","sourceRoot":"","sources":["../../src/Chat/ChatMessageList.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,KAAK,SAAS,EAA4C,MAAM,OAAO,CAAC;AAGhF,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAKhD,MAAM,WAAW,oBAAqB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACrE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAEzB;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4FD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,UAAU,EACV,iBAAiB,EACjB,OAAoB,EACpB,GAAG,EACH,KAAgB,EAChB,WAAmB,EACnB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,GAAG,IAAI,EACR,EAAE,oBAAoB,+BAoGtB;yBAlHe,eAAe"}
|
|
@@ -31,6 +31,8 @@ import { ChatListContext, useChatLayoutContext } from "./ChatContext.js";
|
|
|
31
31
|
import { isRenderable, mergeProps } from "../utils/index.js";
|
|
32
32
|
import { Spinner } from "../Spinner/index.js";
|
|
33
33
|
import { themeProps } from "../utils/themeProps.js";
|
|
34
|
+
import { useTranslator } from "../i18n/index.js";
|
|
35
|
+
import { useBusyAnnouncement } from "../hooks/useBusyAnnouncement.js";
|
|
34
36
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
35
37
|
// =============================================================================
|
|
36
38
|
// Styles
|
|
@@ -161,6 +163,10 @@ export function ChatMessageList({
|
|
|
161
163
|
const sentinelRef = useRef(null);
|
|
162
164
|
const innerRef = useRef(null);
|
|
163
165
|
const [isLoadingTop, startTransition] = useTransition();
|
|
166
|
+
const t = useTranslator();
|
|
167
|
+
// The top loading Spinner sits inside the log and is silent; the list
|
|
168
|
+
// announces its loading through the channel (spec:AST-086 FR4, FR8).
|
|
169
|
+
useBusyAnnouncement(isLoadingTop, t('@astryx.spinner.loading'), innerRef);
|
|
164
170
|
|
|
165
171
|
// Register inner content element with the layout for height observation
|
|
166
172
|
useEffect(() => {
|
|
@@ -219,7 +225,8 @@ export function ChatMessageList({
|
|
|
219
225
|
className: "x78zum5 xl56j7k x8o8v82"
|
|
220
226
|
},
|
|
221
227
|
children: /*#__PURE__*/_jsx(Spinner, {
|
|
222
|
-
size: "md"
|
|
228
|
+
size: "md",
|
|
229
|
+
"aria-hidden": "true"
|
|
223
230
|
})
|
|
224
231
|
}), align === 'bottom' && /*#__PURE__*/_jsx("div", {
|
|
225
232
|
...{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CheckboxInput.d.ts","sourceRoot":"","sources":["../../src/CheckboxInput/CheckboxInput.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAS3C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAG9C,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AACtC,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;
|
|
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;
|
|
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":"
|
|
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.
|
|
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.
|
|
796
|
-
"@astryxdesign/cli": "0.7.4-canary.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
232
|
-
await page
|
|
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.
|
|
266
|
+
...(measures.loader
|
|
264
267
|
? {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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.
|
|
327
|
-
expect(observed.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|