@astryxdesign/core 0.7.4-canary.f32572e → 0.7.4
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/CHANGELOG.md +40 -0
- 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/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/CommandPalette/CommandPaletteItem.d.ts +1 -0
- package/dist/CommandPalette/CommandPaletteItem.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteItem.js +5 -1
- package/dist/DateInput/DateInput.d.ts +6 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +5 -1
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +5 -1
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +5 -1
- package/dist/DateInput/tokens.stylex.d.ts.map +1 -1
- package/dist/DateInput/tokens.stylex.js +3 -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/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +9 -2
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.spec.md +1 -0
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +25 -2
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +7 -6
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +221 -1
- package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
- 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/CommandPalette/CommandPalette.doc.mjs +4 -1
- 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/CommandPalette/CommandPaletteItem.doc.mjs +17 -5
- package/src/CommandPalette/CommandPaletteItem.test.tsx +65 -12
- package/src/CommandPalette/CommandPaletteItem.tsx +6 -1
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +250 -20
- package/src/DateInput/DateInput.doc.mjs +23 -5
- package/src/DateInput/DateInput.spec.md +52 -45
- package/src/DateInput/DateInput.tsx +13 -7
- package/src/DateInput/NativeDateField.tsx +6 -2
- package/src/DateInput/TouchDateField.tsx +6 -2
- package/src/DateInput/__tests__/DateInputAudit.a11y.chromium.spec.ts +576 -0
- package/src/DateInput/tokens.stylex.ts +3 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
- package/src/FileInput/FileInput.tsx +18 -2
- package/src/Overlay/Overlay.spec.md +304 -0
- package/src/Thumbnail/Thumbnail.test.tsx +6 -1
- package/src/Thumbnail/Thumbnail.tsx +12 -3
- package/src/__tests__/BusyOwnerAnnouncement.test.tsx +324 -2
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* owning control announces each busy start once through the shared channel
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
StrictMode,
|
|
13
|
+
useEffect,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type ReactElement,
|
|
17
|
+
} from 'react';
|
|
12
18
|
import {
|
|
13
19
|
afterAll,
|
|
14
20
|
afterEach,
|
|
@@ -29,6 +35,16 @@ import {DateInput} from '../DateInput';
|
|
|
29
35
|
import {DateRangeInput} from '../DateRangeInput';
|
|
30
36
|
import {DateTimeInput} from '../DateTimeInput';
|
|
31
37
|
import {TimeInput} from '../TimeInput';
|
|
38
|
+
import {CheckboxInput} from '../CheckboxInput';
|
|
39
|
+
import {FileInput} from '../FileInput';
|
|
40
|
+
import {CommandPalette} from '../CommandPalette';
|
|
41
|
+
import {Thumbnail} from '../Thumbnail';
|
|
42
|
+
import {
|
|
43
|
+
DropdownMenu,
|
|
44
|
+
DropdownMenuItem,
|
|
45
|
+
DropdownMenuSubMenu,
|
|
46
|
+
} from '../DropdownMenu';
|
|
47
|
+
import {ChatMessageList} from '../Chat';
|
|
32
48
|
import {BaseTypeahead, Typeahead} from '../Typeahead';
|
|
33
49
|
import type {SearchableItem, SearchSource} from '../Typeahead';
|
|
34
50
|
import type {ISOTimeString} from '../utils';
|
|
@@ -131,6 +147,38 @@ const OWNERS: ReadonlyArray<
|
|
|
131
147
|
),
|
|
132
148
|
] as const,
|
|
133
149
|
]),
|
|
150
|
+
[
|
|
151
|
+
'CheckboxInput',
|
|
152
|
+
isLoading => (
|
|
153
|
+
<CheckboxInput
|
|
154
|
+
label="Subscribe"
|
|
155
|
+
value={false}
|
|
156
|
+
onChange={() => {}}
|
|
157
|
+
isLoading={isLoading}
|
|
158
|
+
/>
|
|
159
|
+
),
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
'Thumbnail (uploading)',
|
|
163
|
+
isLoading => (
|
|
164
|
+
<Thumbnail src="/local.jpg" alt="Upload" isLoading={isLoading} />
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
...(['dropzone', 'input'] as const).map(
|
|
168
|
+
mode =>
|
|
169
|
+
[
|
|
170
|
+
`FileInput (${mode})`,
|
|
171
|
+
(isLoading: boolean) => (
|
|
172
|
+
<FileInput
|
|
173
|
+
label="Attachment"
|
|
174
|
+
mode={mode}
|
|
175
|
+
value={null}
|
|
176
|
+
onChange={() => {}}
|
|
177
|
+
isLoading={isLoading}
|
|
178
|
+
/>
|
|
179
|
+
),
|
|
180
|
+
] as const,
|
|
181
|
+
),
|
|
134
182
|
[
|
|
135
183
|
'DateRangeInput',
|
|
136
184
|
isLoading => (
|
|
@@ -242,7 +290,8 @@ describe.each(OWNERS)('%s busy state (spec:AST-086)', (_name, renderOwner) => {
|
|
|
242
290
|
const {container} = render(renderOwner(true));
|
|
243
291
|
const spinner = container.querySelector('.astryx-spinner');
|
|
244
292
|
expect(spinner).not.toBeNull();
|
|
245
|
-
|
|
293
|
+
// Hidden itself, or inside a decorative hidden indicator (CheckboxInput).
|
|
294
|
+
expect(spinner!.closest('[aria-hidden="true"]')).not.toBeNull();
|
|
246
295
|
expect(screen.queryByRole('status', {name: 'Loading'})).toBeNull();
|
|
247
296
|
});
|
|
248
297
|
|
|
@@ -468,3 +517,276 @@ describe.each(SHEET_OWNERS)(
|
|
|
468
517
|
});
|
|
469
518
|
},
|
|
470
519
|
);
|
|
520
|
+
|
|
521
|
+
describe('DropdownMenuSubMenu loading state (spec:AST-086 FR8)', () => {
|
|
522
|
+
function Menu({isLoading}: {isLoading: boolean}) {
|
|
523
|
+
return (
|
|
524
|
+
<DropdownMenu button={{label: 'Actions'}}>
|
|
525
|
+
<DropdownMenuSubMenu label="Move to" hasSpinner={isLoading}>
|
|
526
|
+
<DropdownMenuItem label="Inbox" onClick={() => {}} />
|
|
527
|
+
</DropdownMenuSubMenu>
|
|
528
|
+
</DropdownMenu>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
it('announces nothing while its menu is closed', async () => {
|
|
533
|
+
render(strict(<Menu isLoading />));
|
|
534
|
+
await nextFrame();
|
|
535
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('announces the busy start when its menu opens while loading', async () => {
|
|
539
|
+
render(strict(<Menu isLoading />));
|
|
540
|
+
fireEvent.click(screen.getByRole('button', {name: /Actions/}));
|
|
541
|
+
await nextFrame();
|
|
542
|
+
expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('keeps the trigger Spinner silent and announces each busy start once', async () => {
|
|
546
|
+
const {rerender} = render(strict(<Menu isLoading={false} />));
|
|
547
|
+
fireEvent.click(screen.getByRole('button', {name: /Actions/}));
|
|
548
|
+
await nextFrame();
|
|
549
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
550
|
+
rerender(strict(<Menu isLoading />));
|
|
551
|
+
await nextFrame();
|
|
552
|
+
const spinner = document.querySelector('.astryx-spinner');
|
|
553
|
+
expect(spinner).toHaveAttribute('aria-hidden', 'true');
|
|
554
|
+
expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
|
|
555
|
+
rerender(strict(<Menu isLoading={false} />));
|
|
556
|
+
rerender(strict(<Menu isLoading />));
|
|
557
|
+
await nextFrame();
|
|
558
|
+
expect(busyAnnouncements()).toEqual([
|
|
559
|
+
['Loading', 'polite'],
|
|
560
|
+
['Loading', 'polite'],
|
|
561
|
+
]);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe('ChatMessageList top loader (spec:AST-086 FR4, FR8)', () => {
|
|
566
|
+
it('keeps the loader inside the log silent and announces loading once through the channel', async () => {
|
|
567
|
+
let notify!: IntersectionObserverCallback;
|
|
568
|
+
vi.stubGlobal(
|
|
569
|
+
'IntersectionObserver',
|
|
570
|
+
class {
|
|
571
|
+
constructor(callback: IntersectionObserverCallback) {
|
|
572
|
+
notify = callback;
|
|
573
|
+
}
|
|
574
|
+
observe() {}
|
|
575
|
+
disconnect() {}
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
render(
|
|
579
|
+
strict(
|
|
580
|
+
<ChatMessageList
|
|
581
|
+
scrollToTopAction={async () => new Promise<void>(() => {})}>
|
|
582
|
+
<div>Earlier message</div>
|
|
583
|
+
</ChatMessageList>,
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
act(() => {
|
|
587
|
+
notify(
|
|
588
|
+
[{isIntersecting: true} as IntersectionObserverEntry],
|
|
589
|
+
{} as IntersectionObserver,
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
await nextFrame();
|
|
593
|
+
expect(document.querySelector('.astryx-spinner')).toHaveAttribute(
|
|
594
|
+
'aria-hidden',
|
|
595
|
+
'true',
|
|
596
|
+
);
|
|
597
|
+
expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
describe('FileInput busy start from a pick or a clear (spec:AST-086 FR6, FR8)', () => {
|
|
602
|
+
const file = new File(['x'], 'report.pdf', {type: 'application/pdf'});
|
|
603
|
+
|
|
604
|
+
it('keeps a pick under a pending changeAction to its one "selected" message', async () => {
|
|
605
|
+
const {container} = render(
|
|
606
|
+
strict(
|
|
607
|
+
<FileInput
|
|
608
|
+
label="Attachment"
|
|
609
|
+
value={null}
|
|
610
|
+
onChange={() => {}}
|
|
611
|
+
changeAction={async () => new Promise<void>(() => {})}
|
|
612
|
+
/>,
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
act(() => {
|
|
616
|
+
fireEvent.change(
|
|
617
|
+
container.querySelector('input[type="file"]') as HTMLInputElement,
|
|
618
|
+
{target: {files: [file]}},
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
await nextFrame();
|
|
622
|
+
expect(container.querySelector('.astryx-spinner')).toHaveAttribute(
|
|
623
|
+
'aria-hidden',
|
|
624
|
+
'true',
|
|
625
|
+
);
|
|
626
|
+
expect(announce).toHaveBeenCalledWith('1 file selected: report.pdf');
|
|
627
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('keeps a pick whose onChange starts an upload (isLoading) to its one "selected" message', async () => {
|
|
631
|
+
function Uploader() {
|
|
632
|
+
const [value, setValue] = useState<File | null>(null);
|
|
633
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
634
|
+
return (
|
|
635
|
+
<FileInput
|
|
636
|
+
label="Attachment"
|
|
637
|
+
value={value}
|
|
638
|
+
isLoading={isLoading}
|
|
639
|
+
onChange={next => {
|
|
640
|
+
setValue(next as File | null);
|
|
641
|
+
setIsLoading(true);
|
|
642
|
+
}}
|
|
643
|
+
/>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const {container} = render(strict(<Uploader />));
|
|
647
|
+
act(() => {
|
|
648
|
+
fireEvent.change(
|
|
649
|
+
container.querySelector('input[type="file"]') as HTMLInputElement,
|
|
650
|
+
{target: {files: [file]}},
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
await nextFrame();
|
|
654
|
+
await nextFrame();
|
|
655
|
+
expect(announce).toHaveBeenCalledWith('1 file selected: report.pdf');
|
|
656
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('announces nothing more when an earlier pick settles while a later one is still pending', async () => {
|
|
660
|
+
const settles: (() => void)[] = [];
|
|
661
|
+
const {container} = render(
|
|
662
|
+
strict(
|
|
663
|
+
<FileInput
|
|
664
|
+
label="Attachment"
|
|
665
|
+
value={null}
|
|
666
|
+
onChange={() => {}}
|
|
667
|
+
changeAction={async () =>
|
|
668
|
+
new Promise<void>(resolve => {
|
|
669
|
+
settles.push(resolve);
|
|
670
|
+
})
|
|
671
|
+
}
|
|
672
|
+
/>,
|
|
673
|
+
),
|
|
674
|
+
);
|
|
675
|
+
const input = container.querySelector(
|
|
676
|
+
'input[type="file"]',
|
|
677
|
+
) as HTMLInputElement;
|
|
678
|
+
act(() => {
|
|
679
|
+
fireEvent.change(input, {target: {files: [file]}});
|
|
680
|
+
});
|
|
681
|
+
await nextFrame();
|
|
682
|
+
act(() => {
|
|
683
|
+
fireEvent.change(input, {
|
|
684
|
+
target: {files: [new File(['y'], 'notes.txt', {type: 'text/plain'})]},
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
await nextFrame();
|
|
688
|
+
await act(async () => {
|
|
689
|
+
settles[0]();
|
|
690
|
+
});
|
|
691
|
+
await nextFrame();
|
|
692
|
+
await nextFrame();
|
|
693
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('announces the busy start of a clear under a pending changeAction', async () => {
|
|
697
|
+
render(
|
|
698
|
+
strict(
|
|
699
|
+
<FileInput
|
|
700
|
+
label="Attachment"
|
|
701
|
+
value={file}
|
|
702
|
+
onChange={() => {}}
|
|
703
|
+
changeAction={async () => new Promise<void>(() => {})}
|
|
704
|
+
/>,
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
act(() => {
|
|
708
|
+
fireEvent.click(screen.getByRole('button', {name: /Clear/}));
|
|
709
|
+
});
|
|
710
|
+
await nextFrame();
|
|
711
|
+
expect(busyAnnouncements()).toEqual([['Loading', 'polite']]);
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe('CommandPalette busy start inside its modal (spec:AST-086 FR8, FR3)', () => {
|
|
716
|
+
beforeEach(() => {
|
|
717
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
718
|
+
this: HTMLDialogElement,
|
|
719
|
+
) {
|
|
720
|
+
this.setAttribute('open', '');
|
|
721
|
+
});
|
|
722
|
+
HTMLDialogElement.prototype.close = vi.fn(function (
|
|
723
|
+
this: HTMLDialogElement,
|
|
724
|
+
) {
|
|
725
|
+
this.removeAttribute('open');
|
|
726
|
+
});
|
|
727
|
+
const matches = Element.prototype.matches;
|
|
728
|
+
vi.spyOn(Element.prototype, 'matches').mockImplementation(function (
|
|
729
|
+
this: Element,
|
|
730
|
+
selector: string,
|
|
731
|
+
) {
|
|
732
|
+
return selector === ':modal'
|
|
733
|
+
? this instanceof HTMLDialogElement && this.open
|
|
734
|
+
: matches.call(this, selector);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
afterEach(() => {
|
|
738
|
+
vi.restoreAllMocks();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
function regionOf(): Element | null {
|
|
742
|
+
return (
|
|
743
|
+
document
|
|
744
|
+
.querySelector('dialog[open]')
|
|
745
|
+
?.querySelector('[data-astryx-modal-busy-region]') ?? null
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
it('announces the load on opening from a region inside its dialog, with a silent Spinner', async () => {
|
|
750
|
+
render(
|
|
751
|
+
strict(
|
|
752
|
+
<CommandPalette
|
|
753
|
+
isOpen
|
|
754
|
+
onOpenChange={() => {}}
|
|
755
|
+
searchSource={{
|
|
756
|
+
bootstrap: async () => new Promise<SearchableItem[]>(() => {}),
|
|
757
|
+
search: async () => new Promise<SearchableItem[]>(() => {}),
|
|
758
|
+
}}
|
|
759
|
+
/>,
|
|
760
|
+
),
|
|
761
|
+
);
|
|
762
|
+
await nextFrame();
|
|
763
|
+
await nextFrame();
|
|
764
|
+
const dialog = document.querySelector('dialog[open]') as HTMLElement;
|
|
765
|
+
expect(dialog.querySelector('.astryx-spinner')).toHaveAttribute(
|
|
766
|
+
'aria-hidden',
|
|
767
|
+
'true',
|
|
768
|
+
);
|
|
769
|
+
expect(regionOf()).toHaveTextContent('Loading');
|
|
770
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('announces a search start once from inside its dialog, not on the hidden page channel', async () => {
|
|
774
|
+
render(
|
|
775
|
+
strict(
|
|
776
|
+
<CommandPalette
|
|
777
|
+
isOpen
|
|
778
|
+
onOpenChange={() => {}}
|
|
779
|
+
searchSource={pendingSource()}
|
|
780
|
+
/>,
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
await nextFrame();
|
|
784
|
+
act(() => {
|
|
785
|
+
fireEvent.change(screen.getByRole('combobox'), {target: {value: 'se'}});
|
|
786
|
+
});
|
|
787
|
+
await nextFrame();
|
|
788
|
+
await nextFrame();
|
|
789
|
+
expect(regionOf()).toHaveTextContent('Loading');
|
|
790
|
+
expect(busyAnnouncements()).toEqual([]);
|
|
791
|
+
});
|
|
792
|
+
});
|