@astryxdesign/core 0.4.2 → 0.4.3-canary.00bed29
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 +45 -0
- package/README.md +79 -45
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +3 -19
- package/dist/Banner/Banner.d.ts +9 -2
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +84 -33
- package/dist/Banner/index.d.ts +14 -12
- package/dist/Banner/index.d.ts.map +1 -1
- package/dist/Banner/index.js +10 -8
- 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 +942 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +98 -31
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- 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/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- 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/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +4 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +2 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +14 -3
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/columnUtils.d.ts.map +1 -1
- package/dist/Table/columnUtils.js +4 -1
- package/dist/TextArea/TextArea.d.ts +6 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +17 -6
- 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/TreeList/TreeList.js +2 -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 +25 -2
- 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 +30 -7
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +36 -47
- 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/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/utils/characters.d.ts +27 -0
- package/dist/utils/characters.d.ts.map +1 -0
- package/dist/utils/characters.js +83 -0
- 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 +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/locales/en.json +48 -0
- package/locales/pseudo.json +36 -0
- package/package.json +9 -8
- package/src/Avatar/Avatar.test.tsx +6 -1
- package/src/Avatar/Avatar.tsx +3 -21
- package/src/Banner/Banner.doc.mjs +12 -10
- package/src/Banner/Banner.test.tsx +108 -0
- package/src/Banner/Banner.tsx +124 -38
- package/src/Banner/index.ts +15 -13
- 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 +1014 -0
- package/src/BottomSheet/useSheetGestures.ts +1407 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +156 -28
- package/src/ComplexSelector/index.ts +3 -1
- 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/FileInput/FileInput.test.tsx +53 -9
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- 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/PowerSearch.test.tsx +48 -3
- package/src/PowerSearch/PowerSearch.tsx +4 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/formatFilterValue.test.ts +22 -0
- package/src/PowerSearch/formatFilterValue.ts +2 -4
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +13 -4
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/Table.test.tsx +6 -0
- package/src/Table/columnUtils.ts +3 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +72 -0
- package/src/TextArea/TextArea.tsx +26 -8
- 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/TreeList/TreeList.doc.mjs +2 -2
- package/src/TreeList/TreeList.tsx +1 -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.test.ts +127 -0
- package/src/theme/defineTheme.ts +70 -55
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/theme/extensibleAxes.test.ts +365 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/utils/characters.test.ts +141 -0
- package/src/utils/characters.ts +88 -0
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +4 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MobileNavCloseVisibility.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, MobileNav
|
|
6
|
+
* @output Regression tests keeping the drawer rendered until the dialog closes
|
|
7
|
+
* @position Testing; guards the close path of MobileNav.tsx
|
|
8
|
+
*
|
|
9
|
+
* Repro for #4290 (page unresponsive after closing the drawer in Safari 26.1).
|
|
10
|
+
*
|
|
11
|
+
* A `<dialog>` opened with `showModal()` blocks the rest of the document for as
|
|
12
|
+
* long as it stays in the top layer — whether or not it is rendered. The drawer
|
|
13
|
+
* hid itself the moment `isOpen` flipped, while the native `close()` only ran
|
|
14
|
+
* afterwards from an effect, so `close()` was always called on a dialog that was
|
|
15
|
+
* already `display: none`. A browser that fails to un-block the document when
|
|
16
|
+
* such a dialog closes leaves the page inert with no JavaScript error.
|
|
17
|
+
*
|
|
18
|
+
* The fix keeps `display` in the transition with `allow-discrete`, so the drawer
|
|
19
|
+
* stays rendered until the slide-out finishes, and moves the unmount close into
|
|
20
|
+
* its own effect so the deferred close is no longer cut off by its own cleanup.
|
|
21
|
+
*
|
|
22
|
+
* Note on scope: jsdom has no top layer and no CSS transitions, so the inertness
|
|
23
|
+
* itself is not observable here. These tests pin the two mechanisms that prevent
|
|
24
|
+
* it — the declared transition, and the close actually being deferred.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {describe, it, expect, vi, beforeAll} from 'vitest';
|
|
28
|
+
import {render, screen, act} from '@testing-library/react';
|
|
29
|
+
import {MobileNav} from './MobileNav';
|
|
30
|
+
import {stubMatchMedia} from '../__tests__/stubMatchMedia';
|
|
31
|
+
|
|
32
|
+
// jsdom doesn't implement showModal/close on <dialog>, so we mock them
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
HTMLDialogElement.prototype.showModal =
|
|
35
|
+
HTMLDialogElement.prototype.showModal ||
|
|
36
|
+
function (this: HTMLDialogElement) {
|
|
37
|
+
this.setAttribute('open', '');
|
|
38
|
+
};
|
|
39
|
+
HTMLDialogElement.prototype.close =
|
|
40
|
+
HTMLDialogElement.prototype.close ||
|
|
41
|
+
function (this: HTMLDialogElement) {
|
|
42
|
+
this.removeAttribute('open');
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function Drawer({isOpen}: {isOpen: boolean}) {
|
|
47
|
+
return (
|
|
48
|
+
<MobileNav isOpen={isOpen} onOpenChange={() => {}} data-testid="mobile-nav">
|
|
49
|
+
<span>Nav content</span>
|
|
50
|
+
</MobileNav>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('MobileNav stays rendered while it closes', () => {
|
|
55
|
+
it('transitions display with allow-discrete so it hides only once closed', () => {
|
|
56
|
+
render(<Drawer isOpen />);
|
|
57
|
+
const style = getComputedStyle(screen.getByTestId('mobile-nav'));
|
|
58
|
+
|
|
59
|
+
// Without these two the drawer blanks out the instant `isOpen` flips,
|
|
60
|
+
// leaving an open modal dialog that nothing renders — and nothing can
|
|
61
|
+
// click past. See the file header.
|
|
62
|
+
expect(style.transitionProperty).toContain('display');
|
|
63
|
+
expect(style.transitionBehavior).toBe('allow-discrete');
|
|
64
|
+
// A zero hold is the bug wearing the fix's clothes: the drawer stops being
|
|
65
|
+
// rendered on the next style recalc, before any effect can close it.
|
|
66
|
+
expect(style.transitionDuration.trim()).not.toBe('');
|
|
67
|
+
// The hold must stay tied to the motion token rather than a literal: the
|
|
68
|
+
// close delay is derived by reading this value back, so a hardcoded hold
|
|
69
|
+
// and a themed slide-out would drift apart again. jsdom leaves var()
|
|
70
|
+
// unresolved, which is exactly why this reads as a declaration.
|
|
71
|
+
expect(style.transitionDuration).toContain('var(');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps the native dialog open until the slide-out has run', () => {
|
|
75
|
+
vi.useFakeTimers();
|
|
76
|
+
try {
|
|
77
|
+
const {rerender} = render(<Drawer isOpen />);
|
|
78
|
+
const dialog = screen.getByTestId('mobile-nav');
|
|
79
|
+
expect(dialog).toHaveAttribute('open');
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
rerender(<Drawer isOpen={false} />);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The drawer is on its way out but the dialog is still open, so it is
|
|
86
|
+
// still rendered and still the thing holding the top layer.
|
|
87
|
+
expect(dialog).toHaveAttribute('open');
|
|
88
|
+
|
|
89
|
+
act(() => {
|
|
90
|
+
vi.advanceTimersByTime(300);
|
|
91
|
+
});
|
|
92
|
+
expect(dialog).not.toHaveAttribute('open');
|
|
93
|
+
} finally {
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still closes the dialog when unmounted mid-slide-out', () => {
|
|
99
|
+
vi.useFakeTimers();
|
|
100
|
+
try {
|
|
101
|
+
const {rerender, unmount} = render(<Drawer isOpen />);
|
|
102
|
+
const dialog = screen.getByTestId('mobile-nav');
|
|
103
|
+
|
|
104
|
+
act(() => {
|
|
105
|
+
rerender(<Drawer isOpen={false} />);
|
|
106
|
+
});
|
|
107
|
+
expect(dialog).toHaveAttribute('open');
|
|
108
|
+
|
|
109
|
+
// Teardown before the timer fires — the unmount-only effect has to close
|
|
110
|
+
// the dialog, or the next showModal() is skipped and the drawer can never
|
|
111
|
+
// be re-opened (#3091).
|
|
112
|
+
unmount();
|
|
113
|
+
expect(dialog).not.toHaveAttribute('open');
|
|
114
|
+
} finally {
|
|
115
|
+
vi.useRealTimers();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('still defers the close under prefers-reduced-motion', () => {
|
|
120
|
+
// Query-aware on purpose. A blanket `matches: true` is the stub shape that
|
|
121
|
+
// once left 9 of 11 tests in the sibling edge-case file silently running
|
|
122
|
+
// against a 0ms close delay — see the header of stubMatchMedia.ts. It is
|
|
123
|
+
// inert for this file today (MobileNav renders standalone here, so
|
|
124
|
+
// prefers-reduced-motion is the only query in play), but the shape is the
|
|
125
|
+
// hazard, not the blast radius.
|
|
126
|
+
stubMatchMedia({reduceMotion: true});
|
|
127
|
+
vi.useFakeTimers();
|
|
128
|
+
try {
|
|
129
|
+
const {rerender} = render(<Drawer isOpen />);
|
|
130
|
+
const dialog = screen.getByTestId('mobile-nav');
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
rerender(<Drawer isOpen={false} />);
|
|
134
|
+
});
|
|
135
|
+
// Reduced motion caps the delay at 0 — but 0 is still a macrotask. What
|
|
136
|
+
// must not happen is the close landing synchronously in the commit, while
|
|
137
|
+
// the dialog is mid-flip. This pins deferral, not duration.
|
|
138
|
+
expect(dialog).toHaveAttribute('open');
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
vi.advanceTimersByTime(20);
|
|
142
|
+
});
|
|
143
|
+
expect(dialog).not.toHaveAttribute('open');
|
|
144
|
+
} finally {
|
|
145
|
+
vi.useRealTimers();
|
|
146
|
+
vi.unstubAllGlobals();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('re-opening mid-slide-out cancels the pending close', () => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
try {
|
|
153
|
+
const {rerender} = render(<Drawer isOpen />);
|
|
154
|
+
const dialog = screen.getByTestId('mobile-nav');
|
|
155
|
+
|
|
156
|
+
act(() => {
|
|
157
|
+
rerender(<Drawer isOpen={false} />);
|
|
158
|
+
});
|
|
159
|
+
act(() => {
|
|
160
|
+
rerender(<Drawer isOpen />);
|
|
161
|
+
});
|
|
162
|
+
act(() => {
|
|
163
|
+
vi.advanceTimersByTime(300);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// The queued close must not fire after the drawer has been re-opened.
|
|
167
|
+
expect(dialog).toHaveAttribute('open');
|
|
168
|
+
} finally {
|
|
169
|
+
vi.useRealTimers();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -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);
|
|
@@ -158,6 +158,41 @@ describe('PowerSearch', () => {
|
|
|
158
158
|
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
describe('token value truncation (#4759)', () => {
|
|
162
|
+
it('truncates token values by characters, not code units', () => {
|
|
163
|
+
const truncConfig: PowerSearchConfig = {
|
|
164
|
+
name: 'trunc',
|
|
165
|
+
fields: [
|
|
166
|
+
{
|
|
167
|
+
key: 'status',
|
|
168
|
+
label: 'Status',
|
|
169
|
+
operators: [{key: 'is', label: 'is', value: {type: 'string'}}],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
// 14 emoji = 28 code units but 14 user-perceived characters.
|
|
174
|
+
// adjustedMaxLength = max(15 - 'Status'.length - 'is'.length, 10) = 10,
|
|
175
|
+
// so the value truncates only past 13 characters, cutting at 10.
|
|
176
|
+
render(
|
|
177
|
+
<PowerSearch
|
|
178
|
+
config={truncConfig}
|
|
179
|
+
filters={[
|
|
180
|
+
{
|
|
181
|
+
field: 'status',
|
|
182
|
+
operator: 'is',
|
|
183
|
+
value: {type: 'string', value: '\u{1F600}'.repeat(14)},
|
|
184
|
+
},
|
|
185
|
+
]}
|
|
186
|
+
onChange={() => {}}
|
|
187
|
+
maxTokenLength={15}
|
|
188
|
+
/>,
|
|
189
|
+
);
|
|
190
|
+
expect(
|
|
191
|
+
screen.getByText('\u{1F600}'.repeat(10) + '...'),
|
|
192
|
+
).toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
161
196
|
describe('startIcon', () => {
|
|
162
197
|
it('does not render a start icon when omitted', () => {
|
|
163
198
|
render(<PowerSearch config={config} filters={[]} onChange={() => {}} />);
|
|
@@ -425,11 +460,15 @@ describe('PowerSearch', () => {
|
|
|
425
460
|
});
|
|
426
461
|
});
|
|
427
462
|
|
|
428
|
-
|
|
429
463
|
describe('PowerSearch statusVariant forwarding', () => {
|
|
430
464
|
it('defaults to attached (status renders with data-variant="attached")', () => {
|
|
431
465
|
const {container} = render(
|
|
432
|
-
<PowerSearch
|
|
466
|
+
<PowerSearch
|
|
467
|
+
config={config}
|
|
468
|
+
filters={[]}
|
|
469
|
+
onChange={() => {}}
|
|
470
|
+
status={{type: 'error', message: 'Required'}}
|
|
471
|
+
/>,
|
|
433
472
|
);
|
|
434
473
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
435
474
|
'data-variant',
|
|
@@ -439,7 +478,13 @@ describe('PowerSearch statusVariant forwarding', () => {
|
|
|
439
478
|
|
|
440
479
|
it('forwards statusVariant="detached" to the underlying Field status', () => {
|
|
441
480
|
const {container} = render(
|
|
442
|
-
<PowerSearch
|
|
481
|
+
<PowerSearch
|
|
482
|
+
config={config}
|
|
483
|
+
filters={[]}
|
|
484
|
+
onChange={() => {}}
|
|
485
|
+
status={{type: 'error', message: 'Required'}}
|
|
486
|
+
statusVariant="detached"
|
|
487
|
+
/>,
|
|
443
488
|
);
|
|
444
489
|
expect(container.querySelector('.astryx-field-status')).toHaveAttribute(
|
|
445
490
|
'data-variant',
|