@astryxdesign/core 0.7.3 → 0.7.4-canary.1a552f2
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/BottomSheet/BottomSheet.d.ts +18 -6
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +268 -58
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +41 -12
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +294 -165
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +51 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -1
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +7 -11
- package/dist/Chat/ChatMessageList.d.ts.map +1 -1
- package/dist/Chat/ChatMessageList.js +8 -1
- package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
- package/dist/Chat/useTriggerMenu.js +46 -7
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +6 -0
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +6 -11
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +7 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +12 -0
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +6 -1
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +6 -1
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +9 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +6 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +6 -1
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +9 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +7 -1
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +16 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +6 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +6 -1
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +9 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +9 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +9 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +7 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +9 -1
- package/dist/astryx.css +2 -0
- package/dist/hooks/useBusyAnnouncement.d.ts +22 -0
- package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -0
- package/dist/hooks/useBusyAnnouncement.js +87 -0
- package/dist/hooks/useMenuPress.d.ts +2 -2
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +5 -1
- package/dist/hooks/useTypeahead.d.ts.map +1 -1
- package/dist/hooks/useTypeahead.js +7 -2
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +1 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
- package/locales/en.json +6 -2
- package/locales/pseudo.json +3 -0
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.doc.mjs +2 -2
- package/src/BottomSheet/BottomSheet.spec.md +3 -5
- package/src/BottomSheet/BottomSheet.tsx +260 -47
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +29 -4
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +11 -10
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +35 -41
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +1051 -49
- package/src/BottomSheet/BottomSheetSwitcher.tsx +396 -215
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +57 -2
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
- package/src/Carousel/Carousel.test.tsx +8 -12
- package/src/Carousel/Carousel.tsx +6 -13
- package/src/Chat/ChatComposerInput.test.tsx +167 -1
- 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/Chat/useTriggerMenu.tsx +55 -6
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/CommandPalette/CommandPalette.test.tsx +4 -2
- package/src/CommandPalette/CommandPalette.tsx +6 -11
- package/src/CommandPalette/CommandPaletteInput.test.tsx +3 -1
- package/src/CommandPalette/CommandPaletteInput.tsx +10 -1
- package/src/ComplexSelector/ComplexSelector.tsx +5 -1
- package/src/ContextMenu/ContextMenu.test.tsx +34 -0
- package/src/ContextMenu/ContextMenu.tsx +13 -1
- package/src/DateInput/DateInput.tsx +5 -1
- package/src/DateInput/DateInputTouch.test.tsx +45 -0
- package/src/DateInput/NativeDateField.test.tsx +10 -2
- package/src/DateInput/NativeDateField.tsx +5 -1
- package/src/DateInput/TouchDateField.tsx +14 -2
- package/src/DateRangeInput/DateRangeInput.tsx +5 -1
- package/src/DateTimeInput/DateTimeInput.tsx +5 -1
- package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
- package/src/FileInput/FileInput.tsx +18 -2
- package/src/MultiSelector/MultiSelector.test.tsx +18 -4
- package/src/MultiSelector/MultiSelector.tsx +5 -1
- package/src/ProgressBar/ProgressBar.test.tsx +4 -21
- package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +114 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +12 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +38 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +123 -0
- package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +107 -0
- package/src/Selector/Selector.test.tsx +22 -5
- package/src/Selector/Selector.tsx +5 -1
- package/src/TextArea/TextArea.test.tsx +9 -1
- package/src/TextArea/TextArea.tsx +5 -1
- package/src/TextInput/TextInput.tsx +5 -1
- package/src/Thumbnail/Thumbnail.test.tsx +6 -1
- package/src/Thumbnail/Thumbnail.tsx +12 -3
- package/src/TimeInput/TimeInput.tsx +5 -1
- package/src/TimeInput/TouchTimeField.tsx +12 -2
- package/src/Tokenizer/Tokenizer.test.tsx +17 -12
- package/src/Tokenizer/Tokenizer.tsx +10 -1
- package/src/Typeahead/BaseTypeahead.tsx +14 -1
- package/src/Typeahead/Typeahead.test.tsx +32 -41
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +792 -0
- package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +50 -0
- package/src/hooks/useBusyAnnouncement.ts +100 -0
- package/src/hooks/useMenuPress.test.tsx +27 -1
- package/src/hooks/useMenuPress.ts +12 -3
- package/src/hooks/useTypeahead.test.tsx +42 -1
- package/src/hooks/useTypeahead.ts +7 -2
- package/src/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
|
@@ -666,6 +666,51 @@ describe('DateInput — field parity', () => {
|
|
|
666
666
|
resolve();
|
|
667
667
|
});
|
|
668
668
|
|
|
669
|
+
it('announces a busy start from a pick inside the open sheet, not behind it (spec:AST-086 FR8, Platform support)', async () => {
|
|
670
|
+
// The stubbed showModal() opens the dialog but jsdom cannot match
|
|
671
|
+
// `:modal`; stand in for the sheet the browser presents modally.
|
|
672
|
+
const matches = Element.prototype.matches;
|
|
673
|
+
const modalSpy = vi
|
|
674
|
+
.spyOn(Element.prototype, 'matches')
|
|
675
|
+
.mockImplementation(function (this: Element, selector: string) {
|
|
676
|
+
return selector === ':modal'
|
|
677
|
+
? this instanceof HTMLDialogElement && this.open
|
|
678
|
+
: matches.call(this, selector);
|
|
679
|
+
});
|
|
680
|
+
const changeAction = vi.fn(async () => new Promise<void>(() => {}));
|
|
681
|
+
withLayout(() => {
|
|
682
|
+
render(
|
|
683
|
+
<DateInput
|
|
684
|
+
nativePicker="never"
|
|
685
|
+
label="Ship date"
|
|
686
|
+
value="2026-03-10"
|
|
687
|
+
min="2026-03-01"
|
|
688
|
+
max="2026-03-31"
|
|
689
|
+
onChange={() => {}}
|
|
690
|
+
changeAction={changeAction}
|
|
691
|
+
/>,
|
|
692
|
+
);
|
|
693
|
+
fireEvent.click(field());
|
|
694
|
+
fireEvent.click(
|
|
695
|
+
within(pane('March 2026')).getByRole('button', {
|
|
696
|
+
name: /March 25, 2026/,
|
|
697
|
+
}),
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
const sheet = document.querySelector('dialog[open]');
|
|
701
|
+
expect(sheet).not.toBeNull();
|
|
702
|
+
await waitFor(() => {
|
|
703
|
+
expect(
|
|
704
|
+
sheet!.querySelector('[data-astryx-modal-busy-region]'),
|
|
705
|
+
).toHaveTextContent('Loading');
|
|
706
|
+
});
|
|
707
|
+
expect(
|
|
708
|
+
document.querySelector('[data-astryx-live-region="polite"]')
|
|
709
|
+
?.textContent ?? '',
|
|
710
|
+
).toBe('');
|
|
711
|
+
modalSpy.mockRestore();
|
|
712
|
+
});
|
|
713
|
+
|
|
669
714
|
it('drops the Field wrapper inside an InputGroup', () => {
|
|
670
715
|
render(
|
|
671
716
|
<InputGroup label="Range">
|
|
@@ -222,7 +222,11 @@ describe('DateInput nativePicker', () => {
|
|
|
222
222
|
expect(changeAction).toHaveBeenCalledExactlyOnceWith('2026-03-21');
|
|
223
223
|
expect(getInput()).not.toBeDisabled();
|
|
224
224
|
expect(getInput()).toHaveAttribute('aria-busy', 'true');
|
|
225
|
-
|
|
225
|
+
// The busy Spinner is decorative (spec:AST-086 FR8).
|
|
226
|
+
expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
|
|
227
|
+
'aria-hidden',
|
|
228
|
+
'true',
|
|
229
|
+
);
|
|
226
230
|
expect(screen.getByText('March 21, 2026')).toBeInTheDocument();
|
|
227
231
|
|
|
228
232
|
fireEvent.change(getInput(), {target: {value: '2026-03-22'}});
|
|
@@ -597,7 +601,11 @@ describe('DateInput nativePicker', () => {
|
|
|
597
601
|
|
|
598
602
|
expect(calls).toEqual(['onChange', 'changeAction']);
|
|
599
603
|
expect(changeAction).toHaveBeenCalledExactlyOnceWith(undefined);
|
|
600
|
-
|
|
604
|
+
// The busy Spinner is decorative (spec:AST-086 FR8).
|
|
605
|
+
expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
|
|
606
|
+
'aria-hidden',
|
|
607
|
+
'true',
|
|
608
|
+
);
|
|
601
609
|
await act(async () => resolveAction());
|
|
602
610
|
});
|
|
603
611
|
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from '../Field';
|
|
50
50
|
import {useInputStatusIcon, useMergedRefs} from '../hooks';
|
|
51
51
|
import {useMediaQuery} from '../hooks/useMediaQuery';
|
|
52
|
+
import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
|
|
52
53
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
53
54
|
import {Icon} from '../Icon';
|
|
54
55
|
import {useLocale, useTranslator} from '../i18n';
|
|
@@ -328,6 +329,9 @@ export function NativeDateField({
|
|
|
328
329
|
const [, startTransition] = useTransition();
|
|
329
330
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
330
331
|
const isBusy = isLoading || optimisticValue !== value;
|
|
332
|
+
// The busy Spinner is decorative; this control announces its busy start
|
|
333
|
+
// (spec:AST-086 FR8).
|
|
334
|
+
useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), inputRef);
|
|
331
335
|
// Keep the OS picker mounted and focusable while its own Action is pending.
|
|
332
336
|
// Native TimeInput follows the same pattern: commit guards block duplicates,
|
|
333
337
|
// while aria-busy and the Spinner communicate the save without detaching the
|
|
@@ -687,7 +691,7 @@ export function NativeDateField({
|
|
|
687
691
|
onClick={handleClear}
|
|
688
692
|
/>
|
|
689
693
|
)}
|
|
690
|
-
{isBusy && <Spinner size="sm" />}
|
|
694
|
+
{isBusy && <Spinner size="sm" aria-hidden="true" />}
|
|
691
695
|
{statusIcon}
|
|
692
696
|
{showsDisabledMessage &&
|
|
693
697
|
disabledMessageTooltip.renderTooltip(disabledMessage)}
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from '../Field';
|
|
74
74
|
import {useInputStatusIcon, useMergedRefs} from '../hooks';
|
|
75
75
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
76
|
+
import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
|
|
76
77
|
import {Icon} from '../Icon';
|
|
77
78
|
import {IconButton} from '../IconButton';
|
|
78
79
|
import {useLocale, useTranslator} from '../i18n';
|
|
@@ -665,6 +666,15 @@ export function TouchDateField({
|
|
|
665
666
|
);
|
|
666
667
|
|
|
667
668
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
669
|
+
const sheetBodyRef = useRef<HTMLDivElement | null>(null);
|
|
670
|
+
// The busy Spinner is decorative; this control announces its busy start
|
|
671
|
+
// (spec:AST-086 FR8). A date picked under a pending changeAction starts it
|
|
672
|
+
// while the modal sheet is open, so it is announced from inside the sheet.
|
|
673
|
+
useBusyAnnouncement(
|
|
674
|
+
isBusy,
|
|
675
|
+
t('@astryx.spinner.loading'),
|
|
676
|
+
isSheetOpen ? sheetBodyRef : inputRef,
|
|
677
|
+
);
|
|
668
678
|
const [isWheelOpen, setIsWheelOpen] = useState(false);
|
|
669
679
|
const scrollerHandleRef = useRef<MonthScrollerHandle | null>(null);
|
|
670
680
|
// Pending focus handoff from the clear button; see handleClear.
|
|
@@ -1303,7 +1313,7 @@ export function TouchDateField({
|
|
|
1303
1313
|
onClick={handleClear}
|
|
1304
1314
|
/>
|
|
1305
1315
|
)}
|
|
1306
|
-
{isBusy && <Spinner size="sm" />}
|
|
1316
|
+
{isBusy && <Spinner size="sm" aria-hidden="true" />}
|
|
1307
1317
|
{statusIcon}
|
|
1308
1318
|
<BottomSheet
|
|
1309
1319
|
isOpen={isSheetOpen}
|
|
@@ -1314,7 +1324,9 @@ export function TouchDateField({
|
|
|
1314
1324
|
height="hug"
|
|
1315
1325
|
// sheetBody owns the inset, including the band under the grab handle.
|
|
1316
1326
|
padding={0}>
|
|
1317
|
-
<div {...stylex.props(styles.sheetBody)}>
|
|
1327
|
+
<div ref={sheetBodyRef} {...stylex.props(styles.sheetBody)}>
|
|
1328
|
+
{surface}
|
|
1329
|
+
</div>
|
|
1318
1330
|
</BottomSheet>
|
|
1319
1331
|
{showsDisabledMessage &&
|
|
1320
1332
|
disabledMessageTooltip.renderTooltip(disabledMessage)}
|
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
import type {SizeValue} from '../utils/types';
|
|
76
76
|
import {useSize} from '../SizeContext/SizeContext';
|
|
77
77
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
78
|
+
import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
|
|
78
79
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
79
80
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
80
81
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -645,6 +646,9 @@ export function DateRangeInput({
|
|
|
645
646
|
);
|
|
646
647
|
|
|
647
648
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
649
|
+
// The busy Spinner is decorative; this control announces its busy start
|
|
650
|
+
// (spec:AST-086 FR8).
|
|
651
|
+
useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), triggerRef);
|
|
648
652
|
const triggerButtonRef = useMergedRefs<HTMLButtonElement>(triggerRef, ref);
|
|
649
653
|
|
|
650
654
|
const handleClear = useCallback(
|
|
@@ -787,7 +791,7 @@ export function DateRangeInput({
|
|
|
787
791
|
{ariaLabelProp ?? triggerAriaLabel}
|
|
788
792
|
</VisuallyHidden>
|
|
789
793
|
)}
|
|
790
|
-
{isBusy && <Spinner size="sm" />}
|
|
794
|
+
{isBusy && <Spinner size="sm" aria-hidden="true" />}
|
|
791
795
|
{statusIcon}
|
|
792
796
|
</div>
|
|
793
797
|
{isEffectivelyRequired && (
|
|
@@ -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)}>
|
|
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)}>
|
|
@@ -47,6 +47,7 @@ import {Icon} from '../Icon';
|
|
|
47
47
|
import {Spinner} from '../Spinner';
|
|
48
48
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
49
49
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
50
|
+
import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
|
|
50
51
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
51
52
|
import {useClickableContainer} from '../hooks/useClickableContainer';
|
|
52
53
|
import {useTooltip} from '../Tooltip';
|
|
@@ -490,6 +491,20 @@ export function FileInput({
|
|
|
490
491
|
const [, startTransition] = useTransition();
|
|
491
492
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
492
493
|
const isBusy = isLoading || optimisticValue !== value;
|
|
494
|
+
// A pick is one user action, already announced as "file selected"; a busy
|
|
495
|
+
// period it starts (changeAction, or a caller setting isLoading from
|
|
496
|
+
// onChange) adds no second message (spec:AST-086 FR6). The mark lasts until
|
|
497
|
+
// the field is idle again. Other busy starts announce; the Spinner stays
|
|
498
|
+
// decorative (FR8).
|
|
499
|
+
const [isPickAnnounced, setIsPickAnnounced] = useState(false);
|
|
500
|
+
if (isPickAnnounced && !isBusy) {
|
|
501
|
+
setIsPickAnnounced(false);
|
|
502
|
+
}
|
|
503
|
+
useBusyAnnouncement(
|
|
504
|
+
isBusy && !isPickAnnounced,
|
|
505
|
+
t('@astryx.spinner.loading'),
|
|
506
|
+
containerRef,
|
|
507
|
+
);
|
|
493
508
|
|
|
494
509
|
// Announce successful file selection to screen readers via a persistent
|
|
495
510
|
// live region (forms-17). Validation errors are announced (assertively) by
|
|
@@ -580,6 +595,7 @@ export function FileInput({
|
|
|
580
595
|
// status mounts, so only announce the attach here (do not
|
|
581
596
|
// double-announce errors).
|
|
582
597
|
if (errors.length === 0) {
|
|
598
|
+
setIsPickAnnounced(true);
|
|
583
599
|
announce(
|
|
584
600
|
valid.length === 1
|
|
585
601
|
? t('@astryx.fileInput.fileSelected', {fileName: valid[0].name})
|
|
@@ -741,7 +757,7 @@ export function FileInput({
|
|
|
741
757
|
|
|
742
758
|
const renderDropzoneContent = () => {
|
|
743
759
|
if (isBusy) {
|
|
744
|
-
return <Spinner size="md" />;
|
|
760
|
+
return <Spinner size="md" aria-hidden="true" />;
|
|
745
761
|
}
|
|
746
762
|
if (hasFiles) {
|
|
747
763
|
return (
|
|
@@ -772,7 +788,7 @@ export function FileInput({
|
|
|
772
788
|
<span {...stylex.props(styles.fileNameText)}>
|
|
773
789
|
{fileNames ?? displayPlaceholder}
|
|
774
790
|
</span>
|
|
775
|
-
<Spinner size="sm" />
|
|
791
|
+
<Spinner size="sm" aria-hidden="true" />
|
|
776
792
|
</>
|
|
777
793
|
);
|
|
778
794
|
}
|
|
@@ -105,6 +105,14 @@ afterEach(() => {
|
|
|
105
105
|
// "visible" in the accessibility tree. Use hidden: true to find it.
|
|
106
106
|
const h = {hidden: true} as const;
|
|
107
107
|
|
|
108
|
+
/** A busy control's Spinner is decorative and silent (spec:AST-086 FR8). */
|
|
109
|
+
function expectSilentBusySpinner() {
|
|
110
|
+
const spinner = document.querySelector('.astryx-spinner');
|
|
111
|
+
expect(spinner).not.toBeNull();
|
|
112
|
+
expect(spinner).toHaveAttribute('aria-hidden', 'true');
|
|
113
|
+
expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
describe('MultiSelector', () => {
|
|
109
117
|
const defaultOptions = ['Apple', 'Banana', 'Orange'];
|
|
110
118
|
|
|
@@ -1303,7 +1311,10 @@ describe('MultiSelector', () => {
|
|
|
1303
1311
|
// component cannot make; the trigger's spinner carries the state.
|
|
1304
1312
|
const listbox = screen.getByRole('listbox', h);
|
|
1305
1313
|
expect(within(listbox).queryByText('No options')).not.toBeInTheDocument();
|
|
1306
|
-
|
|
1314
|
+
// Only the busy start is spoken (spec:AST-086 FR8), never an empty or
|
|
1315
|
+
// no-results message the panel is suppressing.
|
|
1316
|
+
// (The channel clears its text on a timer, so it may already be empty.)
|
|
1317
|
+
expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
|
|
1307
1318
|
});
|
|
1308
1319
|
|
|
1309
1320
|
it('announces nothing while isLoading, matching the silent panel', async () => {
|
|
@@ -1337,7 +1348,10 @@ describe('MultiSelector', () => {
|
|
|
1337
1348
|
expect(
|
|
1338
1349
|
within(listbox).queryByText('Nothing like that here'),
|
|
1339
1350
|
).not.toBeInTheDocument();
|
|
1340
|
-
|
|
1351
|
+
// Only the busy start is spoken (spec:AST-086 FR8), never an empty or
|
|
1352
|
+
// no-results message the panel is suppressing.
|
|
1353
|
+
// (The channel clears its text on a timer, so it may already be empty.)
|
|
1354
|
+
expect(['', 'Loading']).toContain(politeRegion()?.textContent ?? '');
|
|
1341
1355
|
});
|
|
1342
1356
|
|
|
1343
1357
|
it('renders emptyText when there are no options and no search input', async () => {
|
|
@@ -1555,7 +1569,7 @@ describe('MultiSelector', () => {
|
|
|
1555
1569
|
/>,
|
|
1556
1570
|
);
|
|
1557
1571
|
expect(screen.getByRole('combobox')).toHaveAttribute('aria-busy', 'true');
|
|
1558
|
-
|
|
1572
|
+
expectSilentBusySpinner();
|
|
1559
1573
|
});
|
|
1560
1574
|
|
|
1561
1575
|
it('renders with custom selectAllLabel', async () => {
|
|
@@ -2250,7 +2264,7 @@ describe('MultiSelector', () => {
|
|
|
2250
2264
|
|
|
2251
2265
|
const trigger = screen.getByRole('combobox', {name: 'Fruit'});
|
|
2252
2266
|
expect(trigger).toHaveAttribute('aria-busy', 'true');
|
|
2253
|
-
|
|
2267
|
+
expectSilentBusySpinner();
|
|
2254
2268
|
|
|
2255
2269
|
await user.click(trigger);
|
|
2256
2270
|
expect(screen.queryByRole('listbox', h)).not.toBeInTheDocument();
|
|
@@ -92,6 +92,7 @@ import {useDevWarning} from '../hooks/useDevWarning';
|
|
|
92
92
|
import {composeEventHandlers} from '../utils/composeEventHandlers';
|
|
93
93
|
import {mergeRefs} from '../utils/mergeRefs';
|
|
94
94
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
95
|
+
import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
|
|
95
96
|
import type {SizeValue} from '../utils/types';
|
|
96
97
|
import {useSize} from '../SizeContext/SizeContext';
|
|
97
98
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -1203,6 +1204,9 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1203
1204
|
const [, startTransition] = useTransition();
|
|
1204
1205
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
1205
1206
|
const isBusy = isLoading || optimisticValue !== value;
|
|
1207
|
+
// The busy Spinner is decorative; this control announces its busy start
|
|
1208
|
+
// (spec:AST-086 FR8).
|
|
1209
|
+
useBusyAnnouncement(isBusy, t('@astryx.spinner.loading'), triggerRef);
|
|
1206
1210
|
|
|
1207
1211
|
// Disabled-reason tooltip. Disabled controls swallow pointer events, so the
|
|
1208
1212
|
// tooltip listeners attach to the trigger container (which already exists)
|
|
@@ -2643,7 +2647,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
2643
2647
|
value.map(v => (
|
|
2644
2648
|
<input key={v} type="hidden" name={htmlName} value={v} />
|
|
2645
2649
|
))}
|
|
2646
|
-
{isBusy && <Spinner size="sm" />}
|
|
2650
|
+
{isBusy && <Spinner size="sm" aria-hidden="true" />}
|
|
2647
2651
|
{hasClear &&
|
|
2648
2652
|
value.length > 0 &&
|
|
2649
2653
|
!isDisabled &&
|
|
@@ -565,21 +565,6 @@ describe('ProgressBar', () => {
|
|
|
565
565
|
expect(styleClasses(accentMarks[1])).toBe(styleClasses(errorMarks[1]));
|
|
566
566
|
});
|
|
567
567
|
|
|
568
|
-
it('keeps tabbable target marks outside the progressbar subtree', () => {
|
|
569
|
-
const {container} = render(
|
|
570
|
-
<ProgressBar
|
|
571
|
-
value={50}
|
|
572
|
-
label="Progress"
|
|
573
|
-
marks={[{value: 80, label: 'Goal'}]}
|
|
574
|
-
/>,
|
|
575
|
-
);
|
|
576
|
-
const progressbar = screen.getByRole('progressbar', {name: 'Progress'});
|
|
577
|
-
const mark = container.querySelector(MARK)!;
|
|
578
|
-
expect(mark).toHaveAttribute('tabindex', '0');
|
|
579
|
-
expect(progressbar.querySelector('[tabindex="0"]')).toBeNull();
|
|
580
|
-
expect(mark.closest('[role="progressbar"]')).toBeNull();
|
|
581
|
-
});
|
|
582
|
-
|
|
583
568
|
it('renders every mark as a focusable trigger (label is required, never decorative)', () => {
|
|
584
569
|
const {container} = render(
|
|
585
570
|
<ProgressBar
|
|
@@ -637,7 +622,6 @@ describe('ProgressBar', () => {
|
|
|
637
622
|
/>,
|
|
638
623
|
);
|
|
639
624
|
const progressbar = screen.getByRole('progressbar');
|
|
640
|
-
expect(progressbar.querySelector(MARK)).toBeNull();
|
|
641
625
|
expect(progressbar.querySelector('[role="img"]')).toBeNull();
|
|
642
626
|
expect(progressbar.querySelector('[aria-label]')).toBeNull();
|
|
643
627
|
expect(container.querySelectorAll(MARK)).toHaveLength(1);
|
|
@@ -655,9 +639,10 @@ describe('ProgressBar', () => {
|
|
|
655
639
|
expect(progressbar.getAttribute('aria-valuetext')).toBe('50%');
|
|
656
640
|
});
|
|
657
641
|
|
|
658
|
-
it('
|
|
659
|
-
// Only the fill belongs to the semantic track
|
|
660
|
-
//
|
|
642
|
+
it('keeps the fill as the progressbar child when marks are rendered', () => {
|
|
643
|
+
// Only the fill belongs to the semantic track. That the focusable marks
|
|
644
|
+
// stay outside its presentational subtree is the progressbar contract's
|
|
645
|
+
// target-marks state (progressbar.subtree.no-focusable-descendants).
|
|
661
646
|
const {container} = render(
|
|
662
647
|
<ProgressBar
|
|
663
648
|
value={50}
|
|
@@ -669,8 +654,6 @@ describe('ProgressBar', () => {
|
|
|
669
654
|
const fill = progressbar.firstElementChild as HTMLElement;
|
|
670
655
|
expect(fill.style.width).toBe('50%');
|
|
671
656
|
expect(fill.classList.contains('astryx-progress-bar-mark')).toBe(false);
|
|
672
|
-
const mark = container.querySelector<HTMLElement>(MARK)!;
|
|
673
|
-
expect(progressbar).not.toContainElement(mark);
|
|
674
657
|
expect(container.querySelectorAll(MARK)).toHaveLength(1);
|
|
675
658
|
});
|
|
676
659
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ProgressBar.a11y.chromium.spec.ts
|
|
5
|
+
* @input Built ProgressBar stories and PROGRESSBAR_PATTERN in real Chromium
|
|
6
|
+
* @output Browser evidence for every progressbar expectation across the binding's states, reconciled against the exact known failures
|
|
7
|
+
* @position Chromium lane of the ProgressBar binding
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
expect,
|
|
12
|
+
test,
|
|
13
|
+
type CDPSession,
|
|
14
|
+
type Locator,
|
|
15
|
+
type Page,
|
|
16
|
+
} from '@playwright/test';
|
|
17
|
+
import {
|
|
18
|
+
PROGRESSBAR_PATTERN,
|
|
19
|
+
blockingResults,
|
|
20
|
+
checkAccessibilitySpec,
|
|
21
|
+
formatFailures,
|
|
22
|
+
neverExercised,
|
|
23
|
+
summarize,
|
|
24
|
+
unmatchedKnownFailures,
|
|
25
|
+
type BindingResult,
|
|
26
|
+
} from '@astryxdesign/a11y-spec';
|
|
27
|
+
import {
|
|
28
|
+
createChromiumHarness,
|
|
29
|
+
holdMotionStill,
|
|
30
|
+
} from '@astryxdesign/a11y-spec/chromium';
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_STORYBOOK_DIR,
|
|
33
|
+
serveStorybook,
|
|
34
|
+
type StaticServer,
|
|
35
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
36
|
+
import {PROGRESSBAR_KNOWN_FAILURES} from './ProgressBar.a11y.known-failures';
|
|
37
|
+
import {
|
|
38
|
+
PROGRESSBAR_BINDING_STATES,
|
|
39
|
+
type ProgressBarBindingState,
|
|
40
|
+
} from './ProgressBar.a11y.states';
|
|
41
|
+
|
|
42
|
+
let storybook: StaticServer;
|
|
43
|
+
|
|
44
|
+
test.beforeAll(async () => {
|
|
45
|
+
storybook = await serveStorybook(
|
|
46
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test.afterAll(async () => {
|
|
51
|
+
await storybook?.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function storyUrl(storyId: string): string {
|
|
55
|
+
return `${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function mountState(
|
|
59
|
+
page: Page,
|
|
60
|
+
state: ProgressBarBindingState,
|
|
61
|
+
): Promise<{subject: Locator; related: Record<string, Locator>}> {
|
|
62
|
+
await page.goto(storyUrl(state.storyId), {waitUntil: 'load'});
|
|
63
|
+
await holdMotionStill(page);
|
|
64
|
+
const subject = page
|
|
65
|
+
.locator('#storybook-root')
|
|
66
|
+
.getByRole('progressbar', {name: state.facts.name});
|
|
67
|
+
await subject.waitFor({state: 'attached'});
|
|
68
|
+
return {subject, related: {}};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runState(
|
|
72
|
+
page: Page,
|
|
73
|
+
cdp: CDPSession,
|
|
74
|
+
state: ProgressBarBindingState,
|
|
75
|
+
): Promise<BindingResult> {
|
|
76
|
+
return checkAccessibilitySpec({
|
|
77
|
+
spec: PROGRESSBAR_PATTERN,
|
|
78
|
+
binding: 'ProgressBar',
|
|
79
|
+
state: state.id,
|
|
80
|
+
facts: state.facts,
|
|
81
|
+
knownFailures: PROGRESSBAR_KNOWN_FAILURES,
|
|
82
|
+
mount: async () => {
|
|
83
|
+
const {subject, related} = await mountState(page, state);
|
|
84
|
+
return createChromiumHarness({page, cdp, subject, related});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('every progressbar expectation is exercised by a ProgressBar state', async ({
|
|
90
|
+
page,
|
|
91
|
+
}) => {
|
|
92
|
+
const cdp = await page.context().newCDPSession(page);
|
|
93
|
+
const results: BindingResult[] = [];
|
|
94
|
+
for (const state of PROGRESSBAR_BINDING_STATES) {
|
|
95
|
+
results.push(await runState(page, cdp, state));
|
|
96
|
+
}
|
|
97
|
+
expect(neverExercised(results)).toEqual([]);
|
|
98
|
+
expect(unmatchedKnownFailures(PROGRESSBAR_KNOWN_FAILURES, results)).toEqual(
|
|
99
|
+
[],
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
for (const state of PROGRESSBAR_BINDING_STATES) {
|
|
104
|
+
test(`ProgressBar [${state.id}] — ${state.summary}`, async ({page}) => {
|
|
105
|
+
const cdp = await page.context().newCDPSession(page);
|
|
106
|
+
const result = await runState(page, cdp, state);
|
|
107
|
+
const report = summarize(PROGRESSBAR_PATTERN, [result]);
|
|
108
|
+
// Required rows block (AST-020 FR9); advisory rows and exact known
|
|
109
|
+
// failures report.
|
|
110
|
+
expect(formatFailures(blockingResults([result]))).toBe('');
|
|
111
|
+
expect(report.unrunLayers).toEqual([]);
|
|
112
|
+
expect(report.counts.unexpectedPass).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ProgressBar.a11y.known-failures.ts
|
|
5
|
+
* @input The exact progressbar-contract failures the shipped ProgressBar produces today
|
|
6
|
+
* @output PROGRESSBAR_KNOWN_FAILURES — records both lanes reconcile against, so a fix shows up as an unexpected pass and any other failure stays a failure
|
|
7
|
+
* @position Known-failure ledger for the ProgressBar binding
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {KnownFailure} from '@astryxdesign/a11y-spec';
|
|
11
|
+
|
|
12
|
+
export const PROGRESSBAR_KNOWN_FAILURES: ReadonlyArray<KnownFailure> = [];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file ProgressBar.a11y.renders.tsx
|
|
7
|
+
* @input One ProgressBar binding state
|
|
8
|
+
* @output The React tree every lane renders for that state (the jsdom test directly, the story through Storybook)
|
|
9
|
+
* @position Shared fixture for the ProgressBar binding
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {ReactElement} from 'react';
|
|
13
|
+
import {ProgressBar} from '../ProgressBar';
|
|
14
|
+
import {
|
|
15
|
+
formatGigabytes,
|
|
16
|
+
type ProgressBarBindingState,
|
|
17
|
+
} from './ProgressBar.a11y.states';
|
|
18
|
+
|
|
19
|
+
export function renderProgressBarState(
|
|
20
|
+
state: ProgressBarBindingState,
|
|
21
|
+
): ReactElement {
|
|
22
|
+
return (
|
|
23
|
+
<ProgressBar
|
|
24
|
+
style={{width: 320}}
|
|
25
|
+
label={state.facts.name}
|
|
26
|
+
// An indeterminate state still passes a value, so the proof shows the
|
|
27
|
+
// bar withholds it rather than never having had one.
|
|
28
|
+
value={state.facts.value ?? 50}
|
|
29
|
+
max={state.facts.max}
|
|
30
|
+
isLabelHidden={state.isLabelHidden}
|
|
31
|
+
hasValueLabel={state.hasValueLabel}
|
|
32
|
+
formatValueLabel={state.formatsValue ? formatGigabytes : undefined}
|
|
33
|
+
isIndeterminate={state.isIndeterminate}
|
|
34
|
+
isDisabled={state.isDisabled}
|
|
35
|
+
marks={state.marks}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|