@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2
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 +156 -2
- package/README.md +4 -4
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -1
- package/dist/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +8 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CheckboxList/CheckboxList.d.ts +1 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +6 -2
- package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -2
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +84 -113
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionStops.js +131 -0
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.d.ts +8 -7
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +5 -4
- package/dist/Grid/GridSpan.d.ts +1 -1
- package/dist/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- package/dist/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -4
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +13 -5
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +21 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +8 -2
- package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
- package/dist/Markdown/plugins/headingLinks.js +9 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.d.ts +9 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +30 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- package/dist/RadioList/RadioListItem.d.ts +9 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +2 -0
- package/dist/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +76 -10
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- package/dist/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/Token/Token.js +1 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +5 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +5 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/astryx.css +27 -4
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/reset.a11y.chromium.spec.js +50 -1
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/docs.mjs +4 -4
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +66 -6
- package/scripts/postinstall.mjs +44 -3
- package/src/AppShell/AppShell.tsx +45 -1
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Avatar/Avatar.doc.mjs +5 -3
- package/src/Avatar/Avatar.tsx +11 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CheckboxList/CheckboxList.fr10.test.tsx +29 -2
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxList.tsx +4 -1
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/ContextMenu.test.tsx +12 -2
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/Drawer/Drawer.doc.mjs +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
- package/src/DropdownMenu/DropdownMenu.tsx +88 -144
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/DropdownMenu/menuRegionStops.ts +167 -0
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.test.tsx +15 -0
- package/src/FormLayout/FormLayout.tsx +7 -6
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.doc.mjs +7 -1
- package/src/Grid/Grid.spec.md +93 -113
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +10 -9
- package/src/Grid/GridSpan.doc.mjs +3 -3
- package/src/Grid/GridSpan.tsx +4 -1
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +163 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.doc.mjs +16 -0
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/parser.ts +8 -2
- package/src/Markdown/plugins/headingLinks.ts +9 -1
- package/src/MetadataList/MetadataList.doc.mjs +3 -3
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MobileNav/MobileNav.tsx +30 -3
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- package/src/RadioList/RadioList.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioList.tsx +4 -1
- package/src/RadioList/RadioListItem.doc.mjs +16 -0
- package/src/RadioList/RadioListItem.tsx +10 -1
- package/src/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/SideNav/SideNav.spec.md +227 -0
- package/src/SideNav/SideNav.test.tsx +220 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +99 -17
- package/src/Slider/Slider.spec.md +39 -18
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/TabList/TabList.spec.md +57 -44
- package/src/Table/Table.spec.md +6 -1
- package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
- package/src/TopNav/TopNavMegaMenu.tsx +9 -3
- package/src/TopNav/TopNavMenu.test.tsx +46 -1
- package/src/TopNav/TopNavMenu.tsx +13 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/docPropLiterals.test.ts +36 -10
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/postinstall.test.mjs +79 -0
- package/src/reset.a11y.chromium.spec.ts +52 -1
- package/src/reset.css +9 -29
- package/src/reset.test.ts +11 -0
- package/src/theme/derivedVarRegistry.test.ts +7 -0
- package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuGroupAudit.a11y.chromium.spec.ts
|
|
5
|
+
* @input Exact-head DropdownMenuGroup story across themes, color modes, direction, and viewport widths
|
|
6
|
+
* @output Screenshots and fail-closed sensor receipts for the DropdownMenuGroup component audit
|
|
7
|
+
* @position Component-owned Night Watch browser evidence; CI runs it only for a DropdownMenuGroup audit pull request
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {createHash} from 'node:crypto';
|
|
11
|
+
import {execFileSync} from 'node:child_process';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import {expect, test, type Page} from '@playwright/test';
|
|
15
|
+
import {holdMotionStill} from '@astryxdesign/a11y-spec/chromium';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_STORYBOOK_DIR,
|
|
18
|
+
serveStorybook,
|
|
19
|
+
type StaticServer,
|
|
20
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
21
|
+
|
|
22
|
+
const OUTPUT = path.resolve('dropdown-menu-group-audit-evidence');
|
|
23
|
+
const STORY_ID = 'core-dropdownmenugroup--named-and-unnamed-groups';
|
|
24
|
+
const WIDE = {width: 1024, height: 768};
|
|
25
|
+
const NARROW = {width: 320, height: 720};
|
|
26
|
+
|
|
27
|
+
type Mode = 'light' | 'dark';
|
|
28
|
+
type Direction = 'ltr' | 'rtl';
|
|
29
|
+
type Theme = 'neutral' | 'butter';
|
|
30
|
+
type AuditCase = {
|
|
31
|
+
key: string;
|
|
32
|
+
mode: Mode;
|
|
33
|
+
direction: Direction;
|
|
34
|
+
theme: Theme;
|
|
35
|
+
viewport: {width: number; height: number};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const cases: AuditCase[] = [
|
|
39
|
+
{
|
|
40
|
+
key: 'neutral-light',
|
|
41
|
+
mode: 'light',
|
|
42
|
+
direction: 'ltr',
|
|
43
|
+
theme: 'neutral',
|
|
44
|
+
viewport: WIDE,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'neutral-dark',
|
|
48
|
+
mode: 'dark',
|
|
49
|
+
direction: 'ltr',
|
|
50
|
+
theme: 'neutral',
|
|
51
|
+
viewport: WIDE,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'butter-light',
|
|
55
|
+
mode: 'light',
|
|
56
|
+
direction: 'ltr',
|
|
57
|
+
theme: 'butter',
|
|
58
|
+
viewport: WIDE,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'butter-dark',
|
|
62
|
+
mode: 'dark',
|
|
63
|
+
direction: 'ltr',
|
|
64
|
+
theme: 'butter',
|
|
65
|
+
viewport: WIDE,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'rtl-light',
|
|
69
|
+
mode: 'light',
|
|
70
|
+
direction: 'rtl',
|
|
71
|
+
theme: 'neutral',
|
|
72
|
+
viewport: WIDE,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'rtl-dark',
|
|
76
|
+
mode: 'dark',
|
|
77
|
+
direction: 'rtl',
|
|
78
|
+
theme: 'neutral',
|
|
79
|
+
viewport: WIDE,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'narrow-light',
|
|
83
|
+
mode: 'light',
|
|
84
|
+
direction: 'ltr',
|
|
85
|
+
theme: 'neutral',
|
|
86
|
+
viewport: NARROW,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'narrow-dark',
|
|
90
|
+
mode: 'dark',
|
|
91
|
+
direction: 'ltr',
|
|
92
|
+
theme: 'neutral',
|
|
93
|
+
viewport: NARROW,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
test.describe.configure({mode: 'serial', retries: 0});
|
|
98
|
+
let server: StaticServer;
|
|
99
|
+
let checkoutSha: string;
|
|
100
|
+
let storybookSha: string;
|
|
101
|
+
|
|
102
|
+
function storyUrl(item: AuditCase) {
|
|
103
|
+
return `${server.origin}/iframe.html?id=${STORY_ID}&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function settle(page: Page) {
|
|
107
|
+
await holdMotionStill(page);
|
|
108
|
+
await page.evaluate(async () => document.fonts.ready);
|
|
109
|
+
await page.waitForTimeout(40);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test.beforeAll(async () => {
|
|
113
|
+
fs.rmSync(OUTPUT, {recursive: true, force: true});
|
|
114
|
+
fs.mkdirSync(OUTPUT, {recursive: true});
|
|
115
|
+
checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
}).trim();
|
|
118
|
+
const expected = process.env.ASTRYX_HEAD_SHA;
|
|
119
|
+
if (expected && expected !== checkoutSha) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`checkout ${checkoutSha} does not match expected head ${expected}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
server = await serveStorybook(
|
|
125
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
126
|
+
);
|
|
127
|
+
const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`Storybook build has no SHA stamp (${response.status})`);
|
|
130
|
+
}
|
|
131
|
+
storybookSha = (await response.text()).trim();
|
|
132
|
+
if (storybookSha !== checkoutSha) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test.afterAll(async () => {
|
|
140
|
+
await server?.close();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const item of cases) {
|
|
144
|
+
test(item.key, async ({browser}) => {
|
|
145
|
+
const context = await browser.newContext({
|
|
146
|
+
viewport: item.viewport,
|
|
147
|
+
colorScheme: item.mode,
|
|
148
|
+
reducedMotion: 'reduce',
|
|
149
|
+
});
|
|
150
|
+
const page = await context.newPage();
|
|
151
|
+
const pageErrors: string[] = [];
|
|
152
|
+
page.on('pageerror', error => pageErrors.push(String(error)));
|
|
153
|
+
await page.goto(storyUrl(item), {waitUntil: 'load'});
|
|
154
|
+
await page.locator('#storybook-root').waitFor();
|
|
155
|
+
|
|
156
|
+
const menu = page.getByRole('menu').last();
|
|
157
|
+
await expect(menu).toBeVisible();
|
|
158
|
+
const groups = menu.getByRole('group');
|
|
159
|
+
await expect(groups).toHaveCount(2);
|
|
160
|
+
const namedGroup = menu.getByRole('group', {name: 'Version history'});
|
|
161
|
+
const unnamedGroup = groups.nth(1);
|
|
162
|
+
await expect(namedGroup).toBeVisible();
|
|
163
|
+
await expect(unnamedGroup).not.toHaveAttribute('aria-labelledby');
|
|
164
|
+
const heading = namedGroup.getByText('Version history', {exact: true});
|
|
165
|
+
await expect(heading).toBeVisible();
|
|
166
|
+
const headingId = await heading.getAttribute('id');
|
|
167
|
+
if (headingId == null) {
|
|
168
|
+
throw new Error('named group heading has no id');
|
|
169
|
+
}
|
|
170
|
+
await expect(namedGroup).toHaveAttribute('aria-labelledby', headingId);
|
|
171
|
+
await settle(page);
|
|
172
|
+
|
|
173
|
+
const menuItems = menu.getByRole('menuitem');
|
|
174
|
+
await expect(menuItems).toHaveCount(3);
|
|
175
|
+
await menu.focus();
|
|
176
|
+
await page.keyboard.press('ArrowDown');
|
|
177
|
+
await expect(menuItems.first()).toBeFocused();
|
|
178
|
+
await page.keyboard.press('ArrowDown');
|
|
179
|
+
await expect(menuItems.nth(1)).toBeFocused();
|
|
180
|
+
await page.keyboard.press('ArrowDown');
|
|
181
|
+
await expect(menuItems.last()).toBeFocused();
|
|
182
|
+
await page.keyboard.press('v');
|
|
183
|
+
await expect(menuItems.last()).toBeFocused();
|
|
184
|
+
await expect(namedGroup).not.toBeFocused();
|
|
185
|
+
await expect(heading).not.toBeFocused();
|
|
186
|
+
|
|
187
|
+
const observed = await namedGroup.evaluate(element => {
|
|
188
|
+
const headingId = element.getAttribute('aria-labelledby');
|
|
189
|
+
const currentHeading = headingId
|
|
190
|
+
? document.getElementById(headingId)
|
|
191
|
+
: null;
|
|
192
|
+
const groupBox = element.getBoundingClientRect();
|
|
193
|
+
const headingBox = currentHeading?.getBoundingClientRect() ?? null;
|
|
194
|
+
const groupStyle = getComputedStyle(element);
|
|
195
|
+
const headingStyle = currentHeading
|
|
196
|
+
? getComputedStyle(currentHeading)
|
|
197
|
+
: null;
|
|
198
|
+
const currentMenu = element.closest('[role="menu"]');
|
|
199
|
+
const currentGroups = currentMenu?.querySelectorAll('[role="group"]');
|
|
200
|
+
const currentUnnamed = currentGroups?.item(1) ?? null;
|
|
201
|
+
return {
|
|
202
|
+
theme: document
|
|
203
|
+
.querySelector('[data-astryx-theme]')
|
|
204
|
+
?.getAttribute('data-astryx-theme'),
|
|
205
|
+
mode: document.documentElement.getAttribute('data-theme'),
|
|
206
|
+
direction: groupStyle.direction,
|
|
207
|
+
viewport: {width: innerWidth, height: innerHeight},
|
|
208
|
+
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
209
|
+
horizontalOverflow:
|
|
210
|
+
document.documentElement.scrollWidth > innerWidth + 1,
|
|
211
|
+
fonts: document.fonts.status,
|
|
212
|
+
named: {
|
|
213
|
+
role: element.getAttribute('role'),
|
|
214
|
+
labelledBy: headingId,
|
|
215
|
+
accessibleText: currentHeading?.textContent?.trim() ?? null,
|
|
216
|
+
headingRole: currentHeading?.getAttribute('role') ?? null,
|
|
217
|
+
headingTabIndex: currentHeading?.getAttribute('tabindex') ?? null,
|
|
218
|
+
headingTarget:
|
|
219
|
+
currentHeading?.classList.contains(
|
|
220
|
+
'astryx-dropdown-menu-section-heading',
|
|
221
|
+
) ?? false,
|
|
222
|
+
},
|
|
223
|
+
unnamed: {
|
|
224
|
+
role: currentUnnamed?.getAttribute('role') ?? null,
|
|
225
|
+
labelledBy: currentUnnamed?.getAttribute('aria-labelledby') ?? null,
|
|
226
|
+
ariaLabel: currentUnnamed?.getAttribute('aria-label') ?? null,
|
|
227
|
+
},
|
|
228
|
+
geometry: {
|
|
229
|
+
group: {
|
|
230
|
+
x: groupBox.x,
|
|
231
|
+
y: groupBox.y,
|
|
232
|
+
width: groupBox.width,
|
|
233
|
+
height: groupBox.height,
|
|
234
|
+
},
|
|
235
|
+
heading:
|
|
236
|
+
headingBox === null
|
|
237
|
+
? null
|
|
238
|
+
: {
|
|
239
|
+
x: headingBox.x,
|
|
240
|
+
y: headingBox.y,
|
|
241
|
+
width: headingBox.width,
|
|
242
|
+
height: headingBox.height,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
headingPaint:
|
|
246
|
+
headingStyle === null
|
|
247
|
+
? null
|
|
248
|
+
: {
|
|
249
|
+
color: headingStyle.color,
|
|
250
|
+
fontFamily: headingStyle.fontFamily,
|
|
251
|
+
fontSize: headingStyle.fontSize,
|
|
252
|
+
lineHeight: headingStyle.lineHeight,
|
|
253
|
+
paddingInlineStart: headingStyle.paddingInlineStart,
|
|
254
|
+
paddingInlineEnd: headingStyle.paddingInlineEnd,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const semanticsPass =
|
|
260
|
+
observed.named.role === 'group' &&
|
|
261
|
+
observed.named.labelledBy !== null &&
|
|
262
|
+
observed.named.accessibleText === 'Version history' &&
|
|
263
|
+
observed.named.headingRole === null &&
|
|
264
|
+
observed.named.headingTabIndex === null &&
|
|
265
|
+
observed.named.headingTarget &&
|
|
266
|
+
observed.unnamed.role === 'group' &&
|
|
267
|
+
observed.unnamed.labelledBy === null &&
|
|
268
|
+
observed.unnamed.ariaLabel === null;
|
|
269
|
+
const geometryPass =
|
|
270
|
+
observed.geometry.group.width > 0 &&
|
|
271
|
+
observed.geometry.group.height > 0 &&
|
|
272
|
+
observed.geometry.heading !== null &&
|
|
273
|
+
observed.geometry.heading.width > 0 &&
|
|
274
|
+
observed.geometry.heading.height > 0;
|
|
275
|
+
const paintPass =
|
|
276
|
+
observed.headingPaint !== null &&
|
|
277
|
+
observed.headingPaint.color !== 'rgba(0, 0, 0, 0)' &&
|
|
278
|
+
observed.headingPaint.fontSize !== '0px' &&
|
|
279
|
+
observed.headingPaint.lineHeight !== '0px' &&
|
|
280
|
+
observed.headingPaint.paddingInlineStart ===
|
|
281
|
+
observed.headingPaint.paddingInlineEnd;
|
|
282
|
+
|
|
283
|
+
const png = await page.screenshot({animations: 'disabled', fullPage: true});
|
|
284
|
+
const image = `${item.key}.png`;
|
|
285
|
+
const receipt = {
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
expected: {
|
|
288
|
+
build: checkoutSha,
|
|
289
|
+
storyId: STORY_ID,
|
|
290
|
+
mode: item.mode,
|
|
291
|
+
direction: item.direction,
|
|
292
|
+
theme: item.theme,
|
|
293
|
+
viewport: item.viewport,
|
|
294
|
+
},
|
|
295
|
+
observed: {
|
|
296
|
+
build: storybookSha,
|
|
297
|
+
...observed,
|
|
298
|
+
groupCount: await groups.count(),
|
|
299
|
+
menuItemCount: await menuItems.count(),
|
|
300
|
+
pageErrors,
|
|
301
|
+
},
|
|
302
|
+
image: {
|
|
303
|
+
file: image,
|
|
304
|
+
sha256: createHash('sha256').update(png).digest('hex'),
|
|
305
|
+
width: png.readUInt32BE(16),
|
|
306
|
+
height: png.readUInt32BE(20),
|
|
307
|
+
},
|
|
308
|
+
checks: {semanticsPass, geometryPass, paintPass},
|
|
309
|
+
passed:
|
|
310
|
+
storybookSha === checkoutSha &&
|
|
311
|
+
pageErrors.length === 0 &&
|
|
312
|
+
observed.theme === item.theme &&
|
|
313
|
+
observed.mode === item.mode &&
|
|
314
|
+
observed.direction === item.direction &&
|
|
315
|
+
observed.viewport.width === item.viewport.width &&
|
|
316
|
+
observed.viewport.height === item.viewport.height &&
|
|
317
|
+
observed.reducedMotion &&
|
|
318
|
+
!observed.horizontalOverflow &&
|
|
319
|
+
observed.fonts === 'loaded' &&
|
|
320
|
+
(await groups.count()) === 2 &&
|
|
321
|
+
(await menuItems.count()) === 3 &&
|
|
322
|
+
semanticsPass &&
|
|
323
|
+
geometryPass &&
|
|
324
|
+
paintPass,
|
|
325
|
+
};
|
|
326
|
+
fs.writeFileSync(path.join(OUTPUT, image), png);
|
|
327
|
+
fs.writeFileSync(
|
|
328
|
+
path.join(OUTPUT, `${item.key}.sensors.json`),
|
|
329
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
330
|
+
);
|
|
331
|
+
expect(receipt.passed).toBe(true);
|
|
332
|
+
await context.close();
|
|
333
|
+
});
|
|
334
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file warmMenuBottomSheet.tsx
|
|
5
|
+
* @input Uses DropdownMenu and Testing Library
|
|
6
|
+
* @output Exports warmMenuBottomSheet, a test helper that resolves the lazily
|
|
7
|
+
* loaded touch sheet once
|
|
8
|
+
* @position Test-only helper for suites that open a DropdownMenu bottom sheet
|
|
9
|
+
*
|
|
10
|
+
* DropdownMenu loads its touch sheet with React.lazy, so the first sheet
|
|
11
|
+
* renders a tick after it opens. Opening one up front caches the resolved
|
|
12
|
+
* modules on the lazy components, and every later sheet renders
|
|
13
|
+
* synchronously. Call it from `beforeAll`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {cleanup, render, screen} from '@testing-library/react';
|
|
17
|
+
import {DropdownMenu} from '../DropdownMenu';
|
|
18
|
+
|
|
19
|
+
export async function warmMenuBottomSheet(): Promise<void> {
|
|
20
|
+
const {showModal, close} = HTMLDialogElement.prototype;
|
|
21
|
+
HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
|
|
22
|
+
this.setAttribute('open', '');
|
|
23
|
+
};
|
|
24
|
+
HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
|
|
25
|
+
this.removeAttribute('open');
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
render(
|
|
29
|
+
<DropdownMenu
|
|
30
|
+
button={{label: 'Warm'}}
|
|
31
|
+
presentation="bottom-sheet"
|
|
32
|
+
isMenuOpen
|
|
33
|
+
items={[{label: 'Warm item'}]}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
// A cold import transforms the sheet's module graph; allow for it.
|
|
37
|
+
await screen.findByText('Warm item', undefined, {timeout: 15000});
|
|
38
|
+
} finally {
|
|
39
|
+
cleanup();
|
|
40
|
+
HTMLDialogElement.prototype.showModal = showModal;
|
|
41
|
+
HTMLDialogElement.prototype.close = close;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file lazyMenuBottomSheet.ts
|
|
7
|
+
* @input Uses React lazy and Suspense, useIsHydrating, MenuBottomSheet, and
|
|
8
|
+
* MenuBottomSheetActionList
|
|
9
|
+
* @output Exports MenuBottomSheet and MenuBottomSheetActionList wrappers that
|
|
10
|
+
* load the touch sheet on first render
|
|
11
|
+
* @position Internal to DropdownMenu
|
|
12
|
+
*
|
|
13
|
+
* The touch sheet (BottomSheet, its gestures, and the touch action list) only
|
|
14
|
+
* renders for the bottom-sheet presentation. Loading it on demand keeps it out
|
|
15
|
+
* of pages whose dropdowns only open as anchored popovers. Each wrapper keeps
|
|
16
|
+
* the wrapped component's props and renders nothing until its module loads.
|
|
17
|
+
*
|
|
18
|
+
* Neither wrapper renders on the server or in the render that hydrates server
|
|
19
|
+
* HTML (spec:AST-013 FR9): a lazy component that has not loaded yet would
|
|
20
|
+
* leave server markup incomplete and abort hydration. The closed sheet has no
|
|
21
|
+
* visible markup, so it first renders right after hydration.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
createElement,
|
|
26
|
+
lazy,
|
|
27
|
+
Suspense,
|
|
28
|
+
type ComponentProps,
|
|
29
|
+
type ReactElement,
|
|
30
|
+
} from 'react';
|
|
31
|
+
import {useIsHydrating} from '../hooks/useIsHydrating';
|
|
32
|
+
import type {MenuBottomSheet as EagerMenuBottomSheet} from './MenuBottomSheet';
|
|
33
|
+
import type {MenuBottomSheetActionList as EagerMenuBottomSheetActionList} from './MenuBottomSheetActionList';
|
|
34
|
+
|
|
35
|
+
const LazyMenuBottomSheet = lazy(async () =>
|
|
36
|
+
import('./MenuBottomSheet').then(module => ({
|
|
37
|
+
default: module.MenuBottomSheet,
|
|
38
|
+
})),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const LazyMenuBottomSheetActionList = lazy(async () =>
|
|
42
|
+
import('./MenuBottomSheetActionList').then(module => ({
|
|
43
|
+
default: module.MenuBottomSheetActionList,
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export function MenuBottomSheet(
|
|
48
|
+
props: ComponentProps<typeof EagerMenuBottomSheet>,
|
|
49
|
+
): ReactElement | null {
|
|
50
|
+
if (useIsHydrating()) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return createElement(
|
|
54
|
+
Suspense,
|
|
55
|
+
{fallback: null},
|
|
56
|
+
createElement(LazyMenuBottomSheet, props),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function MenuBottomSheetActionList(
|
|
61
|
+
props: ComponentProps<typeof EagerMenuBottomSheetActionList>,
|
|
62
|
+
): ReactElement | null {
|
|
63
|
+
if (useIsHydrating()) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return createElement(
|
|
67
|
+
Suspense,
|
|
68
|
+
{fallback: null},
|
|
69
|
+
createElement(LazyMenuBottomSheetActionList, props),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file menuRegionStops.ts
|
|
7
|
+
* @input The focus trap's own reachability test (getFocusableElements)
|
|
8
|
+
* @output `tabbablesIn`, `focusRegionControl`, `moveRegionsTab`: the Tab stops
|
|
9
|
+
* of a header or footer region and the walk through a menu with regions,
|
|
10
|
+
* with a named radio group as one stop
|
|
11
|
+
* @position Internal; DropdownMenu's walk through its header and footer, in a
|
|
12
|
+
* module of its own so another menu level with regions can share it.
|
|
13
|
+
*
|
|
14
|
+
* A menu with a header or footer is a dialog: Tab and Shift+Tab walk the
|
|
15
|
+
* header's controls, the menu as one stop, then the footer's controls, and
|
|
16
|
+
* wrap at either end.
|
|
17
|
+
*/
|
|
18
|
+
import type React from 'react';
|
|
19
|
+
import {getFocusableElements} from '../hooks/useFocusTrap';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The elements Tab reaches inside a header or footer region, in order. Hidden
|
|
23
|
+
* controls (`hidden`, `display: none`, `visibility: hidden`, inert or
|
|
24
|
+
* aria-hidden subtrees) are not reachable, judged by the popover focus trap's
|
|
25
|
+
* own test so the two never disagree about where Tab can go.
|
|
26
|
+
*/
|
|
27
|
+
export function tabbablesIn(region: HTMLElement | null): HTMLElement[] {
|
|
28
|
+
if (region == null) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return getFocusableElements(region).filter(
|
|
32
|
+
element => element.tabIndex >= 0 && !isInsideHiddenWrapper(element, region),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether a `display: none` ancestor between the element and its region hides
|
|
38
|
+
* it — a control in a wrapper hidden at some widths, say. The trap's test reads
|
|
39
|
+
* only the element's own display.
|
|
40
|
+
*/
|
|
41
|
+
function isInsideHiddenWrapper(
|
|
42
|
+
element: HTMLElement,
|
|
43
|
+
region: HTMLElement,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (typeof window === 'undefined' || !window.getComputedStyle) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
for (
|
|
49
|
+
let node = element.parentElement;
|
|
50
|
+
node != null && node !== region.parentElement;
|
|
51
|
+
node = node.parentElement
|
|
52
|
+
) {
|
|
53
|
+
if (window.getComputedStyle(node).display === 'none') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Collapse each named radio group to the one Tab stop a browser gives it: the
|
|
62
|
+
* focused radio, else the checked one, else the first radio moving forward or
|
|
63
|
+
* the last moving backward. The stop takes the group's first position.
|
|
64
|
+
*/
|
|
65
|
+
function collapseRadioGroups(
|
|
66
|
+
elements: HTMLElement[],
|
|
67
|
+
active: Element | null,
|
|
68
|
+
isBackward: boolean,
|
|
69
|
+
): HTMLElement[] {
|
|
70
|
+
const groups = new Map<string, HTMLInputElement[]>();
|
|
71
|
+
const groupKey = (el: HTMLElement): string | null =>
|
|
72
|
+
el instanceof HTMLInputElement &&
|
|
73
|
+
el.getAttribute('type') === 'radio' &&
|
|
74
|
+
el.name !== ''
|
|
75
|
+
? `${el.form?.id ?? ''}\u0000${el.name}`
|
|
76
|
+
: null;
|
|
77
|
+
for (const el of elements) {
|
|
78
|
+
const key = groupKey(el);
|
|
79
|
+
if (key != null) {
|
|
80
|
+
const members = groups.get(key) ?? [];
|
|
81
|
+
members.push(el as HTMLInputElement);
|
|
82
|
+
groups.set(key, members);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const placed = new Set<string>();
|
|
86
|
+
const result: HTMLElement[] = [];
|
|
87
|
+
for (const el of elements) {
|
|
88
|
+
const key = groupKey(el);
|
|
89
|
+
if (key == null) {
|
|
90
|
+
result.push(el);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (placed.has(key)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
placed.add(key);
|
|
97
|
+
const members = groups.get(key) ?? [];
|
|
98
|
+
const stop =
|
|
99
|
+
members.find(radio => radio === active) ??
|
|
100
|
+
members.find(radio => radio.checked) ??
|
|
101
|
+
(isBackward ? members[members.length - 1] : members[0]);
|
|
102
|
+
result.push(stop);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Move focus to a Tab stop in the dialog. Reports whether focus actually
|
|
109
|
+
* arrived, so a caller can fall back to the menu instead of stranding focus on
|
|
110
|
+
* the page.
|
|
111
|
+
*/
|
|
112
|
+
export function focusRegionControl(element: HTMLElement): boolean {
|
|
113
|
+
element.focus();
|
|
114
|
+
return element.ownerDocument.activeElement === element;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Handle Tab in a menu with a header or footer: move to the next stop of the
|
|
119
|
+
* header's controls, the menu as one stop, then the footer's controls,
|
|
120
|
+
* wrapping at either end. The key is handled in full, so a document-level
|
|
121
|
+
* focus trap never reads it a second time. A row, or a row of an open
|
|
122
|
+
* flyout below this menu, counts as the menu's stop; with no other stop,
|
|
123
|
+
* focus stays in the menu (`isFromFlyout`: a flyout that has just closed
|
|
124
|
+
* itself hands the focus back to the menu's own stop).
|
|
125
|
+
*/
|
|
126
|
+
export function moveRegionsTab(
|
|
127
|
+
event: React.KeyboardEvent,
|
|
128
|
+
menu: HTMLElement | null,
|
|
129
|
+
header: HTMLElement | null,
|
|
130
|
+
footer: HTMLElement | null,
|
|
131
|
+
isFromFlyout = false,
|
|
132
|
+
): void {
|
|
133
|
+
if (menu == null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
const active = document.activeElement;
|
|
139
|
+
// A named radio group is one stop, as the browser's own Tab order has it.
|
|
140
|
+
const cycle = collapseRadioGroups(
|
|
141
|
+
[...tabbablesIn(header), menu, ...tabbablesIn(footer)],
|
|
142
|
+
active,
|
|
143
|
+
event.shiftKey,
|
|
144
|
+
);
|
|
145
|
+
if (cycle.length === 1) {
|
|
146
|
+
// Nothing else to reach: focus stays in the menu. From a flyout that has
|
|
147
|
+
// just closed itself, that is the menu's own stop.
|
|
148
|
+
if (isFromFlyout) {
|
|
149
|
+
menu.focus();
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const menuIndex = cycle.indexOf(menu);
|
|
154
|
+
let index = menu.contains(active)
|
|
155
|
+
? menuIndex
|
|
156
|
+
: cycle.findIndex(
|
|
157
|
+
element => element === active || element.contains(active),
|
|
158
|
+
);
|
|
159
|
+
if (index < 0) {
|
|
160
|
+
index = menuIndex;
|
|
161
|
+
}
|
|
162
|
+
const step = event.shiftKey ? -1 : 1;
|
|
163
|
+
const next = cycle[(index + step + cycle.length) % cycle.length];
|
|
164
|
+
if (!focusRegionControl(next)) {
|
|
165
|
+
menu.focus();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -12,7 +12,7 @@ export const docs = {
|
|
|
12
12
|
name: 'title',
|
|
13
13
|
type: 'string',
|
|
14
14
|
description:
|
|
15
|
-
'Primary message rendered as
|
|
15
|
+
'Primary message rendered as the configured heading level inside the empty state.',
|
|
16
16
|
required: true,
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -81,6 +81,7 @@ export const docs = {
|
|
|
81
81
|
{ guidance: false, description: 'Use an EmptyState for error messages that require immediate action; use a Banner instead.' },
|
|
82
82
|
],
|
|
83
83
|
anatomy: [
|
|
84
|
+
{name: 'Container', required: true, description: 'The centered status region that owns spacing, reading order, and the compact layout variant.'},
|
|
84
85
|
{name: 'Icon', required: false, description: 'A visual cue above the title that reinforces the context, like a search icon for no results.'},
|
|
85
86
|
{name: 'Title', required: true, description: 'Primary message explaining what is empty: "No projects yet" not "No data".'},
|
|
86
87
|
{name: 'Description', required: false, description: 'Additional context explaining why it is empty or what the user can do.'},
|
|
@@ -98,7 +99,7 @@ export const docsZh = {
|
|
|
98
99
|
name: 'title',
|
|
99
100
|
type: 'string',
|
|
100
101
|
description:
|
|
101
|
-
'
|
|
102
|
+
'在空状态内部按配置的标题级别渲染主要信息。',
|
|
102
103
|
required: true,
|
|
103
104
|
},
|
|
104
105
|
{
|