@astryxdesign/core 0.4.3 → 0.4.4-canary.0f4d45f
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 +60 -0
- package/README.md +79 -45
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +13 -3
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +392 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +398 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +72 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +130 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +180 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +989 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +12 -6
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +31 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +21 -7
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +14 -3
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tokenizer/Tokenizer.js +11 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +21 -0
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +22 -4
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +48 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +36 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +9 -8
- package/src/Banner/Banner.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +40 -0
- package/src/Banner/Banner.tsx +27 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
- package/src/BottomSheet/BottomSheet.tsx +545 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +547 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +15 -0
- package/src/BottomSheet/snapOffsets.test.ts +187 -0
- package/src/BottomSheet/snapOffsets.ts +159 -0
- package/src/BottomSheet/useMobileKeyboard.ts +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
- package/src/BottomSheet/useSheetGestures.ts +1459 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/ComplexSelector/ComplexSelector.tsx +12 -5
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +9 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +39 -2
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FileInput/FileInput.test.tsx +53 -9
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +195 -51
- package/src/MultiSelector/MultiSelector.tsx +22 -8
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +13 -4
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/Typeahead/BaseTypeahead.tsx +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.ts +22 -4
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +2 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import userEvent from '@testing-library/user-event';
|
|
21
21
|
import {MultiSelector} from './MultiSelector';
|
|
22
22
|
import {Icon} from '../Icon';
|
|
23
|
+
import {InternationalizationProvider} from '../i18n';
|
|
23
24
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
24
25
|
import {defineTheme} from '../theme/defineTheme';
|
|
25
26
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
@@ -415,6 +416,37 @@ describe('MultiSelector', () => {
|
|
|
415
416
|
expect(onChange).toHaveBeenCalledWith(['Apple']);
|
|
416
417
|
});
|
|
417
418
|
|
|
419
|
+
it('does not toggle the highlighted option on a composing Enter (IME)', async () => {
|
|
420
|
+
const user = userEvent.setup();
|
|
421
|
+
const onChange = vi.fn();
|
|
422
|
+
render(
|
|
423
|
+
<MultiSelector
|
|
424
|
+
label="Fruit"
|
|
425
|
+
options={defaultOptions}
|
|
426
|
+
value={[]}
|
|
427
|
+
onChange={onChange}
|
|
428
|
+
hasSearch
|
|
429
|
+
/>,
|
|
430
|
+
);
|
|
431
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
432
|
+
const search = screen.getByRole('combobox', h);
|
|
433
|
+
// Filter to Banana and highlight it so an unguarded Enter would toggle it.
|
|
434
|
+
await user.type(search, 'ban');
|
|
435
|
+
await user.keyboard('{ArrowDown}');
|
|
436
|
+
expect(search).toHaveAttribute('aria-activedescendant');
|
|
437
|
+
|
|
438
|
+
// The composing keydown (isComposing / legacy keyCode 229) that commits an
|
|
439
|
+
// IME candidate must not be read as "toggle the highlighted option".
|
|
440
|
+
fireEvent.keyDown(search, {key: 'Enter', isComposing: true});
|
|
441
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
442
|
+
fireEvent.keyDown(search, {key: 'Enter', keyCode: 229});
|
|
443
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
444
|
+
|
|
445
|
+
// A real, non-composing Enter still toggles the highlighted option.
|
|
446
|
+
fireEvent.keyDown(search, {key: 'Enter'});
|
|
447
|
+
expect(onChange).toHaveBeenCalledWith(['Banana']);
|
|
448
|
+
});
|
|
449
|
+
|
|
418
450
|
it('toggles the correct item when selected items are sorted to top', async () => {
|
|
419
451
|
const user = userEvent.setup();
|
|
420
452
|
const onChange = vi.fn();
|
|
@@ -798,56 +830,112 @@ describe('MultiSelector', () => {
|
|
|
798
830
|
it('announces the match count politely while searching', async () => {
|
|
799
831
|
const user = userEvent.setup();
|
|
800
832
|
render(
|
|
801
|
-
<
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
833
|
+
<InternationalizationProvider
|
|
834
|
+
locale="fr"
|
|
835
|
+
overrides={{
|
|
836
|
+
fr: {
|
|
837
|
+
'@astryx.multiSelector.resultCount':
|
|
838
|
+
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
839
|
+
},
|
|
840
|
+
}}>
|
|
841
|
+
<MultiSelector
|
|
842
|
+
label="Fruit"
|
|
843
|
+
options={defaultOptions}
|
|
844
|
+
value={EMPTY_VALUE}
|
|
845
|
+
onChange={() => {}}
|
|
846
|
+
hasSearch
|
|
847
|
+
/>
|
|
848
|
+
</InternationalizationProvider>,
|
|
808
849
|
);
|
|
809
850
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
810
851
|
// "an" matches Banana and Orange.
|
|
811
852
|
await user.type(screen.getByRole('combobox', h), 'an');
|
|
812
853
|
await waitFor(() => {
|
|
813
|
-
|
|
854
|
+
// The plural branch of a message no catalog supplies.
|
|
855
|
+
expect(politeRegion()?.textContent).toBe('2 résultats');
|
|
814
856
|
});
|
|
815
857
|
});
|
|
816
858
|
|
|
817
859
|
it('announces the singular form when one option matches', async () => {
|
|
818
860
|
const user = userEvent.setup();
|
|
819
861
|
render(
|
|
820
|
-
<
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
862
|
+
<InternationalizationProvider
|
|
863
|
+
locale="fr"
|
|
864
|
+
overrides={{
|
|
865
|
+
fr: {
|
|
866
|
+
'@astryx.multiSelector.resultCount':
|
|
867
|
+
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
868
|
+
},
|
|
869
|
+
}}>
|
|
870
|
+
<MultiSelector
|
|
871
|
+
label="Fruit"
|
|
872
|
+
options={defaultOptions}
|
|
873
|
+
value={EMPTY_VALUE}
|
|
874
|
+
onChange={() => {}}
|
|
875
|
+
hasSearch
|
|
876
|
+
/>
|
|
877
|
+
</InternationalizationProvider>,
|
|
827
878
|
);
|
|
828
879
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
829
|
-
// "app" matches only Apple.
|
|
880
|
+
// "app" matches only Apple. Exact match, so "1 résultats" would fail.
|
|
830
881
|
await user.type(screen.getByRole('combobox', h), 'app');
|
|
831
882
|
await waitFor(() => {
|
|
832
|
-
expect(politeRegion()).
|
|
883
|
+
expect(politeRegion()?.textContent).toBe('1 résultat');
|
|
833
884
|
});
|
|
834
885
|
});
|
|
835
886
|
|
|
836
|
-
it('announces
|
|
887
|
+
it('announces the empty-results message when nothing matches', async () => {
|
|
837
888
|
const user = userEvent.setup();
|
|
838
889
|
render(
|
|
839
|
-
<
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
890
|
+
<InternationalizationProvider
|
|
891
|
+
locale="fr"
|
|
892
|
+
overrides={{
|
|
893
|
+
fr: {'@astryx.multiSelector.emptySearchResults': 'Aucun résultat'},
|
|
894
|
+
}}>
|
|
895
|
+
<MultiSelector
|
|
896
|
+
label="Fruit"
|
|
897
|
+
options={defaultOptions}
|
|
898
|
+
value={EMPTY_VALUE}
|
|
899
|
+
onChange={() => {}}
|
|
900
|
+
hasSearch
|
|
901
|
+
/>
|
|
902
|
+
</InternationalizationProvider>,
|
|
846
903
|
);
|
|
847
904
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
848
905
|
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
849
906
|
await waitFor(() => {
|
|
850
|
-
expect(politeRegion()).
|
|
907
|
+
expect(politeRegion()?.textContent).toBe('Aucun résultat');
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('speaks the result count from a provider catalog (plural)', async () => {
|
|
912
|
+
const user = userEvent.setup();
|
|
913
|
+
render(
|
|
914
|
+
<InternationalizationProvider
|
|
915
|
+
locale="fr"
|
|
916
|
+
messages={{
|
|
917
|
+
fr: {
|
|
918
|
+
'@astryx.multiSelector.resultCount': {
|
|
919
|
+
defaultMessage:
|
|
920
|
+
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
}}>
|
|
924
|
+
<MultiSelector
|
|
925
|
+
label="Fruit"
|
|
926
|
+
options={defaultOptions}
|
|
927
|
+
value={EMPTY_VALUE}
|
|
928
|
+
onChange={() => {}}
|
|
929
|
+
hasSearch
|
|
930
|
+
/>
|
|
931
|
+
</InternationalizationProvider>,
|
|
932
|
+
);
|
|
933
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
934
|
+
// "an" matches Banana and Orange.
|
|
935
|
+
await user.type(screen.getByRole('combobox', h), 'an');
|
|
936
|
+
await waitFor(() => {
|
|
937
|
+
// Same key through the catalog path rather than `overrides`.
|
|
938
|
+
expect(politeRegion()?.textContent).toBe('2 résultats');
|
|
851
939
|
});
|
|
852
940
|
});
|
|
853
941
|
|
|
@@ -1159,53 +1247,109 @@ describe('MultiSelector', () => {
|
|
|
1159
1247
|
it('announces the selection count politely when toggling an option', async () => {
|
|
1160
1248
|
const user = userEvent.setup();
|
|
1161
1249
|
render(
|
|
1162
|
-
<
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1250
|
+
<InternationalizationProvider
|
|
1251
|
+
locale="fr"
|
|
1252
|
+
overrides={{
|
|
1253
|
+
fr: {
|
|
1254
|
+
'@astryx.multiSelector.selectionCount':
|
|
1255
|
+
'{count, number} sur {total, number} sélectionnés',
|
|
1256
|
+
},
|
|
1257
|
+
}}>
|
|
1258
|
+
<MultiSelector
|
|
1259
|
+
label="Fruit"
|
|
1260
|
+
options={[...ANNOUNCE_OPTIONS]}
|
|
1261
|
+
value={EMPTY_VALUE}
|
|
1262
|
+
onChange={() => {}}
|
|
1263
|
+
/>
|
|
1264
|
+
</InternationalizationProvider>,
|
|
1168
1265
|
);
|
|
1169
1266
|
await user.click(screen.getByRole('combobox'));
|
|
1170
1267
|
const options = screen.getAllByRole('option', {hidden: true});
|
|
1171
1268
|
await user.click(options[0]);
|
|
1172
1269
|
await waitFor(() => {
|
|
1173
|
-
|
|
1270
|
+
// Both arguments land, in the order the message asks for them.
|
|
1271
|
+
expect(politeRegion()?.textContent).toBe('1 sur 3 sélectionnés');
|
|
1174
1272
|
});
|
|
1175
1273
|
});
|
|
1176
1274
|
|
|
1177
|
-
it('announces
|
|
1275
|
+
it('announces the all-selected message when select-all selects everything', async () => {
|
|
1178
1276
|
const user = userEvent.setup();
|
|
1179
1277
|
render(
|
|
1180
|
-
<
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1278
|
+
<InternationalizationProvider
|
|
1279
|
+
locale="fr"
|
|
1280
|
+
overrides={{
|
|
1281
|
+
fr: {'@astryx.multiSelector.allSelected': 'Tout est là'},
|
|
1282
|
+
}}>
|
|
1283
|
+
<MultiSelector
|
|
1284
|
+
label="Fruit"
|
|
1285
|
+
options={[...ANNOUNCE_OPTIONS]}
|
|
1286
|
+
value={EMPTY_VALUE}
|
|
1287
|
+
onChange={() => {}}
|
|
1288
|
+
hasSelectAll
|
|
1289
|
+
/>
|
|
1290
|
+
</InternationalizationProvider>,
|
|
1187
1291
|
);
|
|
1188
1292
|
await user.click(screen.getByRole('combobox'));
|
|
1189
1293
|
await user.click(screen.getByText('Select all'));
|
|
1190
1294
|
await waitFor(() => {
|
|
1191
|
-
expect(politeRegion()).
|
|
1295
|
+
expect(politeRegion()?.textContent).toBe('Tout est là');
|
|
1192
1296
|
});
|
|
1193
1297
|
});
|
|
1194
1298
|
|
|
1195
|
-
it('announces
|
|
1299
|
+
it('announces the selection-cleared message when clearing', async () => {
|
|
1196
1300
|
const user = userEvent.setup();
|
|
1197
1301
|
render(
|
|
1198
|
-
<
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1302
|
+
<InternationalizationProvider
|
|
1303
|
+
locale="fr"
|
|
1304
|
+
overrides={{
|
|
1305
|
+
fr: {'@astryx.multiSelector.selectionCleared': 'Plus rien'},
|
|
1306
|
+
}}>
|
|
1307
|
+
<MultiSelector
|
|
1308
|
+
label="Fruit"
|
|
1309
|
+
options={[...ANNOUNCE_OPTIONS]}
|
|
1310
|
+
value={['Apple', 'Banana']}
|
|
1311
|
+
onChange={() => {}}
|
|
1312
|
+
hasClear
|
|
1313
|
+
/>
|
|
1314
|
+
</InternationalizationProvider>,
|
|
1205
1315
|
);
|
|
1206
1316
|
await user.click(screen.getByRole('button', {name: 'Clear all Fruit'}));
|
|
1207
1317
|
await waitFor(() => {
|
|
1208
|
-
expect(politeRegion()).
|
|
1318
|
+
expect(politeRegion()?.textContent).toBe('Plus rien');
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it('prefers a provider override over the provider catalog', async () => {
|
|
1323
|
+
const user = userEvent.setup();
|
|
1324
|
+
render(
|
|
1325
|
+
<InternationalizationProvider
|
|
1326
|
+
locale="fr"
|
|
1327
|
+
messages={{
|
|
1328
|
+
fr: {
|
|
1329
|
+
'@astryx.multiSelector.selectionCount': {
|
|
1330
|
+
defaultMessage: '{count, number} du catalogue',
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
}}
|
|
1334
|
+
overrides={{
|
|
1335
|
+
fr: {
|
|
1336
|
+
'@astryx.multiSelector.selectionCount':
|
|
1337
|
+
'{count, number} remplacé',
|
|
1338
|
+
},
|
|
1339
|
+
}}>
|
|
1340
|
+
<MultiSelector
|
|
1341
|
+
label="Fruit"
|
|
1342
|
+
options={[...ANNOUNCE_OPTIONS]}
|
|
1343
|
+
value={EMPTY_VALUE}
|
|
1344
|
+
onChange={() => {}}
|
|
1345
|
+
/>
|
|
1346
|
+
</InternationalizationProvider>,
|
|
1347
|
+
);
|
|
1348
|
+
await user.click(screen.getByRole('combobox'));
|
|
1349
|
+
const options = screen.getAllByRole('option', {hidden: true});
|
|
1350
|
+
await user.click(options[0]);
|
|
1351
|
+
await waitFor(() => {
|
|
1352
|
+
expect(politeRegion()?.textContent).toBe('1 remplacé');
|
|
1209
1353
|
});
|
|
1210
1354
|
});
|
|
1211
1355
|
});
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
getSelectableOptions,
|
|
72
72
|
} from '../Selector/utils';
|
|
73
73
|
import {useMultiCombobox} from './hooks';
|
|
74
|
-
import {getInputARIA, mergeProps} from '../utils';
|
|
74
|
+
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
75
75
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
76
76
|
import type {BaseProps} from '../BaseProps';
|
|
77
77
|
import type {SizeValue} from '../utils/types';
|
|
@@ -792,14 +792,19 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
792
792
|
const selectableSet = new Set(selectableItems.map(item => item.value));
|
|
793
793
|
const selectedCount = nextValue.filter(v => selectableSet.has(v)).length;
|
|
794
794
|
if (selectedCount === 0) {
|
|
795
|
-
announce('
|
|
795
|
+
announce(t('@astryx.multiSelector.selectionCleared'));
|
|
796
796
|
} else if (total > 0 && selectedCount === total) {
|
|
797
|
-
announce('
|
|
797
|
+
announce(t('@astryx.multiSelector.allSelected'));
|
|
798
798
|
} else {
|
|
799
|
-
announce(
|
|
799
|
+
announce(
|
|
800
|
+
t('@astryx.multiSelector.selectionCount', {
|
|
801
|
+
count: selectedCount,
|
|
802
|
+
total,
|
|
803
|
+
}),
|
|
804
|
+
);
|
|
800
805
|
}
|
|
801
806
|
},
|
|
802
|
-
[announce, selectableItems],
|
|
807
|
+
[announce, selectableItems, t],
|
|
803
808
|
);
|
|
804
809
|
|
|
805
810
|
// Filter items by search query
|
|
@@ -903,11 +908,11 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
903
908
|
const count = filterOptionsByQuery(selectableItems, nextQuery).length;
|
|
904
909
|
announce(
|
|
905
910
|
count === 0
|
|
906
|
-
? '
|
|
907
|
-
:
|
|
911
|
+
? t('@astryx.multiSelector.emptySearchResults')
|
|
912
|
+
: t('@astryx.multiSelector.resultCount', {count}),
|
|
908
913
|
);
|
|
909
914
|
},
|
|
910
|
-
[announce, selectableItems],
|
|
915
|
+
[announce, selectableItems, t],
|
|
911
916
|
);
|
|
912
917
|
|
|
913
918
|
// Handle toggle
|
|
@@ -1182,6 +1187,15 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1182
1187
|
}
|
|
1183
1188
|
}}
|
|
1184
1189
|
onKeyDown={e => {
|
|
1190
|
+
// An in-progress IME composition uses these same keys (Enter to
|
|
1191
|
+
// commit the candidate, Escape/Arrows to navigate the candidate
|
|
1192
|
+
// window); the composing keydown fires before compositionend, so
|
|
1193
|
+
// without this guard a Korean/Japanese/Chinese user committing a
|
|
1194
|
+
// syllable with Enter would instead toggle the highlighted option.
|
|
1195
|
+
// See utils/ime.ts.
|
|
1196
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1185
1199
|
// Arrow keys navigate options; Enter toggles; Escape closes.
|
|
1186
1200
|
// Space and Home/End are left to the input (type a space / move
|
|
1187
1201
|
// the caret) per the APG editable combobox; PageUp/PageDown are
|
|
@@ -1340,6 +1340,24 @@ describe('NumberInput stepping', () => {
|
|
|
1340
1340
|
expect(onChange).toHaveBeenLastCalledWith(4);
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
|
+
it('does not step or commit on a composing keydown (IME)', () => {
|
|
1344
|
+
const onChange = vi.fn();
|
|
1345
|
+
render(<NumberInput label="Amount" value={5} onChange={onChange} />);
|
|
1346
|
+
const input = screen.getByRole('spinbutton');
|
|
1347
|
+
|
|
1348
|
+
// The field is type="text", so an IME composes into it. Enter commits the
|
|
1349
|
+
// candidate and the arrows walk the candidate window; neither should reach
|
|
1350
|
+
// the stepping or commit paths. Both signals a browser may report.
|
|
1351
|
+
fireEvent.keyDown(input, {key: 'ArrowUp', isComposing: true});
|
|
1352
|
+
fireEvent.keyDown(input, {key: 'ArrowDown', keyCode: 229});
|
|
1353
|
+
fireEvent.keyDown(input, {key: 'Enter', isComposing: true});
|
|
1354
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1355
|
+
|
|
1356
|
+
// A real, non-composing ArrowUp still steps.
|
|
1357
|
+
fireEvent.keyDown(input, {key: 'ArrowUp'});
|
|
1358
|
+
expect(onChange).toHaveBeenLastCalledWith(6);
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1343
1361
|
it('lets onKeyDown cancel keyboard stepping', () => {
|
|
1344
1362
|
const onChange = vi.fn();
|
|
1345
1363
|
const onKeyDown = vi.fn((event: React.KeyboardEvent<HTMLInputElement>) =>
|
|
@@ -169,7 +169,7 @@ export type {
|
|
|
169
169
|
InputStatus as NumberInputStatus,
|
|
170
170
|
InputStatusType as NumberInputStatusType,
|
|
171
171
|
} from '../Field';
|
|
172
|
-
import {mergeProps, mergeRefs} from '../utils';
|
|
172
|
+
import {isImeKeyEvent, mergeProps, mergeRefs} from '../utils';
|
|
173
173
|
import type {BaseProps} from '../BaseProps';
|
|
174
174
|
import type {SizeValue} from '../utils/types';
|
|
175
175
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -746,6 +746,14 @@ export function NumberInput({
|
|
|
746
746
|
// Handle keyboard events
|
|
747
747
|
const handleKeyDown = useCallback(
|
|
748
748
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
749
|
+
// The field is type="text" for formatted display, so an IME can compose
|
|
750
|
+
// into it: Enter commits the candidate and the arrows walk the candidate
|
|
751
|
+
// window. The composing keydown fires before compositionend, so without
|
|
752
|
+
// this guard those keystrokes would commit or step the value instead.
|
|
753
|
+
// See utils/ime.ts.
|
|
754
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
749
757
|
const hasModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
|
750
758
|
if (!hasModifier && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
751
759
|
onKeyDown?.(e);
|
|
@@ -20,7 +20,7 @@ import {HStack, VStack} from '../Stack';
|
|
|
20
20
|
import {Icon} from '../Icon';
|
|
21
21
|
import {TreeList, type TreeListItemData} from '../TreeList';
|
|
22
22
|
import {useTranslator} from '../i18n';
|
|
23
|
-
import {isImeKeyEvent} from '../
|
|
23
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
24
24
|
import {spacingVars, typeScaleVars} from '../theme/tokens.stylex';
|
|
25
25
|
import {PowerSearchValueEditor} from './PowerSearchValueEditor';
|
|
26
26
|
import {resolveOperatorLabel} from './resolveOperatorLabel';
|
|
@@ -26,6 +26,7 @@ import {RadioIndicator} from '../Indicator';
|
|
|
26
26
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
27
27
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
28
28
|
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
29
|
+
import {InternationalizationProvider} from '../i18n';
|
|
29
30
|
import {defineTheme} from '../theme/defineTheme';
|
|
30
31
|
import {Theme} from '../theme/Theme';
|
|
31
32
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
@@ -1000,60 +1001,118 @@ describe('Selector', () => {
|
|
|
1000
1001
|
).toBeInTheDocument();
|
|
1001
1002
|
});
|
|
1002
1003
|
|
|
1004
|
+
it('does not select the highlighted option on a composing Enter (IME)', async () => {
|
|
1005
|
+
const user = userEvent.setup();
|
|
1006
|
+
const onChange = vi.fn();
|
|
1007
|
+
render(
|
|
1008
|
+
<Selector
|
|
1009
|
+
label="Fruit"
|
|
1010
|
+
options={OPTIONS}
|
|
1011
|
+
onChange={onChange}
|
|
1012
|
+
hasSearch
|
|
1013
|
+
/>,
|
|
1014
|
+
);
|
|
1015
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1016
|
+
const search = screen.getByRole('combobox', h);
|
|
1017
|
+
// Filter to Banana and highlight it so an unguarded Enter would commit a
|
|
1018
|
+
// selection.
|
|
1019
|
+
await user.type(search, 'ban');
|
|
1020
|
+
await user.keyboard('{ArrowDown}');
|
|
1021
|
+
expect(search).toHaveAttribute('aria-activedescendant');
|
|
1022
|
+
|
|
1023
|
+
// The browser fires this composing keydown for the Enter that commits an
|
|
1024
|
+
// IME candidate (isComposing: true, or the legacy keyCode 229) before
|
|
1025
|
+
// compositionend writes the syllable. It must NOT be read as "select the
|
|
1026
|
+
// highlighted option".
|
|
1027
|
+
fireEvent.keyDown(search, {key: 'Enter', isComposing: true});
|
|
1028
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1029
|
+
fireEvent.keyDown(search, {key: 'Enter', keyCode: 229});
|
|
1030
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
1031
|
+
|
|
1032
|
+
// A real, non-composing Enter still selects the highlighted option.
|
|
1033
|
+
fireEvent.keyDown(search, {key: 'Enter'});
|
|
1034
|
+
expect(onChange).toHaveBeenCalledWith('Banana');
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1003
1037
|
describe('result announcements', () => {
|
|
1004
1038
|
it('announces the match count politely while searching', async () => {
|
|
1005
1039
|
const user = userEvent.setup();
|
|
1006
1040
|
render(
|
|
1007
|
-
<
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1041
|
+
<InternationalizationProvider
|
|
1042
|
+
locale="fr"
|
|
1043
|
+
overrides={{
|
|
1044
|
+
fr: {
|
|
1045
|
+
'@astryx.selector.resultCount':
|
|
1046
|
+
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
1047
|
+
},
|
|
1048
|
+
}}>
|
|
1049
|
+
<Selector
|
|
1050
|
+
label="Fruit"
|
|
1051
|
+
options={OPTIONS}
|
|
1052
|
+
value="Apple"
|
|
1053
|
+
onChange={() => {}}
|
|
1054
|
+
hasSearch
|
|
1055
|
+
/>
|
|
1056
|
+
</InternationalizationProvider>,
|
|
1014
1057
|
);
|
|
1015
1058
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1016
1059
|
// "a" matches Apple and Banana.
|
|
1017
1060
|
await user.type(screen.getByRole('combobox', h), 'a');
|
|
1018
1061
|
await waitFor(() => {
|
|
1019
|
-
|
|
1062
|
+
// The plural branch of a message no catalog supplies.
|
|
1063
|
+
expect(politeRegion()?.textContent).toBe('2 résultats');
|
|
1020
1064
|
});
|
|
1021
1065
|
});
|
|
1022
1066
|
|
|
1023
1067
|
it('announces the singular form when one option matches', async () => {
|
|
1024
1068
|
const user = userEvent.setup();
|
|
1025
1069
|
render(
|
|
1026
|
-
<
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1070
|
+
<InternationalizationProvider
|
|
1071
|
+
locale="fr"
|
|
1072
|
+
overrides={{
|
|
1073
|
+
fr: {
|
|
1074
|
+
'@astryx.selector.resultCount':
|
|
1075
|
+
'{count, number} {count, plural, one {résultat} other {résultats}}',
|
|
1076
|
+
},
|
|
1077
|
+
}}>
|
|
1078
|
+
<Selector
|
|
1079
|
+
label="Fruit"
|
|
1080
|
+
options={OPTIONS}
|
|
1081
|
+
value="Apple"
|
|
1082
|
+
onChange={() => {}}
|
|
1083
|
+
hasSearch
|
|
1084
|
+
/>
|
|
1085
|
+
</InternationalizationProvider>,
|
|
1033
1086
|
);
|
|
1034
1087
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1035
|
-
// "ban" matches only Banana.
|
|
1088
|
+
// "ban" matches only Banana. Exact, so "1 résultats" would fail.
|
|
1036
1089
|
await user.type(screen.getByRole('combobox', h), 'ban');
|
|
1037
1090
|
await waitFor(() => {
|
|
1038
|
-
expect(politeRegion()).
|
|
1091
|
+
expect(politeRegion()?.textContent).toBe('1 résultat');
|
|
1039
1092
|
});
|
|
1040
1093
|
});
|
|
1041
1094
|
|
|
1042
|
-
it('announces
|
|
1095
|
+
it('announces the empty-results message when nothing matches', async () => {
|
|
1043
1096
|
const user = userEvent.setup();
|
|
1044
1097
|
render(
|
|
1045
|
-
<
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1098
|
+
<InternationalizationProvider
|
|
1099
|
+
locale="fr"
|
|
1100
|
+
overrides={{
|
|
1101
|
+
fr: {'@astryx.selector.emptySearchResults': 'Aucun résultat'},
|
|
1102
|
+
}}>
|
|
1103
|
+
<Selector
|
|
1104
|
+
label="Fruit"
|
|
1105
|
+
options={OPTIONS}
|
|
1106
|
+
value="Apple"
|
|
1107
|
+
onChange={() => {}}
|
|
1108
|
+
hasSearch
|
|
1109
|
+
/>
|
|
1110
|
+
</InternationalizationProvider>,
|
|
1052
1111
|
);
|
|
1053
1112
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1054
1113
|
await user.type(screen.getByRole('combobox', h), 'xyz');
|
|
1055
1114
|
await waitFor(() => {
|
|
1056
|
-
expect(politeRegion()).
|
|
1115
|
+
expect(politeRegion()?.textContent).toBe('Aucun résultat');
|
|
1057
1116
|
});
|
|
1058
1117
|
});
|
|
1059
1118
|
|
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
import {useCombobox, useSelectedItemOffset} from './hooks';
|
|
73
73
|
import {useTypeahead} from '../hooks/useTypeahead';
|
|
74
74
|
import {SelectorOption} from './SelectorOption';
|
|
75
|
-
import {getInputARIA, mergeProps} from '../utils';
|
|
75
|
+
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
76
76
|
import {useSize} from '../SizeContext/SizeContext';
|
|
77
77
|
import type {BaseProps} from '../BaseProps';
|
|
78
78
|
import type {SizeValue} from '../utils/types';
|
|
@@ -844,11 +844,11 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
844
844
|
const count = filterOptionsByQuery(selectableItems, nextQuery).length;
|
|
845
845
|
announce(
|
|
846
846
|
count === 0
|
|
847
|
-
? '
|
|
848
|
-
:
|
|
847
|
+
? t('@astryx.selector.emptySearchResults')
|
|
848
|
+
: t('@astryx.selector.resultCount', {count}),
|
|
849
849
|
);
|
|
850
850
|
},
|
|
851
|
-
[announce, selectableItems],
|
|
851
|
+
[announce, selectableItems, t],
|
|
852
852
|
);
|
|
853
853
|
|
|
854
854
|
// Calculate offset to position selected item over trigger. Explicit
|
|
@@ -1055,6 +1055,15 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1055
1055
|
}
|
|
1056
1056
|
}}
|
|
1057
1057
|
onKeyDown={e => {
|
|
1058
|
+
// An in-progress IME composition uses these same keys (Enter to
|
|
1059
|
+
// commit the candidate, Escape/Arrows to navigate the candidate
|
|
1060
|
+
// window); the composing keydown fires before compositionend, so
|
|
1061
|
+
// without this guard a Korean/Japanese/Chinese user committing a
|
|
1062
|
+
// syllable with Enter would instead select the highlighted option.
|
|
1063
|
+
// See utils/ime.ts.
|
|
1064
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1058
1067
|
// Arrow keys navigate options; Enter selects; Escape closes.
|
|
1059
1068
|
// Home/End are left to the input for caret movement (APG editable
|
|
1060
1069
|
// combobox); PageUp/PageDown are the sanctioned substitute for
|