@astryxdesign/core 0.4.0 → 0.4.1-canary.f4868b2
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/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 +20 -17
- package/dist/astryx.umd.js +48 -48
- 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 +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 +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/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/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/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/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
|
@@ -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',
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* SYNC: When TopNavMegaMenu changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import * as stylex from '@stylexjs/stylex';
|
|
13
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
12
14
|
import {describe, it, expect, vi, beforeAll, afterAll, afterEach} from 'vitest';
|
|
13
15
|
import {render, screen, act, fireEvent} from '@testing-library/react';
|
|
14
16
|
import userEvent from '@testing-library/user-event';
|
|
@@ -817,3 +819,44 @@ describe('TopNavMegaMenuItem', () => {
|
|
|
817
819
|
expect(screen.getByRole('link', {name: /Analytics/})).toBeInTheDocument();
|
|
818
820
|
});
|
|
819
821
|
});
|
|
822
|
+
|
|
823
|
+
// =============================================================================
|
|
824
|
+
// The shared focus ring (#4654) — see the note in SideNav.test.tsx: jsdom will
|
|
825
|
+
// not derive `:focus-visible` here, so what is pinned is that the focusable
|
|
826
|
+
// element composes the shared utility's classes rather than falling back to
|
|
827
|
+
// the browser's own outline.
|
|
828
|
+
// =============================================================================
|
|
829
|
+
|
|
830
|
+
const sharedFocusRingClasses = stylex
|
|
831
|
+
.props(focusOutlineStyles.focusVisible)
|
|
832
|
+
.className!.split(' ');
|
|
833
|
+
|
|
834
|
+
function expectSharedFocusRing(el: Element) {
|
|
835
|
+
const classes = el.className.split(' ');
|
|
836
|
+
for (const c of sharedFocusRingClasses) {
|
|
837
|
+
expect(classes).toContain(c);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
describe('TopNavMegaMenu — drawer focus ring', () => {
|
|
842
|
+
it('draws the shared ring on the drawer section header', () => {
|
|
843
|
+
render(
|
|
844
|
+
<TopNavRenderContext value="drawer">
|
|
845
|
+
<TopNavMegaMenu
|
|
846
|
+
label="Products"
|
|
847
|
+
items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
|
|
848
|
+
/>
|
|
849
|
+
</TopNavRenderContext>,
|
|
850
|
+
);
|
|
851
|
+
expectSharedFocusRing(screen.getByRole('button', {name: 'Products'}));
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('draws the shared ring on a drawer item', () => {
|
|
855
|
+
render(
|
|
856
|
+
<TopNavRenderContext value="drawer">
|
|
857
|
+
<TopNavMegaMenuItem title="Analytics" href="/analytics" />
|
|
858
|
+
</TopNavRenderContext>,
|
|
859
|
+
);
|
|
860
|
+
expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
|
|
861
|
+
});
|
|
862
|
+
});
|
|
@@ -602,7 +602,10 @@ function DrawerMegaMenu({
|
|
|
602
602
|
aria-controls={`${menuId}-items`}
|
|
603
603
|
{...mergeProps(
|
|
604
604
|
themeProps('top-nav-mega-menu', {mode: 'drawer'}),
|
|
605
|
-
|
|
605
|
+
focusOutlineProps.focusVisible(
|
|
606
|
+
navItemStyles.item,
|
|
607
|
+
styles.drawerHeader,
|
|
608
|
+
),
|
|
606
609
|
)}>
|
|
607
610
|
{label}
|
|
608
611
|
<Icon
|
|
@@ -215,7 +215,7 @@ export function TopNavMegaMenuItem({
|
|
|
215
215
|
{...elementProps}
|
|
216
216
|
{...mergeProps(
|
|
217
217
|
themeProps('top-nav-mega-menu-item', {mode: 'drawer'}),
|
|
218
|
-
|
|
218
|
+
focusOutlineProps.focusVisible(navItemStyles.item, styles.drawerItem),
|
|
219
219
|
)}>
|
|
220
220
|
{icon && <div {...stylex.props(styles.drawerItemIcon)}>{icon}</div>}
|
|
221
221
|
<div {...stylex.props(styles.drawerItemContent)}>
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import * as stylex from '@stylexjs/stylex';
|
|
16
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
17
|
+
import {TopNavRenderContext} from './TopNavRenderContext';
|
|
15
18
|
import {TopNavMenu} from './TopNavMenu';
|
|
16
19
|
|
|
17
20
|
const mockItems = [
|
|
@@ -171,3 +174,41 @@ describe('keyboard navigation (APG menu pattern)', () => {
|
|
|
171
174
|
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
172
175
|
});
|
|
173
176
|
});
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// The shared focus ring (#4654) — see the note in SideNav.test.tsx: jsdom will
|
|
180
|
+
// not derive `:focus-visible` here, so what is pinned is that the focusable
|
|
181
|
+
// element composes the shared utility's classes rather than falling back to
|
|
182
|
+
// the browser's own outline.
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
const sharedFocusRingClasses = stylex
|
|
186
|
+
.props(focusOutlineStyles.focusVisible)
|
|
187
|
+
.className!.split(' ');
|
|
188
|
+
|
|
189
|
+
function expectSharedFocusRing(el: Element) {
|
|
190
|
+
const classes = el.className.split(' ');
|
|
191
|
+
for (const c of sharedFocusRingClasses) {
|
|
192
|
+
expect(classes).toContain(c);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
describe('TopNavMenu — drawer focus ring', () => {
|
|
197
|
+
it('draws the shared ring on the drawer section header', () => {
|
|
198
|
+
render(
|
|
199
|
+
<TopNavRenderContext value="drawer">
|
|
200
|
+
<TopNavMenu label="Products" items={mockItems} />
|
|
201
|
+
</TopNavRenderContext>,
|
|
202
|
+
);
|
|
203
|
+
expectSharedFocusRing(screen.getByRole('button', {name: /Products/}));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('draws the shared ring on a drawer item', () => {
|
|
207
|
+
render(
|
|
208
|
+
<TopNavRenderContext value="drawer">
|
|
209
|
+
<TopNavMenu label="Products" items={mockItems} />
|
|
210
|
+
</TopNavRenderContext>,
|
|
211
|
+
);
|
|
212
|
+
expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -432,7 +432,10 @@ export function TopNavMenu({
|
|
|
432
432
|
onClick={() => setDrawerExpanded(v => !v)}
|
|
433
433
|
aria-expanded={drawerExpanded}
|
|
434
434
|
aria-controls={`${menuId}-items`}
|
|
435
|
-
{...
|
|
435
|
+
{...focusOutlineProps.focusVisible(
|
|
436
|
+
navItemStyles.item,
|
|
437
|
+
drawerStyles.header,
|
|
438
|
+
)}>
|
|
436
439
|
{label}
|
|
437
440
|
<Icon
|
|
438
441
|
icon="chevronDown"
|
|
@@ -459,7 +462,10 @@ export function TopNavMenu({
|
|
|
459
462
|
item.onClick?.();
|
|
460
463
|
closeMobileNav();
|
|
461
464
|
}}
|
|
462
|
-
{...
|
|
465
|
+
{...focusOutlineProps.focusVisible(
|
|
466
|
+
navItemStyles.item,
|
|
467
|
+
drawerStyles.item,
|
|
468
|
+
)}>
|
|
463
469
|
{item.icon && (
|
|
464
470
|
<span {...stylex.props(drawerStyles.itemIcon)}>
|
|
465
471
|
{item.icon}
|
|
@@ -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)}
|