@astryxdesign/core 0.4.0 → 0.4.1
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 +2 -2
- 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/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/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/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/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/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 +18 -17
- package/dist/astryx.umd.js +45 -45
- package/dist/astryx.umd.js.map +4 -4
- 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 +40 -0
- package/locales/pseudo.json +30 -0
- package/package.json +2 -2
- 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 +5 -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/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/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/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/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/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +91 -14
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- 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/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/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
|
@@ -14,6 +14,7 @@ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {TimeInput} from './TimeInput';
|
|
16
16
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
17
|
+
import {InternationalizationProvider} from '../i18n';
|
|
17
18
|
import type {ISOTimeString} from '../utils';
|
|
18
19
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
19
20
|
|
|
@@ -209,6 +210,59 @@ describe('TimeInput', () => {
|
|
|
209
210
|
expect(screen.queryByText('Invalid time')).not.toBeInTheDocument();
|
|
210
211
|
});
|
|
211
212
|
|
|
213
|
+
it('resolves the invalid-time announcement from the i18n catalog', () => {
|
|
214
|
+
render(
|
|
215
|
+
<InternationalizationProvider
|
|
216
|
+
locale="en"
|
|
217
|
+
overrides={{en: {'@astryx.timeInput.invalidTime': 'Ungültige Zeit'}}}>
|
|
218
|
+
<TimeInput label="Time" onChange={() => {}} />
|
|
219
|
+
</InternationalizationProvider>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
fireEvent.change(screen.getByRole('textbox'), {target: {value: '25:99'}});
|
|
223
|
+
|
|
224
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Ungültige Zeit');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Arrow-key stepping mutates a plain textbox programmatically, and screen
|
|
228
|
+
// readers do not announce programmatic textbox changes — the new value must
|
|
229
|
+
// be announced through the polite live region (WCAG 4.1.2).
|
|
230
|
+
it('politely announces the new time after ArrowUp stepping', async () => {
|
|
231
|
+
const onChange = vi.fn();
|
|
232
|
+
render(
|
|
233
|
+
<TimeInput
|
|
234
|
+
label="Time"
|
|
235
|
+
value={'14:30' as ISOTimeString}
|
|
236
|
+
onChange={onChange}
|
|
237
|
+
/>,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
fireEvent.keyDown(screen.getByRole('textbox'), {key: 'ArrowUp'});
|
|
241
|
+
|
|
242
|
+
expect(onChange).toHaveBeenCalledWith('14:31');
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(politeRegion()).toHaveTextContent('2:31 PM');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('politely announces the new time after ArrowDown stepping', async () => {
|
|
249
|
+
const onChange = vi.fn();
|
|
250
|
+
render(
|
|
251
|
+
<TimeInput
|
|
252
|
+
label="Time"
|
|
253
|
+
value={'14:30' as ISOTimeString}
|
|
254
|
+
onChange={onChange}
|
|
255
|
+
/>,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
fireEvent.keyDown(screen.getByRole('textbox'), {key: 'ArrowDown'});
|
|
259
|
+
|
|
260
|
+
expect(onChange).toHaveBeenCalledWith('14:29');
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(politeRegion()).toHaveTextContent('2:29 PM');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
212
266
|
it('calls onChange on blur when input is valid', async () => {
|
|
213
267
|
const user = userEvent.setup();
|
|
214
268
|
const onChange = vi.fn();
|
|
@@ -558,10 +558,25 @@ export function TimeInput({
|
|
|
558
558
|
// Check if within range
|
|
559
559
|
if (isTimeInRange(newTime, min, max)) {
|
|
560
560
|
fireChange(newTime);
|
|
561
|
+
// Stepping programmatically rewrites a plain textbox's value, and
|
|
562
|
+
// screen readers do not announce programmatic textbox changes — the
|
|
563
|
+
// new value must be spoken explicitly or stepping is silent
|
|
564
|
+
// (WCAG 4.1.2).
|
|
565
|
+
announce(formatDisplayTime(newTime, hasSeconds));
|
|
561
566
|
}
|
|
562
567
|
}
|
|
563
568
|
},
|
|
564
|
-
[
|
|
569
|
+
[
|
|
570
|
+
value,
|
|
571
|
+
hasSeconds,
|
|
572
|
+
increment,
|
|
573
|
+
min,
|
|
574
|
+
max,
|
|
575
|
+
fireChange,
|
|
576
|
+
isDisabled,
|
|
577
|
+
announce,
|
|
578
|
+
formatDisplayTime,
|
|
579
|
+
],
|
|
565
580
|
);
|
|
566
581
|
|
|
567
582
|
// Handle clear button click
|
|
@@ -659,7 +674,7 @@ export function TimeInput({
|
|
|
659
674
|
user would get no feedback that their entry was rejected (WCAG 3.3.1).
|
|
660
675
|
*/}
|
|
661
676
|
<VisuallyHidden as="div" role="alert" aria-live="assertive">
|
|
662
|
-
{!isInputValid ? '
|
|
677
|
+
{!isInputValid ? t('@astryx.timeInput.invalidTime') : ''}
|
|
663
678
|
</VisuallyHidden>
|
|
664
679
|
{isBusy && <Spinner size="sm" />}
|
|
665
680
|
{hasClear && value && !isDisabled && (
|
|
@@ -408,6 +408,50 @@ describe('Timestamp', () => {
|
|
|
408
408
|
expect(el.getAttribute('aria-label')).toBeNull();
|
|
409
409
|
});
|
|
410
410
|
|
|
411
|
+
// Timezone abbreviations like "PST" or "GMT+2" are unexpanded abbreviations
|
|
412
|
+
// to a screen-reader user (WCAG 3.1.4) — the AT-facing aria-label must spell
|
|
413
|
+
// the timezone out in full, while visible text keeps the compact short form.
|
|
414
|
+
it('uses the long timezone name in the aria-label (WCAG 3.1.4)', () => {
|
|
415
|
+
const oneHourAgo = new Date(Date.now() - 3600 * 1000);
|
|
416
|
+
render(
|
|
417
|
+
<Timestamp
|
|
418
|
+
value={oneHourAgo.getTime() / 1000}
|
|
419
|
+
format="relative"
|
|
420
|
+
hasTooltip={false}
|
|
421
|
+
data-testid="ts"
|
|
422
|
+
/>,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const longTz =
|
|
426
|
+
new Intl.DateTimeFormat(undefined, {timeZoneName: 'long'})
|
|
427
|
+
.formatToParts(oneHourAgo)
|
|
428
|
+
.find(p => p.type === 'timeZoneName')?.value ?? '';
|
|
429
|
+
expect(longTz).not.toBe('');
|
|
430
|
+
expect(screen.getByTestId('ts').getAttribute('aria-label')).toContain(
|
|
431
|
+
longTz,
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('keeps the short timezone form in visible text when isTimezoneShown', () => {
|
|
436
|
+
const date = new Date('2026-03-25T10:00:00Z');
|
|
437
|
+
render(
|
|
438
|
+
<Timestamp
|
|
439
|
+
value="2026-03-25T10:00:00Z"
|
|
440
|
+
format="time"
|
|
441
|
+
isTimezoneShown
|
|
442
|
+
data-testid="ts"
|
|
443
|
+
/>,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const tzPart = (form: 'short' | 'long') =>
|
|
447
|
+
new Intl.DateTimeFormat(undefined, {timeZoneName: form})
|
|
448
|
+
.formatToParts(date)
|
|
449
|
+
.find(p => p.type === 'timeZoneName')?.value ?? '';
|
|
450
|
+
const text = screen.getByTestId('ts').textContent ?? '';
|
|
451
|
+
expect(text).toContain(tzPart('short'));
|
|
452
|
+
expect(text).not.toContain(tzPart('long'));
|
|
453
|
+
});
|
|
454
|
+
|
|
411
455
|
// --- Input handling ---
|
|
412
456
|
|
|
413
457
|
it('accepts Unix timestamp in seconds', () => {
|
|
@@ -602,15 +646,26 @@ describe('Timestamp', () => {
|
|
|
602
646
|
);
|
|
603
647
|
const el = screen.getByTestId('ts');
|
|
604
648
|
|
|
605
|
-
// The card layer mounts inline and carries the full absolute time
|
|
606
|
-
//
|
|
649
|
+
// The card layer mounts inline and carries the full absolute time in
|
|
650
|
+
// its visible form (short timezone abbreviation) as its single default
|
|
651
|
+
// copyable row. The aria-label spells the timezone out in full
|
|
652
|
+
// (WCAG 3.1.4), so the two strings intentionally differ — compare the
|
|
653
|
+
// card against an independently formatted short-form string.
|
|
607
654
|
// Compare with normalized whitespace: Intl output can contain narrow
|
|
608
655
|
// no-break spaces that jest-dom's matcher normalization would break on.
|
|
609
656
|
const card = await screen.findByRole('dialog', {hidden: true});
|
|
610
657
|
const normalize = (s: string) => s.replace(/\s+/g, ' ');
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
658
|
+
const datetime = new Date(el.getAttribute('datetime') ?? '');
|
|
659
|
+
const expected = new Intl.DateTimeFormat(undefined, {
|
|
660
|
+
year: 'numeric',
|
|
661
|
+
month: 'long',
|
|
662
|
+
day: 'numeric',
|
|
663
|
+
hour: 'numeric',
|
|
664
|
+
minute: '2-digit',
|
|
665
|
+
second: '2-digit',
|
|
666
|
+
timeZoneName: 'short',
|
|
667
|
+
}).format(datetime);
|
|
668
|
+
expect(normalize(card.textContent ?? '')).toContain(normalize(expected));
|
|
614
669
|
|
|
615
670
|
// Tab onto the timestamp — the only tab stop in the document.
|
|
616
671
|
await user.tab();
|
|
@@ -705,15 +760,28 @@ describe('Timestamp', () => {
|
|
|
705
760
|
// The default card is the named details card, exactly as the configured
|
|
706
761
|
// one is.
|
|
707
762
|
expect(card).toHaveAttribute('aria-label', 'Timestamp details');
|
|
708
|
-
// Exactly one row, carrying the full absolute time
|
|
709
|
-
//
|
|
763
|
+
// Exactly one row, carrying the full absolute time in its visible form
|
|
764
|
+
// (short timezone abbreviation) with its own copy button. The
|
|
765
|
+
// aria-label spells the timezone out in full (WCAG 3.1.4), so the two
|
|
766
|
+
// strings intentionally differ — compare the row against an
|
|
767
|
+
// independently formatted short-form string.
|
|
710
768
|
expect(card.querySelectorAll('dd')).toHaveLength(1);
|
|
711
769
|
expect(card.querySelectorAll('button')).toHaveLength(1);
|
|
712
770
|
|
|
713
771
|
const normalize = (s: string) => s.replace(/\s+/g, ' ');
|
|
714
|
-
|
|
715
|
-
|
|
772
|
+
const datetime = new Date(
|
|
773
|
+
screen.getByTestId('ts').getAttribute('datetime') ?? '',
|
|
716
774
|
);
|
|
775
|
+
const expected = new Intl.DateTimeFormat(undefined, {
|
|
776
|
+
year: 'numeric',
|
|
777
|
+
month: 'long',
|
|
778
|
+
day: 'numeric',
|
|
779
|
+
hour: 'numeric',
|
|
780
|
+
minute: '2-digit',
|
|
781
|
+
second: '2-digit',
|
|
782
|
+
timeZoneName: 'short',
|
|
783
|
+
}).format(datetime);
|
|
784
|
+
expect(normalize(card.textContent ?? '')).toContain(normalize(expected));
|
|
717
785
|
});
|
|
718
786
|
|
|
719
787
|
it("copies the default absolute row's value", async () => {
|
|
@@ -1171,10 +1239,14 @@ describe('Timestamp', () => {
|
|
|
1171
1239
|
},
|
|
1172
1240
|
);
|
|
1173
1241
|
|
|
1174
|
-
it('renders the full style
|
|
1242
|
+
it('renders the full style as the aria-label and as a line, differing only in zone-name spelling', () => {
|
|
1175
1243
|
// 'full' is the one member with no visible-text counterpart — it backs
|
|
1176
1244
|
// the accessible name of a relative timestamp and the tooltip's default
|
|
1177
|
-
// line.
|
|
1245
|
+
// line. The two must not drift apart on anything but the zone name: the
|
|
1246
|
+
// aria-label spells the zone out in full for assistive tech (WCAG 3.1.4),
|
|
1247
|
+
// the visible line keeps the abbreviation. Comparing them after swapping
|
|
1248
|
+
// the abbreviation for the spelled-out form keeps this a direct
|
|
1249
|
+
// surface-to-surface check, so re-forking either path still fails here.
|
|
1178
1250
|
render(
|
|
1179
1251
|
<Timestamp
|
|
1180
1252
|
value={VALUE}
|
|
@@ -1183,9 +1255,14 @@ describe('Timestamp', () => {
|
|
|
1183
1255
|
data-testid="ts"
|
|
1184
1256
|
/>,
|
|
1185
1257
|
);
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1258
|
+
const date = new Date(VALUE);
|
|
1259
|
+
const tzPart = (form: 'short' | 'long') =>
|
|
1260
|
+
new Intl.DateTimeFormat(undefined, {timeZoneName: form})
|
|
1261
|
+
.formatToParts(date)
|
|
1262
|
+
.find(p => p.type === 'timeZoneName')?.value ?? '';
|
|
1263
|
+
const [line] = formatTooltipLines(date, [{format: 'full'}]);
|
|
1264
|
+
expect(screen.getByTestId('ts').getAttribute('aria-label')).toBe(
|
|
1265
|
+
line.value.replace(tzPart('short'), tzPart('long')),
|
|
1189
1266
|
);
|
|
1190
1267
|
});
|
|
1191
1268
|
});
|
|
@@ -451,8 +451,14 @@ export function Timestamp({
|
|
|
451
451
|
? formatInstant(date, effectiveFormat, {isTimezoneShown})
|
|
452
452
|
: '';
|
|
453
453
|
|
|
454
|
-
// Full absolute text for tooltip
|
|
454
|
+
// Full absolute text for the tooltip (visible — keeps the compact timezone
|
|
455
|
+
// abbreviation) and for the AT-facing aria-label, which spells the timezone
|
|
456
|
+
// out in full: abbreviations like "PST" or "GMT+2" are unexpanded
|
|
457
|
+
// abbreviations to a screen-reader user (WCAG 3.1.4).
|
|
455
458
|
const fullAbsoluteText = isValidDate ? formatInstant(date, 'full') : '';
|
|
459
|
+
const ariaLabelText = isValidDate
|
|
460
|
+
? formatInstant(date, 'full', {timeZoneNameStyle: 'long'})
|
|
461
|
+
: '';
|
|
456
462
|
|
|
457
463
|
// Live updates
|
|
458
464
|
useEffect(() => {
|
|
@@ -518,8 +524,14 @@ export function Timestamp({
|
|
|
518
524
|
<time
|
|
519
525
|
ref={mergeRefs(ref, timeRef)}
|
|
520
526
|
dateTime={isoString}
|
|
527
|
+
// `ariaLabelText` is '' only for an invalid date, which bails out
|
|
528
|
+
// before rendering — but keep the guard local: an empty aria-label
|
|
529
|
+
// must be omitted entirely (not rendered as aria-label="") so AT
|
|
530
|
+
// falls back to reading the visible <time> content.
|
|
521
531
|
aria-label={
|
|
522
|
-
isRelativeFormat(effectiveFormat)
|
|
532
|
+
isRelativeFormat(effectiveFormat) && ariaLabelText !== ''
|
|
533
|
+
? ariaLabelText
|
|
534
|
+
: undefined
|
|
523
535
|
}
|
|
524
536
|
// The hover card is anchored here with focusTrigger="always", which
|
|
525
537
|
// attaches focus listeners but does not itself make the anchor
|
|
@@ -69,6 +69,16 @@ export interface FormatInstantOptions {
|
|
|
69
69
|
* @default false
|
|
70
70
|
*/
|
|
71
71
|
isTimezoneShown?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* How the zone name is spelled when a format carries one. `'short'` is the
|
|
74
|
+
* abbreviation ("PST"); `'long'` spells it out ("Pacific Standard Time").
|
|
75
|
+
* Only `'full'` honours it — it is the one format that always names its zone.
|
|
76
|
+
* The spelled-out form backs a relative timestamp's accessible name, where an
|
|
77
|
+
* abbreviation reads as an unexpanded initialism to a screen reader
|
|
78
|
+
* (WCAG 3.1.4); every visible surface keeps the abbreviation.
|
|
79
|
+
* @default 'short'
|
|
80
|
+
*/
|
|
81
|
+
timeZoneNameStyle?: 'short' | 'long';
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
// =============================================================================
|
|
@@ -155,7 +165,11 @@ function getWallClock(date: Date, timeZone: string | undefined): WallClock {
|
|
|
155
165
|
export function formatInstant(
|
|
156
166
|
date: Date,
|
|
157
167
|
format: InstantFormat,
|
|
158
|
-
{
|
|
168
|
+
{
|
|
169
|
+
timeZone,
|
|
170
|
+
isTimezoneShown = false,
|
|
171
|
+
timeZoneNameStyle = 'short',
|
|
172
|
+
}: FormatInstantOptions = {},
|
|
159
173
|
): string {
|
|
160
174
|
const zone = timeZone === undefined ? {} : {timeZone};
|
|
161
175
|
const zoneName = isTimezoneShown ? {timeZoneName: 'short' as const} : {};
|
|
@@ -164,6 +178,7 @@ export function formatInstant(
|
|
|
164
178
|
case 'full':
|
|
165
179
|
return new Intl.DateTimeFormat(undefined, {
|
|
166
180
|
...FULL_OPTIONS,
|
|
181
|
+
timeZoneName: timeZoneNameStyle,
|
|
167
182
|
...zone,
|
|
168
183
|
}).format(date);
|
|
169
184
|
|
|
@@ -7,6 +7,11 @@ export const docs = {
|
|
|
7
7
|
subComponentOf: 'TopNav',
|
|
8
8
|
displayName: 'Top Nav Mega Menu',
|
|
9
9
|
description: 'Navigation item that displays a full-width mega menu panel on hover. Uses a slots API with items and featured props. TopNavMegaMenuItem renders itself in both desktop and mobile drawer modes. Supports inline collapsible drawer via render mode context.',
|
|
10
|
+
// The panel anchors to the nearest <nav>; wrap the preview in a TopNav so
|
|
11
|
+
// it opens below the bar instead of pinning to the viewport corner (#4905).
|
|
12
|
+
playground: {
|
|
13
|
+
wrapper: {component: 'TopNav'},
|
|
14
|
+
},
|
|
10
15
|
props: [
|
|
11
16
|
{
|
|
12
17
|
name: 'label',
|
|
@@ -16,6 +16,7 @@ import {TreeList} from './TreeList';
|
|
|
16
16
|
import type {TreeListItemData} from './TreeListTypes';
|
|
17
17
|
import {defineTheme} from '../theme/defineTheme';
|
|
18
18
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
19
|
+
import {FOCUS_OUTLINE} from '../utils/focusOutline.stylex';
|
|
19
20
|
|
|
20
21
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
21
22
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -299,8 +300,8 @@ describe('TreeList', () => {
|
|
|
299
300
|
expect(root).toHaveFocus();
|
|
300
301
|
|
|
301
302
|
expect(
|
|
302
|
-
getComputedStyle(root).getPropertyValue('--_focus-outline'),
|
|
303
|
-
).
|
|
303
|
+
getComputedStyle(root).getPropertyValue('--_focus-outline').trim(),
|
|
304
|
+
).toBe(FOCUS_OUTLINE);
|
|
304
305
|
// Mid and Leaf are DOM descendants of Root's <li> (nested <ul role="group">
|
|
305
306
|
// subtrees) — their own outline var must stay unset, not inherit Root's.
|
|
306
307
|
expect(getComputedStyle(mid).getPropertyValue('--_focus-outline')).toBe(
|
|
@@ -40,8 +40,9 @@ import type {FieldStatusVariant} from '../FieldStatus/FieldStatus';
|
|
|
40
40
|
import type {InputStatus, InputStatusType} from '../Field/types';
|
|
41
41
|
import {useTooltip} from '../Tooltip';
|
|
42
42
|
import {useTranslator} from '../i18n';
|
|
43
|
-
import {
|
|
43
|
+
import {radiusVars} from '../theme/tokens.stylex';
|
|
44
44
|
import {themeProps} from '../utils/themeProps';
|
|
45
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Maps each status type to its glyph. Shared so every input shows the same icon
|
|
@@ -82,12 +83,8 @@ const styles = stylex.create({
|
|
|
82
83
|
color: 'inherit',
|
|
83
84
|
cursor: 'pointer',
|
|
84
85
|
borderRadius: radiusVars['--radius-full'],
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
outlineColor: {
|
|
88
|
-
default: null,
|
|
89
|
-
':focus-visible': colorVars['--color-accent'],
|
|
90
|
-
},
|
|
86
|
+
// Not the shared offset: this button sits inside the field, and measured at
|
|
87
|
+
// the standard 3px its ring crosses the field border.
|
|
91
88
|
outlineOffset: {default: null, ':focus-visible': '2px'},
|
|
92
89
|
},
|
|
93
90
|
});
|
|
@@ -224,7 +221,10 @@ export function useInputStatusIcon({
|
|
|
224
221
|
aria-describedby={tooltip.describedBy}
|
|
225
222
|
onClick={handleButtonClick}
|
|
226
223
|
onBlur={handleButtonBlur}
|
|
227
|
-
{...stylex.props(
|
|
224
|
+
{...stylex.props(
|
|
225
|
+
focusOutlineStyles.focusVisible,
|
|
226
|
+
styles.statusButton,
|
|
227
|
+
)}>
|
|
228
228
|
{icon}
|
|
229
229
|
</button>
|
|
230
230
|
{tooltip.renderTooltip(status.message)}
|