@astryxdesign/core 0.4.0 → 0.4.1-canary.1ce0b4c
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 +39 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +1 -2
- package/dist/AspectRatio/index.d.ts.map +1 -1
- package/dist/AspectRatio/index.js +1 -2
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +1 -2
- package/dist/Badge/index.d.ts.map +1 -1
- package/dist/Badge/index.js +1 -2
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +17 -3
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +1 -2
- package/dist/Blockquote/index.d.ts.map +1 -1
- package/dist/Blockquote/index.js +1 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -9
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +1 -2
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +1 -2
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +1 -2
- package/dist/Center/index.d.ts.map +1 -1
- package/dist/Center/index.js +1 -2
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +1 -2
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +7 -8
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +23 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +44 -16
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +32 -13
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
- package/dist/Field/PanelSearchInput.d.ts +81 -0
- package/dist/Field/PanelSearchInput.d.ts.map +1 -0
- package/dist/Field/PanelSearchInput.js +151 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +21 -10
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +1 -2
- package/dist/Grid/index.d.ts.map +1 -1
- package/dist/Grid/index.js +1 -2
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/Icon/globalIconRegistry.d.ts +5 -4
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +5 -4
- package/dist/Indicator/index.d.ts +1 -1
- package/dist/Indicator/index.d.ts.map +1 -1
- package/dist/Indicator/types.d.ts +10 -0
- package/dist/Indicator/types.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +155 -29
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +24 -50
- package/dist/MultiSelector/MultiSelector.d.ts +8 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +134 -102
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/Popover/usePopover.d.ts +15 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +14 -2
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -3
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +1 -2
- package/dist/Section/index.d.ts.map +1 -1
- package/dist/Section/index.js +1 -2
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +135 -95
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +70 -24
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +114 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -2
- package/dist/Skeleton/index.d.ts.map +1 -1
- package/dist/Skeleton/index.js +1 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +3 -3
- package/dist/Table/TableRow.d.ts +1 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +4 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +7 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +14 -3
- package/dist/Timestamp/formatInstant.d.ts +11 -1
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +3 -1
- package/dist/TopNav/TopNavMegaMenu.js +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -2
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
- package/dist/VisuallyHidden/index.d.ts.map +1 -1
- package/dist/VisuallyHidden/index.js +1 -2
- package/dist/astryx.css +23 -24
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +17 -4
- package/dist/theme/defineTheme.d.ts +2 -2
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +2 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +23 -0
- package/dist/utils/focusOutline.stylex.d.ts +34 -19
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +46 -33
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/interactionModality.d.ts +34 -0
- package/dist/utils/interactionModality.d.ts.map +1 -0
- package/dist/utils/interactionModality.js +70 -0
- package/locales/en.json +44 -0
- package/locales/pseudo.json +33 -0
- package/package.json +3 -3
- package/src/AppShell/AppShell.doc.mjs +3 -0
- package/src/AspectRatio/AspectRatio.tsx +0 -2
- package/src/AspectRatio/index.ts +0 -2
- package/src/Badge/Badge.tsx +0 -2
- package/src/Badge/index.ts +0 -2
- package/src/Banner/Banner.test.tsx +58 -1
- package/src/Banner/Banner.tsx +24 -5
- package/src/Blockquote/Blockquote.tsx +0 -2
- package/src/Blockquote/index.ts +0 -2
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.tsx +10 -7
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.tsx +0 -2
- package/src/Card/index.ts +0 -2
- package/src/Center/Center.tsx +0 -2
- package/src/Center/index.ts +0 -2
- package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
- package/src/Code/Code.tsx +0 -2
- package/src/CommandPalette/CommandPalette.test.tsx +54 -0
- package/src/CommandPalette/CommandPalette.tsx +5 -0
- package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
- package/src/ComplexSelector/ComplexSelector.tsx +6 -10
- package/src/DateInput/DateInput.test.tsx +19 -0
- package/src/DateInput/DateInput.tsx +3 -6
- package/src/DateRangeInput/DateRangeInput.tsx +3 -9
- package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
- package/src/DateTimeInput/DateTimeInput.tsx +23 -9
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
- package/src/Field/PanelSearchInput.tsx +271 -0
- package/src/FileInput/FileInput.tsx +21 -8
- package/src/Grid/Grid.tsx +0 -2
- package/src/Grid/index.ts +0 -2
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/Icon/globalIconRegistry.tsx +5 -4
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/Indicator/index.ts +1 -0
- package/src/Indicator/types.ts +11 -0
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +318 -7
- package/src/Layer/useLayer.tsx +229 -35
- package/src/Lightbox/Lightbox.tsx +2 -11
- package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
- package/src/MultiSelector/MultiSelector.test.tsx +135 -5
- package/src/MultiSelector/MultiSelector.tsx +170 -127
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/Popover/Popover.doc.mjs +2 -0
- package/src/Popover/usePopover.tsx +37 -4
- package/src/ProgressBar/ProgressBar.tsx +2 -8
- package/src/Section/Section.tsx +0 -2
- package/src/Section/index.ts +0 -2
- package/src/Selector/Selector.doc.mjs +10 -0
- package/src/Selector/Selector.test.tsx +386 -2
- package/src/Selector/Selector.tsx +182 -122
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +771 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +28 -10
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +92 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Skeleton/Skeleton.tsx +0 -2
- package/src/Skeleton/index.ts +0 -2
- package/src/Slider/Slider.tsx +2 -11
- package/src/Switch/Switch.tsx +10 -2
- package/src/Table/Table.test.tsx +31 -0
- package/src/Table/TableRow.tsx +6 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +125 -30
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +4 -1
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +8 -2
- package/src/TreeList/TreeList.test.tsx +3 -2
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
- package/src/VisuallyHidden/index.ts +0 -2
- package/src/hooks/index.ts +5 -1
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useInputStatusIcon.tsx +8 -8
- package/src/serverSafeComponents.test.ts +665 -0
- package/src/theme/defineTheme.ts +3 -0
- package/src/theme/generateThemeRules.test.ts +17 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +18 -0
- package/src/theme/tokens.test.ts +37 -0
- package/src/utils/focusOutline.stylex.ts +37 -23
- package/src/utils/index.ts +4 -0
- package/src/utils/interactionModality.ts +74 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
11
11
|
import {render, screen, waitFor, fireEvent, act} from '@testing-library/react';
|
|
12
|
+
import {useState} from 'react';
|
|
12
13
|
import {CommandPalette} from './CommandPalette';
|
|
13
14
|
import {createStaticSource} from '@astryxdesign/core/Typeahead';
|
|
14
15
|
import type {SearchSource, SearchableItem} from '@astryxdesign/core/Typeahead';
|
|
@@ -274,6 +275,59 @@ describe('CommandPalette', () => {
|
|
|
274
275
|
);
|
|
275
276
|
});
|
|
276
277
|
|
|
278
|
+
it('discards a search response that resolves after the palette closed', async () => {
|
|
279
|
+
// A source whose search resolves only when released, and no cancel()
|
|
280
|
+
// implementation — closing the palette must invalidate the request itself.
|
|
281
|
+
const resolvers: ((items: SearchableItem[]) => void)[] = [];
|
|
282
|
+
const source: SearchSource = {
|
|
283
|
+
bootstrap: () => [],
|
|
284
|
+
async search(): Promise<SearchableItem[]> {
|
|
285
|
+
return new Promise<SearchableItem[]>(resolve => {
|
|
286
|
+
resolvers.push(resolve);
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function Harness() {
|
|
292
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
293
|
+
return (
|
|
294
|
+
<CommandPalette
|
|
295
|
+
isOpen={isOpen}
|
|
296
|
+
onOpenChange={setIsOpen}
|
|
297
|
+
searchSource={source}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
render(<Harness />);
|
|
302
|
+
|
|
303
|
+
const input = screen.getByRole('combobox');
|
|
304
|
+
fireEvent.change(input, {target: {value: 'a'}});
|
|
305
|
+
await waitFor(() => expect(resolvers).toHaveLength(1));
|
|
306
|
+
|
|
307
|
+
// Close while the search is still in flight.
|
|
308
|
+
const dialog = screen.getByRole('dialog');
|
|
309
|
+
const escapeEvent = new KeyboardEvent('keydown', {
|
|
310
|
+
key: 'Escape',
|
|
311
|
+
bubbles: true,
|
|
312
|
+
cancelable: true,
|
|
313
|
+
});
|
|
314
|
+
dialog.dispatchEvent(escapeEvent);
|
|
315
|
+
await waitFor(() =>
|
|
316
|
+
expect(screen.getByRole('dialog', {hidden: true})).not.toHaveAttribute(
|
|
317
|
+
'open',
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// The stale response arrives after close — it must not re-commit the
|
|
322
|
+
// abandoned query/results into the closed palette.
|
|
323
|
+
await act(async () => {
|
|
324
|
+
resolvers[0]([{id: 'stale', label: 'Stale item'}]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(screen.getByRole('combobox', {hidden: true})).toHaveValue('');
|
|
328
|
+
expect(screen.queryByText('Stale item')).not.toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
277
331
|
it('does not move the highlight while typing in the search input', async () => {
|
|
278
332
|
// The palette's input already filters; letting the shared combobox
|
|
279
333
|
// typeahead also chase prefixes would drag the highlight — and Enter —
|
|
@@ -319,6 +319,11 @@ export function CommandPalette<T extends SearchableItem = SearchableItem>({
|
|
|
319
319
|
);
|
|
320
320
|
|
|
321
321
|
const handleClose = useCallback(() => {
|
|
322
|
+
// Invalidate any in-flight search. Most sources don't implement cancel(),
|
|
323
|
+
// and a response that resolves after close would still pass runSearch's
|
|
324
|
+
// version check and re-commit the stale query/results into the closed
|
|
325
|
+
// palette (visible as a ghost query on reopen while bootstrap is pending).
|
|
326
|
+
searchVersionRef.current++;
|
|
322
327
|
// Reset both committed and optimistic search on close
|
|
323
328
|
setSearch('');
|
|
324
329
|
setSearchResults([]);
|
|
@@ -177,7 +177,7 @@ describe('ComplexSelector', () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
describe('ComplexSelector popup theme target', () => {
|
|
180
|
-
it('puts astryx-complex-selector-popup on the
|
|
180
|
+
it('puts astryx-complex-selector-popup on the surface that paints, not the content box', async () => {
|
|
181
181
|
const user = userEvent.setup();
|
|
182
182
|
render(
|
|
183
183
|
<ComplexSelector label="Fruit blend" value="Apple" triggerLabel="Apple">
|
|
@@ -186,18 +186,27 @@ describe('ComplexSelector popup theme target', () => {
|
|
|
186
186
|
);
|
|
187
187
|
await user.click(screen.getByRole('button', {name: 'Fruit blend'}));
|
|
188
188
|
|
|
189
|
-
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
190
189
|
const popup = document.querySelector(
|
|
191
190
|
'.astryx-complex-selector-popup',
|
|
192
191
|
) as HTMLElement;
|
|
193
192
|
expect(popup).not.toBeNull();
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
|
|
194
|
+
// The surface is the element usePopover renders: it carries the dialog
|
|
195
|
+
// role and the shared surface class, and the component's content box —
|
|
196
|
+
// the one with the padding and the scroll — sits INSIDE it. A target on
|
|
197
|
+
// that inner box cannot paint the popup's background or radius, which is
|
|
198
|
+
// what a theme reaches for this class to do.
|
|
199
|
+
expect(popup).toHaveAttribute('role', 'dialog');
|
|
200
|
+
expect(popup).toHaveClass('astryx-popover-surface');
|
|
201
|
+
expect(popup.querySelector('[id]')).not.toBeNull();
|
|
198
202
|
expect(popup).toContainElement(
|
|
199
203
|
screen.getByRole('button', {name: 'Done', ...h}),
|
|
200
204
|
);
|
|
205
|
+
|
|
206
|
+
// And it is not the bare positioning layer either.
|
|
207
|
+
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
208
|
+
expect(popup).not.toBe(layer);
|
|
209
|
+
expect(layer.contains(popup)).toBe(true);
|
|
201
210
|
});
|
|
202
211
|
|
|
203
212
|
it('keeps the target when the consumer also passes contentXstyle', async () => {
|
|
@@ -292,6 +292,11 @@ export function ComplexSelector<Value>({
|
|
|
292
292
|
dialogLabel: label,
|
|
293
293
|
hasCloseButton: false,
|
|
294
294
|
hasAutoFocus: true,
|
|
295
|
+
// The popup's theme target belongs on the SURFACE — the element painting
|
|
296
|
+
// background, radius and elevation — which `usePopover` owns. Rendered on
|
|
297
|
+
// the content box below it, a theme's background or radius rule paints the
|
|
298
|
+
// wrong box.
|
|
299
|
+
surfaceTarget: 'complex-selector-popup',
|
|
295
300
|
onHide: () => {
|
|
296
301
|
document.getElementById(triggerId)?.focus();
|
|
297
302
|
},
|
|
@@ -313,16 +318,7 @@ export function ComplexSelector<Value>({
|
|
|
313
318
|
const triggerContent = triggerLabel ?? placeholder;
|
|
314
319
|
|
|
315
320
|
const content = (
|
|
316
|
-
|
|
317
|
-
// bare positioning box (useLayer zeroes its borders, padding and
|
|
318
|
-
// background), so this content box is the surface a theme has to reach to
|
|
319
|
-
// paint the popup.
|
|
320
|
-
<div
|
|
321
|
-
id={contentId}
|
|
322
|
-
{...mergeProps(
|
|
323
|
-
themeProps('complex-selector-popup'),
|
|
324
|
-
stylex.props(styles.content, contentXstyle),
|
|
325
|
-
)}>
|
|
321
|
+
<div id={contentId} {...stylex.props(styles.content, contentXstyle)}>
|
|
326
322
|
{children(optimisticValue, commitValue, popover.hide, {
|
|
327
323
|
isOpen: popover.isOpen,
|
|
328
324
|
isBusy,
|
|
@@ -25,6 +25,7 @@ import {InputGroup} from '../InputGroup';
|
|
|
25
25
|
import {InputGroupText} from '../InputGroup/InputGroupText';
|
|
26
26
|
import {defineTheme} from '../theme/defineTheme';
|
|
27
27
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
28
|
+
import {InternationalizationProvider} from '../i18n';
|
|
28
29
|
|
|
29
30
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
30
31
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -170,6 +171,24 @@ describe('DateInput', () => {
|
|
|
170
171
|
expect(screen.queryByText('Invalid date')).not.toBeInTheDocument();
|
|
171
172
|
});
|
|
172
173
|
|
|
174
|
+
it('resolves the invalid-date announcement from the i18n catalog', () => {
|
|
175
|
+
const {container} = render(
|
|
176
|
+
<InternationalizationProvider
|
|
177
|
+
locale="en"
|
|
178
|
+
overrides={{en: {'@astryx.dateInput.invalidDate': 'Ungültiges Datum'}}}>
|
|
179
|
+
<DateInput label="Date" onChange={() => {}} />
|
|
180
|
+
</InternationalizationProvider>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
fireEvent.change(screen.getByRole('combobox'), {
|
|
184
|
+
target: {value: '13/45/2024'},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(within(container).getByRole('alert')).toHaveTextContent(
|
|
188
|
+
'Ungültiges Datum',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
173
192
|
it('reverts to previous value on blur when input is invalid', async () => {
|
|
174
193
|
const onChange = vi.fn();
|
|
175
194
|
render(<DateInput label="Date" value="2026-01-25" onChange={onChange} />);
|
|
@@ -80,11 +80,6 @@ const styles = stylex.create({
|
|
|
80
80
|
backgroundColor: 'transparent',
|
|
81
81
|
cursor: 'pointer',
|
|
82
82
|
borderRadius: radiusVars['--radius-element'],
|
|
83
|
-
outline: {
|
|
84
|
-
default: 'none',
|
|
85
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
86
|
-
},
|
|
87
|
-
outlineOffset: 1,
|
|
88
83
|
},
|
|
89
84
|
iconButtonDisabled: {
|
|
90
85
|
cursor: 'not-allowed',
|
|
@@ -163,6 +158,7 @@ import {mergeProps, mergeRefs, isFocusDetached} from '../utils';
|
|
|
163
158
|
import type {BaseProps} from '../BaseProps';
|
|
164
159
|
import type {SizeValue} from '../utils/types';
|
|
165
160
|
import {themeProps} from '../utils/themeProps';
|
|
161
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
166
162
|
import {stableClassName} from '../naming';
|
|
167
163
|
import {useTranslator} from '../i18n';
|
|
168
164
|
|
|
@@ -694,6 +690,7 @@ export function DateInput({
|
|
|
694
690
|
: t('@astryx.dateInput.openCalendar')
|
|
695
691
|
}
|
|
696
692
|
{...stylex.props(
|
|
693
|
+
focusOutlineStyles.focusVisible,
|
|
697
694
|
styles.iconButton,
|
|
698
695
|
isEffectivelyDisabled && styles.iconButtonDisabled,
|
|
699
696
|
)}>
|
|
@@ -754,7 +751,7 @@ export function DateInput({
|
|
|
754
751
|
user would get no feedback that their entry was rejected (WCAG 3.3.1).
|
|
755
752
|
*/}
|
|
756
753
|
<VisuallyHidden as="div" role="alert" aria-live="assertive">
|
|
757
|
-
{!isInputValid ? '
|
|
754
|
+
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
|
|
758
755
|
</VisuallyHidden>
|
|
759
756
|
{hasClear && value !== undefined && !isEffectivelyDisabled && (
|
|
760
757
|
<InputClearButton
|
|
@@ -61,6 +61,7 @@ import type {SizeValue} from '../utils/types';
|
|
|
61
61
|
import {useSize} from '../SizeContext/SizeContext';
|
|
62
62
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
63
63
|
import {themeProps} from '../utils/themeProps';
|
|
64
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
64
65
|
import {stableClassName} from '../naming';
|
|
65
66
|
import {useTranslator} from '../i18n';
|
|
66
67
|
|
|
@@ -118,11 +119,6 @@ const styles = stylex.create({
|
|
|
118
119
|
backgroundColor: 'transparent',
|
|
119
120
|
cursor: 'pointer',
|
|
120
121
|
borderRadius: radiusVars['--radius-element'],
|
|
121
|
-
outline: {
|
|
122
|
-
default: 'none',
|
|
123
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
124
|
-
},
|
|
125
|
-
outlineOffset: 1,
|
|
126
122
|
},
|
|
127
123
|
iconButtonDisabled: {
|
|
128
124
|
cursor: 'not-allowed',
|
|
@@ -160,10 +156,6 @@ const styles = stylex.create({
|
|
|
160
156
|
color: colorVars['--color-text-primary'],
|
|
161
157
|
cursor: 'pointer',
|
|
162
158
|
textAlign: 'start',
|
|
163
|
-
outline: {
|
|
164
|
-
default: 'none',
|
|
165
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
166
|
-
},
|
|
167
159
|
},
|
|
168
160
|
presetButtonActive: {
|
|
169
161
|
backgroundColor: colorVars['--color-accent-muted'],
|
|
@@ -592,6 +584,7 @@ export function DateRangeInput({
|
|
|
592
584
|
}
|
|
593
585
|
tabIndex={-1}
|
|
594
586
|
{...stylex.props(
|
|
587
|
+
focusOutlineStyles.focusVisible,
|
|
595
588
|
styles.iconButton,
|
|
596
589
|
isEffectivelyDisabled && styles.iconButtonDisabled,
|
|
597
590
|
)}>
|
|
@@ -661,6 +654,7 @@ export function DateRangeInput({
|
|
|
661
654
|
aria-current={isActive ? 'true' : undefined}
|
|
662
655
|
onClick={() => handlePresetClick(preset)}
|
|
663
656
|
{...stylex.props(
|
|
657
|
+
focusOutlineStyles.focusVisible,
|
|
664
658
|
styles.presetButton,
|
|
665
659
|
isActive && styles.presetButtonActive,
|
|
666
660
|
)}>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SYNC: When DateTimeInput.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
12
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {getButton, queryButton} from '../__tests__/fastRoleQueries';
|
|
@@ -17,6 +17,16 @@ import {DateTimeInput} from './DateTimeInput';
|
|
|
17
17
|
import type {ISODateTimeString} from './DateTimeInput';
|
|
18
18
|
import {defineTheme} from '../theme/defineTheme';
|
|
19
19
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
20
|
+
import {InternationalizationProvider} from '../i18n';
|
|
21
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
22
|
+
|
|
23
|
+
function politeRegion(): HTMLElement | null {
|
|
24
|
+
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
__resetLiveRegionsForTest();
|
|
29
|
+
});
|
|
20
30
|
|
|
21
31
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
22
32
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -626,6 +636,35 @@ describe('DateTimeInput', () => {
|
|
|
626
636
|
|
|
627
637
|
expect(screen.getByText('Invalid time')).toBeInTheDocument();
|
|
628
638
|
});
|
|
639
|
+
|
|
640
|
+
it('resolves the invalid date and time announcements from the i18n catalog', () => {
|
|
641
|
+
render(
|
|
642
|
+
<InternationalizationProvider
|
|
643
|
+
locale="en"
|
|
644
|
+
overrides={{
|
|
645
|
+
en: {
|
|
646
|
+
'@astryx.dateInput.invalidDate': 'Ungültiges Datum',
|
|
647
|
+
'@astryx.timeInput.invalidTime': 'Ungültige Zeit',
|
|
648
|
+
},
|
|
649
|
+
}}>
|
|
650
|
+
<DateTimeInput
|
|
651
|
+
label="Meeting"
|
|
652
|
+
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
653
|
+
onChange={() => {}}
|
|
654
|
+
/>
|
|
655
|
+
</InternationalizationProvider>,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
fireEvent.change(screen.getByRole('combobox'), {
|
|
659
|
+
target: {value: '13/45/2024'},
|
|
660
|
+
});
|
|
661
|
+
expect(screen.getByText('Ungültiges Datum')).toBeInTheDocument();
|
|
662
|
+
|
|
663
|
+
fireEvent.change(screen.getByLabelText('Meeting time'), {
|
|
664
|
+
target: {value: '99:99 zz'},
|
|
665
|
+
});
|
|
666
|
+
expect(screen.getByText('Ungültige Zeit')).toBeInTheDocument();
|
|
667
|
+
});
|
|
629
668
|
});
|
|
630
669
|
describe('disabledMessage', () => {
|
|
631
670
|
// jsdom does not implement the Popover API used by the tooltip, so mock
|
|
@@ -819,6 +858,45 @@ describe('DateTimeInput', () => {
|
|
|
819
858
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
820
859
|
expect(onChange.mock.calls[0][0]).toContain('14:31');
|
|
821
860
|
});
|
|
861
|
+
|
|
862
|
+
// Arrow-key stepping mutates a plain textbox programmatically, and screen
|
|
863
|
+
// readers do not announce programmatic textbox changes — the new value
|
|
864
|
+
// must be announced through the polite live region (WCAG 4.1.2).
|
|
865
|
+
it('politely announces the new time after ArrowUp stepping', async () => {
|
|
866
|
+
render(
|
|
867
|
+
<DateTimeInput
|
|
868
|
+
label="Meeting"
|
|
869
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
870
|
+
onChange={() => {}}
|
|
871
|
+
/>,
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
875
|
+
key: 'ArrowUp',
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
await waitFor(() => {
|
|
879
|
+
expect(politeRegion()).toHaveTextContent('2:31 PM');
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('politely announces the new time after ArrowDown stepping', async () => {
|
|
884
|
+
render(
|
|
885
|
+
<DateTimeInput
|
|
886
|
+
label="Meeting"
|
|
887
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
888
|
+
onChange={() => {}}
|
|
889
|
+
/>,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
893
|
+
key: 'ArrowDown',
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
await waitFor(() => {
|
|
897
|
+
expect(politeRegion()).toHaveTextContent('2:29 PM');
|
|
898
|
+
});
|
|
899
|
+
});
|
|
822
900
|
});
|
|
823
901
|
|
|
824
902
|
describe('weekStartsOn', () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file DateTimeInput.tsx
|
|
7
|
-
* @input Uses React, Field, Calendar, usePopover, time parsing utilities
|
|
7
|
+
* @input Uses React, Field, Calendar, usePopover, useAnnounce, time parsing utilities
|
|
8
8
|
* @output Exports DateTimeInput component, DateTimeInputProps
|
|
9
9
|
* @position Core implementation; consumed by index.ts, tested by DateTimeInput.test.tsx
|
|
10
10
|
*
|
|
@@ -82,8 +82,10 @@ import {
|
|
|
82
82
|
|
|
83
83
|
import type {BaseProps} from '../BaseProps';
|
|
84
84
|
import type {SizeValue} from '../utils/types';
|
|
85
|
+
import {useAnnounce} from '../hooks/useAnnounce';
|
|
85
86
|
import {useSize} from '../SizeContext/SizeContext';
|
|
86
87
|
import {themeProps} from '../utils/themeProps';
|
|
88
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
87
89
|
import {useTranslator} from '../i18n';
|
|
88
90
|
|
|
89
91
|
export type ISODateTimeString = string & {
|
|
@@ -118,11 +120,6 @@ const styles = stylex.create({
|
|
|
118
120
|
backgroundColor: 'transparent',
|
|
119
121
|
cursor: 'pointer',
|
|
120
122
|
borderRadius: radiusVars['--radius-element'],
|
|
121
|
-
outline: {
|
|
122
|
-
default: 'none',
|
|
123
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
124
|
-
},
|
|
125
|
-
outlineOffset: 1,
|
|
126
123
|
},
|
|
127
124
|
iconButtonDisabled: {
|
|
128
125
|
cursor: 'not-allowed',
|
|
@@ -450,6 +447,10 @@ export function DateTimeInput({
|
|
|
450
447
|
...rest
|
|
451
448
|
}: DateTimeInputProps) {
|
|
452
449
|
const t = useTranslator();
|
|
450
|
+
// Speaks arrow-key stepping results through the persistent live regions:
|
|
451
|
+
// stepping programmatically rewrites a plain textbox's value, which screen
|
|
452
|
+
// readers do not announce on their own (WCAG 4.1.2).
|
|
453
|
+
const announce = useAnnounce();
|
|
453
454
|
const placeholder =
|
|
454
455
|
placeholderFromProps ?? t('@astryx.dateTimeInput.placeholder');
|
|
455
456
|
const timePlaceholder =
|
|
@@ -855,11 +856,23 @@ export function DateTimeInput({
|
|
|
855
856
|
const combined = combineDateTime(valueParts.date, newTime);
|
|
856
857
|
if (combined) {
|
|
857
858
|
fireChange(combined);
|
|
859
|
+
// Screen readers do not announce the programmatic value rewrite,
|
|
860
|
+
// so speak the new time explicitly (WCAG 4.1.2).
|
|
861
|
+
announce(formatDisplayTime(newTime, hasSeconds));
|
|
858
862
|
}
|
|
859
863
|
}
|
|
860
864
|
}
|
|
861
865
|
},
|
|
862
|
-
[
|
|
866
|
+
[
|
|
867
|
+
valueParts,
|
|
868
|
+
hasSeconds,
|
|
869
|
+
timeIncrement,
|
|
870
|
+
timeMin,
|
|
871
|
+
timeMax,
|
|
872
|
+
fireChange,
|
|
873
|
+
announce,
|
|
874
|
+
formatDisplayTime,
|
|
875
|
+
],
|
|
863
876
|
);
|
|
864
877
|
|
|
865
878
|
// --- Clear ---
|
|
@@ -941,6 +954,7 @@ export function DateTimeInput({
|
|
|
941
954
|
: t('@astryx.dateInput.openCalendar')
|
|
942
955
|
}
|
|
943
956
|
{...stylex.props(
|
|
957
|
+
focusOutlineStyles.focusVisible,
|
|
944
958
|
styles.iconButton,
|
|
945
959
|
isEffectivelyDisabled && styles.iconButtonDisabled,
|
|
946
960
|
)}>
|
|
@@ -988,7 +1002,7 @@ export function DateTimeInput({
|
|
|
988
1002
|
rejected (WCAG 3.3.1).
|
|
989
1003
|
*/}
|
|
990
1004
|
<VisuallyHidden as="div" role="alert" aria-live="assertive">
|
|
991
|
-
{!isDateInputValid ? '
|
|
1005
|
+
{!isDateInputValid ? t('@astryx.dateInput.invalidDate') : ''}
|
|
992
1006
|
</VisuallyHidden>
|
|
993
1007
|
{hasClear && value !== undefined && !isEffectivelyDisabled && (
|
|
994
1008
|
<InputClearButton
|
|
@@ -1061,7 +1075,7 @@ export function DateTimeInput({
|
|
|
1061
1075
|
technology (WCAG 3.3.1).
|
|
1062
1076
|
*/}
|
|
1063
1077
|
<VisuallyHidden as="div" role="alert" aria-live="assertive">
|
|
1064
|
-
{!isTimeInputValid ? '
|
|
1078
|
+
{!isTimeInputValid ? t('@astryx.timeInput.invalidTime') : ''}
|
|
1065
1079
|
</VisuallyHidden>
|
|
1066
1080
|
</div>
|
|
1067
1081
|
</div>
|
|
@@ -92,6 +92,7 @@ export function DropdownMenuRadioGroup({
|
|
|
92
92
|
label,
|
|
93
93
|
hasCloseOnSelect = true,
|
|
94
94
|
children,
|
|
95
|
+
xstyle,
|
|
95
96
|
className,
|
|
96
97
|
style,
|
|
97
98
|
...rest
|
|
@@ -106,7 +107,7 @@ export function DropdownMenuRadioGroup({
|
|
|
106
107
|
{...rest}
|
|
107
108
|
role="group"
|
|
108
109
|
aria-label={label}
|
|
109
|
-
{...mergeProps(stylex.props(styles.group), {className, style})}>
|
|
110
|
+
{...mergeProps(stylex.props(styles.group, xstyle), {className, style})}>
|
|
110
111
|
<DropdownMenuRadioGroupContext value={contextValue}>
|
|
111
112
|
{children}
|
|
112
113
|
</DropdownMenuRadioGroupContext>
|