@astryxdesign/core 0.7.2 → 0.7.3-canary.02f5fe4
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/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +7 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +58 -9
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +34 -54
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +125 -58
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +13 -10
- package/src/TimeInput/TimeInput.test.tsx +139 -7
- package/src/TimeInput/TimeInput.tsx +53 -24
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuItemAudit.a11y.chromium.spec.ts
|
|
5
|
+
* @input Exact-head DropdownMenuItem story across states, themes, direction, and viewport widths
|
|
6
|
+
* @output Screenshots and fail-closed sensor receipts for the DropdownMenuItem component audit
|
|
7
|
+
* @position Component-owned Night Watch browser evidence; CI runs it only for a DropdownMenuItem 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-item-audit-evidence');
|
|
23
|
+
const STORY_ID = 'core-dropdownmenuitem--item-states';
|
|
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 Interaction = 'rest' | 'focus' | 'hover';
|
|
31
|
+
type AuditCase = {
|
|
32
|
+
key: string;
|
|
33
|
+
mode: Mode;
|
|
34
|
+
direction: Direction;
|
|
35
|
+
theme: Theme;
|
|
36
|
+
interaction: Interaction;
|
|
37
|
+
viewport: {width: number; height: number};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const cases: AuditCase[] = [
|
|
41
|
+
{
|
|
42
|
+
key: 'neutral-light',
|
|
43
|
+
mode: 'light',
|
|
44
|
+
direction: 'ltr',
|
|
45
|
+
theme: 'neutral',
|
|
46
|
+
interaction: 'rest',
|
|
47
|
+
viewport: WIDE,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'neutral-dark',
|
|
51
|
+
mode: 'dark',
|
|
52
|
+
direction: 'ltr',
|
|
53
|
+
theme: 'neutral',
|
|
54
|
+
interaction: 'rest',
|
|
55
|
+
viewport: WIDE,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'focus-light',
|
|
59
|
+
mode: 'light',
|
|
60
|
+
direction: 'ltr',
|
|
61
|
+
theme: 'neutral',
|
|
62
|
+
interaction: 'focus',
|
|
63
|
+
viewport: WIDE,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'focus-dark',
|
|
67
|
+
mode: 'dark',
|
|
68
|
+
direction: 'ltr',
|
|
69
|
+
theme: 'neutral',
|
|
70
|
+
interaction: 'focus',
|
|
71
|
+
viewport: WIDE,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'hover-light',
|
|
75
|
+
mode: 'light',
|
|
76
|
+
direction: 'ltr',
|
|
77
|
+
theme: 'neutral',
|
|
78
|
+
interaction: 'hover',
|
|
79
|
+
viewport: WIDE,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'hover-dark',
|
|
83
|
+
mode: 'dark',
|
|
84
|
+
direction: 'ltr',
|
|
85
|
+
theme: 'neutral',
|
|
86
|
+
interaction: 'hover',
|
|
87
|
+
viewport: WIDE,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'butter-light',
|
|
91
|
+
mode: 'light',
|
|
92
|
+
direction: 'ltr',
|
|
93
|
+
theme: 'butter',
|
|
94
|
+
interaction: 'rest',
|
|
95
|
+
viewport: WIDE,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: 'butter-dark',
|
|
99
|
+
mode: 'dark',
|
|
100
|
+
direction: 'ltr',
|
|
101
|
+
theme: 'butter',
|
|
102
|
+
interaction: 'rest',
|
|
103
|
+
viewport: WIDE,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: 'rtl-light',
|
|
107
|
+
mode: 'light',
|
|
108
|
+
direction: 'rtl',
|
|
109
|
+
theme: 'neutral',
|
|
110
|
+
interaction: 'rest',
|
|
111
|
+
viewport: WIDE,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: 'rtl-dark',
|
|
115
|
+
mode: 'dark',
|
|
116
|
+
direction: 'rtl',
|
|
117
|
+
theme: 'neutral',
|
|
118
|
+
interaction: 'rest',
|
|
119
|
+
viewport: WIDE,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'narrow-light',
|
|
123
|
+
mode: 'light',
|
|
124
|
+
direction: 'ltr',
|
|
125
|
+
theme: 'neutral',
|
|
126
|
+
interaction: 'rest',
|
|
127
|
+
viewport: NARROW,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: 'narrow-dark',
|
|
131
|
+
mode: 'dark',
|
|
132
|
+
direction: 'ltr',
|
|
133
|
+
theme: 'neutral',
|
|
134
|
+
interaction: 'rest',
|
|
135
|
+
viewport: NARROW,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
test.describe.configure({mode: 'serial', retries: 0});
|
|
140
|
+
let server: StaticServer;
|
|
141
|
+
let checkoutSha: string;
|
|
142
|
+
let storybookSha: string;
|
|
143
|
+
|
|
144
|
+
function storyUrl(item: AuditCase) {
|
|
145
|
+
return `${server.origin}/iframe.html?id=${STORY_ID}&viewMode=story&globals=colorMode:${item.mode};astryxTheme:${item.theme};direction:${item.direction}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function settle(page: Page) {
|
|
149
|
+
await holdMotionStill(page);
|
|
150
|
+
await page.evaluate(async () => document.fonts.ready);
|
|
151
|
+
await page.waitForTimeout(40);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
test.beforeAll(async () => {
|
|
155
|
+
fs.rmSync(OUTPUT, {recursive: true, force: true});
|
|
156
|
+
fs.mkdirSync(OUTPUT, {recursive: true});
|
|
157
|
+
checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
}).trim();
|
|
160
|
+
const expected = process.env.ASTRYX_HEAD_SHA;
|
|
161
|
+
if (expected && expected !== checkoutSha) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`checkout ${checkoutSha} does not match expected head ${expected}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
server = await serveStorybook(
|
|
167
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
168
|
+
);
|
|
169
|
+
const response = await fetch(`${server.origin}/astryx-build-sha.txt`);
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`Storybook build has no SHA stamp (${response.status})`);
|
|
172
|
+
}
|
|
173
|
+
storybookSha = (await response.text()).trim();
|
|
174
|
+
if (storybookSha !== checkoutSha) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Storybook ${storybookSha} does not match checkout ${checkoutSha}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test.afterAll(async () => {
|
|
182
|
+
await server?.close();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
for (const item of cases) {
|
|
186
|
+
test(item.key, async ({browser}) => {
|
|
187
|
+
const context = await browser.newContext({
|
|
188
|
+
viewport: item.viewport,
|
|
189
|
+
colorScheme: item.mode,
|
|
190
|
+
reducedMotion: 'reduce',
|
|
191
|
+
});
|
|
192
|
+
const page = await context.newPage();
|
|
193
|
+
const pageErrors: string[] = [];
|
|
194
|
+
page.on('pageerror', error => pageErrors.push(String(error)));
|
|
195
|
+
await page.goto(storyUrl(item), {waitUntil: 'load'});
|
|
196
|
+
await page.locator('#storybook-root').waitFor();
|
|
197
|
+
|
|
198
|
+
const menu = page.getByRole('menu').last();
|
|
199
|
+
await expect(menu).toBeVisible();
|
|
200
|
+
const rows = menu.getByRole('menuitem');
|
|
201
|
+
await expect(rows).toHaveCount(5);
|
|
202
|
+
const defaultRow = page.getByTestId('default-row');
|
|
203
|
+
const supportingRow = page.getByTestId('supporting-row');
|
|
204
|
+
const destructiveRow = page.getByTestId('destructive-row');
|
|
205
|
+
const disabledRow = page.getByTestId('disabled-row');
|
|
206
|
+
const linkRow = page.getByTestId('link-row');
|
|
207
|
+
await expect(disabledRow).toHaveAttribute('aria-disabled', 'true');
|
|
208
|
+
await expect(destructiveRow).toHaveAttribute('data-variant', 'destructive');
|
|
209
|
+
await expect(linkRow).toHaveAttribute('href', '/settings');
|
|
210
|
+
await expect(linkRow).toHaveAttribute('role', 'menuitem');
|
|
211
|
+
await expect(menu.locator('button, [role="button"]')).toHaveCount(0);
|
|
212
|
+
|
|
213
|
+
if (item.interaction === 'focus') {
|
|
214
|
+
await defaultRow.focus();
|
|
215
|
+
await expect(defaultRow).toBeFocused();
|
|
216
|
+
} else if (item.interaction === 'hover') {
|
|
217
|
+
await supportingRow.hover();
|
|
218
|
+
}
|
|
219
|
+
await settle(page);
|
|
220
|
+
|
|
221
|
+
const observed = await menu.evaluate(element => {
|
|
222
|
+
const currentRows = Array.from(
|
|
223
|
+
element.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
224
|
+
);
|
|
225
|
+
const describe = (row: HTMLElement | undefined) => {
|
|
226
|
+
if (!row) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const style = getComputedStyle(row);
|
|
230
|
+
const box = row.getBoundingClientRect();
|
|
231
|
+
return {
|
|
232
|
+
tagName: row.tagName,
|
|
233
|
+
classTarget: row.classList.contains('astryx-dropdown-menu-item'),
|
|
234
|
+
role: row.getAttribute('role'),
|
|
235
|
+
disabled: row.getAttribute('aria-disabled'),
|
|
236
|
+
variant: row.getAttribute('data-variant'),
|
|
237
|
+
size: row.getAttribute('data-size'),
|
|
238
|
+
href: row.getAttribute('href'),
|
|
239
|
+
tabIndex: row.getAttribute('tabindex'),
|
|
240
|
+
backgroundColor: style.backgroundColor,
|
|
241
|
+
color: style.color,
|
|
242
|
+
opacity: style.opacity,
|
|
243
|
+
outlineStyle: style.outlineStyle,
|
|
244
|
+
outlineColor: style.outlineColor,
|
|
245
|
+
geometry: {x: box.x, y: box.y, width: box.width, height: box.height},
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
theme: document
|
|
250
|
+
.querySelector('[data-astryx-theme]')
|
|
251
|
+
?.getAttribute('data-astryx-theme'),
|
|
252
|
+
mode: document.documentElement.getAttribute('data-theme'),
|
|
253
|
+
direction: getComputedStyle(currentRows[0]).direction,
|
|
254
|
+
viewport: {width: innerWidth, height: innerHeight},
|
|
255
|
+
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
256
|
+
horizontalOverflow:
|
|
257
|
+
document.documentElement.scrollWidth > innerWidth + 1,
|
|
258
|
+
fonts: document.fonts.status,
|
|
259
|
+
activeText: document.activeElement?.textContent
|
|
260
|
+
?.replace(/\s+/g, ' ')
|
|
261
|
+
.trim(),
|
|
262
|
+
rows: currentRows.map(describe),
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const semanticPass =
|
|
267
|
+
observed.rows.length === 5 &&
|
|
268
|
+
observed.rows.every(row => row?.role === 'menuitem' && row.classTarget) &&
|
|
269
|
+
observed.rows[2]?.variant === 'destructive' &&
|
|
270
|
+
observed.rows[3]?.disabled === 'true' &&
|
|
271
|
+
observed.rows[4]?.tagName === 'A' &&
|
|
272
|
+
observed.rows[4]?.href === '/settings' &&
|
|
273
|
+
observed.rows.every(
|
|
274
|
+
row =>
|
|
275
|
+
row !== null && row.geometry.width > 0 && row.geometry.height > 0,
|
|
276
|
+
);
|
|
277
|
+
const paintPass =
|
|
278
|
+
observed.rows[2]?.color !== observed.rows[0]?.color &&
|
|
279
|
+
Number(observed.rows[3]?.opacity ?? 1) < 1;
|
|
280
|
+
const interactionPaintPass =
|
|
281
|
+
item.interaction === 'rest' ||
|
|
282
|
+
(item.interaction === 'focus' &&
|
|
283
|
+
observed.rows[0]?.backgroundColor !==
|
|
284
|
+
observed.rows[1]?.backgroundColor) ||
|
|
285
|
+
(item.interaction === 'hover' &&
|
|
286
|
+
observed.rows[1]?.backgroundColor !==
|
|
287
|
+
observed.rows[0]?.backgroundColor);
|
|
288
|
+
const interactionPass =
|
|
289
|
+
(item.interaction !== 'focus' ||
|
|
290
|
+
observed.activeText?.includes('Edit project')) &&
|
|
291
|
+
interactionPaintPass;
|
|
292
|
+
|
|
293
|
+
const png = await page.screenshot({animations: 'disabled', fullPage: true});
|
|
294
|
+
const image = `${item.key}.png`;
|
|
295
|
+
const receipt = {
|
|
296
|
+
schemaVersion: 1,
|
|
297
|
+
expected: {
|
|
298
|
+
build: checkoutSha,
|
|
299
|
+
storyId: STORY_ID,
|
|
300
|
+
mode: item.mode,
|
|
301
|
+
direction: item.direction,
|
|
302
|
+
theme: item.theme,
|
|
303
|
+
interaction: item.interaction,
|
|
304
|
+
viewport: item.viewport,
|
|
305
|
+
},
|
|
306
|
+
observed: {build: storybookSha, ...observed, pageErrors},
|
|
307
|
+
image: {
|
|
308
|
+
file: image,
|
|
309
|
+
sha256: createHash('sha256').update(png).digest('hex'),
|
|
310
|
+
width: png.readUInt32BE(16),
|
|
311
|
+
height: png.readUInt32BE(20),
|
|
312
|
+
},
|
|
313
|
+
checks: {
|
|
314
|
+
semanticPass,
|
|
315
|
+
paintPass,
|
|
316
|
+
interactionPaintPass,
|
|
317
|
+
interactionPass,
|
|
318
|
+
},
|
|
319
|
+
passed:
|
|
320
|
+
storybookSha === checkoutSha &&
|
|
321
|
+
pageErrors.length === 0 &&
|
|
322
|
+
observed.theme === item.theme &&
|
|
323
|
+
observed.mode === item.mode &&
|
|
324
|
+
observed.direction === item.direction &&
|
|
325
|
+
observed.viewport.width === item.viewport.width &&
|
|
326
|
+
observed.viewport.height === item.viewport.height &&
|
|
327
|
+
observed.reducedMotion &&
|
|
328
|
+
!observed.horizontalOverflow &&
|
|
329
|
+
observed.fonts === 'loaded' &&
|
|
330
|
+
semanticPass &&
|
|
331
|
+
paintPass &&
|
|
332
|
+
interactionPass,
|
|
333
|
+
};
|
|
334
|
+
fs.writeFileSync(path.join(OUTPUT, image), png);
|
|
335
|
+
fs.writeFileSync(
|
|
336
|
+
path.join(OUTPUT, `${item.key}.sensors.json`),
|
|
337
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
338
|
+
);
|
|
339
|
+
expect(receipt.passed).toBe(true);
|
|
340
|
+
await context.close();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
test('manifest is complete', () => {
|
|
345
|
+
const receipts = fs
|
|
346
|
+
.readdirSync(OUTPUT)
|
|
347
|
+
.filter(file => file.endsWith('.sensors.json'));
|
|
348
|
+
fs.writeFileSync(
|
|
349
|
+
path.join(OUTPUT, 'manifest.json'),
|
|
350
|
+
`${JSON.stringify(
|
|
351
|
+
{
|
|
352
|
+
schemaVersion: 1,
|
|
353
|
+
component: 'core/DropdownMenuItem',
|
|
354
|
+
build: checkoutSha,
|
|
355
|
+
frames: cases.map(item => ({
|
|
356
|
+
key: item.key,
|
|
357
|
+
image: `${item.key}.png`,
|
|
358
|
+
receipt: `${item.key}.sensors.json`,
|
|
359
|
+
})),
|
|
360
|
+
},
|
|
361
|
+
null,
|
|
362
|
+
2,
|
|
363
|
+
)}\n`,
|
|
364
|
+
);
|
|
365
|
+
expect(receipts).toHaveLength(cases.length);
|
|
366
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file SubMenuHeaderFooter.a11y.chromium.spec.ts
|
|
5
|
+
* @input The built Storybook "Submenu / header and footer" story (a Labels
|
|
6
|
+
* sub-menu with a filter field above its checkbox rows and a "Create label"
|
|
7
|
+
* button below them) and real Chromium, once as a laptop (fine pointer) and
|
|
8
|
+
* once as a phone (coarse pointer, touch).
|
|
9
|
+
* @output Evidence for component:DropdownMenu FR20: a sub-menu's header and
|
|
10
|
+
* footer sit with its rows in a modal dialog named by its row, beside every
|
|
11
|
+
* menu element rather than inside one; the row says it opens a dialog; Tab
|
|
12
|
+
* and Shift+Tab walk field, rows and button and wrap; typing filters the
|
|
13
|
+
* rows and Escape returns to the row; a pick keeps the sub-menu open. On a
|
|
14
|
+
* phone the drilled-in view holds the same regions while the root menu
|
|
15
|
+
* steps aside. On the "Submenu / header with a choice" story, a flyout the
|
|
16
|
+
* pointer has left stays open while an arrow moves the header's radio choice.
|
|
17
|
+
* @position Browser proof behind the jsdom suite: the accessibility tree, real
|
|
18
|
+
* Tab order and the pointer decision are what jsdom cannot exercise.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {devices, expect, test, type Locator, type Page} from '@playwright/test';
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_STORYBOOK_DIR,
|
|
24
|
+
serveStorybook,
|
|
25
|
+
type StaticServer,
|
|
26
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
27
|
+
|
|
28
|
+
let storybook: StaticServer;
|
|
29
|
+
const storyId = 'core-dropdownmenu--sub-menu-header-and-footer';
|
|
30
|
+
|
|
31
|
+
test.beforeAll(async () => {
|
|
32
|
+
storybook = await serveStorybook(
|
|
33
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
test.afterAll(async () => {
|
|
37
|
+
await storybook?.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function mount(page: Page) {
|
|
41
|
+
await page.goto(
|
|
42
|
+
`${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`,
|
|
43
|
+
);
|
|
44
|
+
await expect(page.getByTestId('submenu-regions-fixture')).toBeVisible();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether the element sits inside any role="menu" element. */
|
|
48
|
+
async function isInsideAMenu(page: Page, testName: string): Promise<boolean> {
|
|
49
|
+
return page
|
|
50
|
+
.getByRole('dialog', {name: testName})
|
|
51
|
+
.evaluate(el => el.parentElement?.closest('[role="menu"]') != null);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function bounds(locator: Locator) {
|
|
55
|
+
const rect = await locator.boundingBox();
|
|
56
|
+
if (rect == null) {
|
|
57
|
+
throw new Error('no rendered bounds');
|
|
58
|
+
}
|
|
59
|
+
return rect;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test.describe('on a laptop (fine pointer)', () => {
|
|
63
|
+
test('the sub-menu opens a dialog beside the menu, named by its row, holding field, rows and button', async ({
|
|
64
|
+
page,
|
|
65
|
+
}) => {
|
|
66
|
+
await mount(page);
|
|
67
|
+
await page.getByRole('button', {name: 'Session'}).click();
|
|
68
|
+
const row = page.getByRole('menuitem', {name: 'Labels'});
|
|
69
|
+
await expect(row).toHaveAttribute('aria-haspopup', 'dialog');
|
|
70
|
+
await row.click();
|
|
71
|
+
|
|
72
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
73
|
+
await expect(dialog).toBeVisible();
|
|
74
|
+
await expect(dialog).toHaveAttribute('aria-modal', 'true');
|
|
75
|
+
await expect(row).toHaveAttribute(
|
|
76
|
+
'aria-controls',
|
|
77
|
+
(await dialog.getAttribute('id')) ?? '',
|
|
78
|
+
);
|
|
79
|
+
expect(await isInsideAMenu(page, 'Labels')).toBe(false);
|
|
80
|
+
// The sub-menu's own menu holds only its rows.
|
|
81
|
+
const menu = dialog.getByRole('menu', {name: 'Labels'});
|
|
82
|
+
expect(
|
|
83
|
+
await menu.evaluate(el =>
|
|
84
|
+
Array.from(el.querySelectorAll('[role]')).map(child =>
|
|
85
|
+
child.getAttribute('role'),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
).toEqual(Array(5).fill('menuitemcheckbox'));
|
|
89
|
+
await expect(
|
|
90
|
+
dialog.getByRole('textbox', {name: 'Filter labels'}),
|
|
91
|
+
).toBeVisible();
|
|
92
|
+
await expect(
|
|
93
|
+
dialog.getByRole('button', {name: 'Create label'}),
|
|
94
|
+
).toBeVisible();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Tab and Shift+Tab walk field, rows and button and wrap; typing filters; Escape returns to the row', async ({
|
|
98
|
+
page,
|
|
99
|
+
}) => {
|
|
100
|
+
await mount(page);
|
|
101
|
+
const trigger = page.getByRole('button', {name: 'Session'});
|
|
102
|
+
await trigger.focus();
|
|
103
|
+
await page.keyboard.press('Enter');
|
|
104
|
+
// A keyboard open lands on the first row; let it land before moving on.
|
|
105
|
+
await expect(page.getByRole('menuitem', {name: 'Rename'})).toBeFocused();
|
|
106
|
+
const row = page.getByRole('menuitem', {name: 'Labels'});
|
|
107
|
+
await page.keyboard.press('ArrowDown');
|
|
108
|
+
await expect(row).toBeFocused();
|
|
109
|
+
await page.keyboard.press('ArrowRight');
|
|
110
|
+
|
|
111
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
112
|
+
const field = dialog.getByRole('textbox', {name: 'Filter labels'});
|
|
113
|
+
const create = dialog.getByRole('button', {name: 'Create label'});
|
|
114
|
+
const menu = dialog.getByRole('menu', {name: 'Labels'});
|
|
115
|
+
// A keyboard open lands on the first row, not the field.
|
|
116
|
+
await expect(
|
|
117
|
+
dialog.getByRole('menuitemcheckbox', {name: 'Bug'}),
|
|
118
|
+
).toBeFocused();
|
|
119
|
+
|
|
120
|
+
await page.keyboard.press('Tab');
|
|
121
|
+
await expect(create).toBeFocused();
|
|
122
|
+
await page.keyboard.press('Tab');
|
|
123
|
+
await expect(field).toBeFocused();
|
|
124
|
+
await page.keyboard.press('Tab');
|
|
125
|
+
await expect(menu).toBeFocused();
|
|
126
|
+
await page.keyboard.press('Shift+Tab');
|
|
127
|
+
await expect(field).toBeFocused();
|
|
128
|
+
await page.keyboard.press('Shift+Tab');
|
|
129
|
+
await expect(create).toBeFocused();
|
|
130
|
+
await expect(dialog).toBeVisible();
|
|
131
|
+
|
|
132
|
+
// The field keeps its keys: letters type and filter the rows, and the
|
|
133
|
+
// arrow that closes a sub-menu from a row moves the caret instead.
|
|
134
|
+
await field.focus();
|
|
135
|
+
await page.keyboard.type('doc');
|
|
136
|
+
await page.keyboard.press('ArrowLeft');
|
|
137
|
+
await expect(field).toHaveValue('doc');
|
|
138
|
+
await expect(dialog.getByRole('menuitemcheckbox')).toHaveText([
|
|
139
|
+
'Documentation',
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
await page.keyboard.press('Escape');
|
|
143
|
+
await expect(dialog).toBeHidden();
|
|
144
|
+
await expect(row).toBeFocused();
|
|
145
|
+
await expect(page.getByRole('menu', {name: 'Session'})).toBeVisible();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a pick keeps the sub-menu open; the button acts without closing anything', async ({
|
|
149
|
+
page,
|
|
150
|
+
}) => {
|
|
151
|
+
await mount(page);
|
|
152
|
+
await page.getByRole('button', {name: 'Session'}).click();
|
|
153
|
+
await page.getByRole('menuitem', {name: 'Labels'}).click();
|
|
154
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
155
|
+
await dialog.getByRole('menuitemcheckbox', {name: 'Feature'}).click();
|
|
156
|
+
await expect(page.getByTestId('picked')).toHaveText('Bug, Feature');
|
|
157
|
+
await expect(dialog).toBeVisible();
|
|
158
|
+
await dialog.getByRole('button', {name: 'Create label'}).click();
|
|
159
|
+
await expect(page.getByTestId('created')).toHaveText('1');
|
|
160
|
+
await expect(dialog).toBeVisible();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test.describe('the press model', () => {
|
|
165
|
+
test('a press on the row, dragged into the flyout and released on a row, picks it', async ({
|
|
166
|
+
page,
|
|
167
|
+
}) => {
|
|
168
|
+
await mount(page);
|
|
169
|
+
await page.getByRole('button', {name: 'Session'}).click();
|
|
170
|
+
const row = page.getByRole('menuitem', {name: 'Labels'});
|
|
171
|
+
await row.hover();
|
|
172
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
173
|
+
await expect(dialog).toBeVisible();
|
|
174
|
+
const feature = dialog.getByRole('menuitemcheckbox', {name: 'Feature'});
|
|
175
|
+
const [from, to] = await Promise.all([row, feature].map(bounds));
|
|
176
|
+
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
|
|
177
|
+
await page.mouse.down();
|
|
178
|
+
await page.mouse.move(from.x + from.width - 4, from.y + from.height / 2, {
|
|
179
|
+
steps: 4,
|
|
180
|
+
});
|
|
181
|
+
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2, {
|
|
182
|
+
steps: 8,
|
|
183
|
+
});
|
|
184
|
+
await page.mouse.up();
|
|
185
|
+
await expect(page.getByTestId('picked')).toHaveText('Bug, Feature');
|
|
186
|
+
// A checkbox pick keeps the sub-menu, and the menu, open.
|
|
187
|
+
await expect(dialog).toBeVisible();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test.describe('on a phone (coarse pointer)', () => {
|
|
192
|
+
const {defaultBrowserType: _browser, ...phone} = devices['Pixel 7'];
|
|
193
|
+
test.use(phone);
|
|
194
|
+
|
|
195
|
+
test('the drilled-in view holds field, rows and button beside the root menu, which steps aside', async ({
|
|
196
|
+
page,
|
|
197
|
+
}) => {
|
|
198
|
+
await mount(page);
|
|
199
|
+
const trigger = page.getByRole('button', {name: 'Session'});
|
|
200
|
+
await trigger.tap();
|
|
201
|
+
const rootMenu = page.getByRole('menu', {name: 'Session'});
|
|
202
|
+
await expect(rootMenu).toBeVisible();
|
|
203
|
+
await page.getByRole('menuitem', {name: 'Labels'}).tap();
|
|
204
|
+
|
|
205
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
206
|
+
await expect(dialog).toBeVisible();
|
|
207
|
+
await expect(rootMenu).toBeHidden();
|
|
208
|
+
expect(await isInsideAMenu(page, 'Labels')).toBe(false);
|
|
209
|
+
// Same popover: the view replaces the root's rows in place.
|
|
210
|
+
expect(
|
|
211
|
+
await dialog.evaluate(
|
|
212
|
+
el =>
|
|
213
|
+
el.closest('[popover]') ===
|
|
214
|
+
document
|
|
215
|
+
.querySelector('[role="menu"][aria-label="Session"]')
|
|
216
|
+
?.closest('[popover]'),
|
|
217
|
+
),
|
|
218
|
+
).toBe(true);
|
|
219
|
+
await expect(
|
|
220
|
+
dialog.getByRole('menuitem', {name: 'Back to Session'}),
|
|
221
|
+
).toBeVisible();
|
|
222
|
+
await expect(
|
|
223
|
+
dialog.getByRole('textbox', {name: 'Filter labels'}),
|
|
224
|
+
).toBeVisible();
|
|
225
|
+
await expect(
|
|
226
|
+
dialog.getByRole('button', {name: 'Create label'}),
|
|
227
|
+
).toBeVisible();
|
|
228
|
+
|
|
229
|
+
await dialog.getByRole('menuitem', {name: 'Back to Session'}).tap();
|
|
230
|
+
await expect(dialog).toBeHidden();
|
|
231
|
+
await expect(rootMenu).toBeVisible();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test.describe('a header with a choice', () => {
|
|
236
|
+
test('a flyout the pointer has left stays open while an arrow moves the choice in its header', async ({
|
|
237
|
+
page,
|
|
238
|
+
}) => {
|
|
239
|
+
await page.goto(
|
|
240
|
+
`${storybook.origin}/iframe.html?id=core-dropdownmenu--sub-menu-header-choice&viewMode=story`,
|
|
241
|
+
);
|
|
242
|
+
await expect(page.getByTestId('submenu-choice-fixture')).toBeVisible();
|
|
243
|
+
await page.getByRole('button', {name: 'Filters'}).click();
|
|
244
|
+
const row = page.getByRole('menuitem', {name: 'Labels'});
|
|
245
|
+
await row.hover();
|
|
246
|
+
const dialog = page.getByRole('dialog', {name: 'Labels'});
|
|
247
|
+
await expect(dialog).toBeVisible();
|
|
248
|
+
const anyLabel = dialog.getByRole('radio', {name: 'Any label'});
|
|
249
|
+
const allLabels = dialog.getByRole('radio', {name: 'All labels'});
|
|
250
|
+
await anyLabel.click();
|
|
251
|
+
await expect(anyLabel).toBeFocused();
|
|
252
|
+
|
|
253
|
+
// The pointer leaves the flyout and the menu, and its hide delay runs out
|
|
254
|
+
// with focus still inside.
|
|
255
|
+
const viewport = page.viewportSize() ?? {width: 1280, height: 720};
|
|
256
|
+
await page.mouse.move(viewport.width - 8, viewport.height - 8, {steps: 4});
|
|
257
|
+
await page.waitForTimeout(500);
|
|
258
|
+
await expect(dialog).toBeVisible();
|
|
259
|
+
|
|
260
|
+
// The browser moves focus and the choice for the arrow itself.
|
|
261
|
+
await page.keyboard.press('ArrowDown');
|
|
262
|
+
await expect(allLabels).toBeFocused();
|
|
263
|
+
await expect(allLabels).toBeChecked();
|
|
264
|
+
await expect(page.getByTestId('match')).toHaveText('all');
|
|
265
|
+
await expect(dialog).toBeVisible();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* @output `tabbablesIn`, `focusRegionControl`, `moveRegionsTab`: the Tab stops
|
|
9
9
|
* of a header or footer region and the walk through a menu with regions,
|
|
10
10
|
* with a named radio group as one stop
|
|
11
|
-
* @position Internal; DropdownMenu
|
|
12
|
-
*
|
|
11
|
+
* @position Internal; used by DropdownMenu (the root menu's header and footer)
|
|
12
|
+
* and DropdownMenuSubMenu (a sub-menu's header and footer).
|
|
13
13
|
*
|
|
14
14
|
* A menu with a header or footer is a dialog: Tab and Shift+Tab walk the
|
|
15
15
|
* header's controls, the menu as one stop, then the footer's controls, and
|
|
@@ -163,6 +163,14 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
163
163
|
claimed by the innermost root carrying `data-astryx-menu-press` (a flyout
|
|
164
164
|
before its parent); the outermost root decides what counts as "inside the
|
|
165
165
|
menu" for the release.
|
|
166
|
+
- **ORD1a (proposed) — A menu beside the root.** A root carrying
|
|
167
|
+
`data-astryx-menu-press-surface`, which names another root's `id`, is nested
|
|
168
|
+
in that root for ORD1, as a flyout inside the root's DOM is: a release on
|
|
169
|
+
its rows is inside the menu. Only the menu of a sub-menu with a header or
|
|
170
|
+
footer, which the root renders beside itself (`component:DropdownMenu`
|
|
171
|
+
FR20), carries it. Anything else beside the root, a header field or a picker
|
|
172
|
+
placed in a region say, is outside the menu, as the root's own regions are,
|
|
173
|
+
and a picker there stays its own root. Pending owner review.
|
|
166
174
|
- **ORD2 — Release resolution.** Enabled row under the point → `act`; inside
|
|
167
175
|
the menu or on the trigger with no row → `settle` (stray click swallowed,
|
|
168
176
|
menu stays); outside → `settle` with `dismiss` only for a mouse.
|
|
@@ -238,6 +246,7 @@ owned by the item components.
|
|
|
238
246
|
| FR4 (scroll) | `MenuPress.a11y.chromium.spec.ts` (real Chromium) | Chromium touch pan in an overflowing menu; touch-action by overflow; finger slide and its stray click | Not provable in jsdom; a pan that acts on a row, or a slide that acts on the press row, fails |
|
|
239
247
|
| FR4 (pinch) | `MenuPinchZoom.a11y.chromium.spec.ts` (real Chromium, phone emulation) | two-finger pinch over a menu whose rows fit, over one that scrolls, and over a picker listbox | Not provable in jsdom; a pinch that leaves the page at scale 1, or acts on a row, fails |
|
|
240
248
|
| FR8, FR9 (proposed) | `menuPressGesture.test.ts` trigger states; `useMenuPress.test.tsx` trigger suite; `DropdownMenu.test.tsx` press-open cases | mouse opens at once, finger tap, held finger, settle before/after, cancel; press-open + drag-release, unsettled release, trigger toggle, held touch, tap | A menu that does not open on a mouse press, an unsettled release that acts, or a trigger that reopens in the same gesture fails |
|
|
249
|
+
| ORD1a (proposed) | `DropdownMenuSubMenu.test.tsx` press cases; `SubMenuHeaderFooter.a11y.chromium.spec.ts` press-drag case | a mouse press on a menu row released on a row of a sub-menu rendered beside the menu; the same press released on that sub-menu's header field | A release on such a row that acts on nothing or closes the menu, or a release on its header field that acts, fails. |
|
|
241
250
|
|
|
242
251
|
## Decision log
|
|
243
252
|
|
|
@@ -38,7 +38,8 @@ detached presentations; consumer usage remains documented in
|
|
|
38
38
|
|
|
39
39
|
- Released default preserved: `yes`
|
|
40
40
|
- Compatibility class: additive documentation only; runtime, DOM, styling, and
|
|
41
|
-
public API remain unchanged
|
|
41
|
+
public API remain unchanged, except that delivery inside a mounted live
|
|
42
|
+
container follows `spec:AST-086` FR4
|
|
42
43
|
- Controlled/uncontrolled behavior: not applicable
|
|
43
44
|
- Migration decision: direct `FieldStatus variant="tooltip"` support is rejected.
|
|
44
45
|
`tooltip` remains a field-family sentinel that MUST be handled before rendering
|
|
@@ -127,7 +128,7 @@ intentional decision.
|
|
|
127
128
|
- **AR1 — Programmatic announcement channel.** Errors MUST update the persistent
|
|
128
129
|
assertive region; warnings and successes MUST update the persistent polite
|
|
129
130
|
region, including on first mount and when message or type changes, without
|
|
130
|
-
moving focus.
|
|
131
|
+
moving focus. Inside a mounted live container, `spec:AST-086` FR4 applies.
|
|
131
132
|
- **AR5 — Assistive-technology output.** Claims about whether, when, or how the
|
|
132
133
|
message is spoken or presented in braille MUST be verified through the real-AT
|
|
133
134
|
evidence process in `spec:AST-009`; DOM and accessibility-tree checks prove
|