aria-ease 6.5.1 → 6.6.0
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/README.md +14 -10
- package/bin/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/bin/cli.cjs +50 -33
- package/bin/cli.js +1 -1
- package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
- package/bin/{test-C3CMRHSI.js → test-LP723IXM.js} +2 -2
- package/dist/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
- package/dist/index.cjs +94 -39
- package/dist/index.js +46 -8
- package/dist/src/menu/index.cjs +44 -6
- package/dist/src/menu/index.js +44 -6
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +143 -30
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +7 -7
- package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-RGKMGXND.js} +51 -29
- package/dist/src/utils/test/index.cjs +50 -33
- package/dist/src/utils/test/index.js +2 -2
- package/package.json +1 -1
package/dist/src/menu/index.cjs
CHANGED
|
@@ -47,11 +47,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
47
47
|
for (let i = 0; i < allItems.length; i++) {
|
|
48
48
|
const item = allItems.item(i);
|
|
49
49
|
const isNested = isItemInNestedSubmenu(item);
|
|
50
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
50
51
|
if (!isNested) {
|
|
51
52
|
if (!item.hasAttribute("tabindex")) {
|
|
52
53
|
item.setAttribute("tabindex", "-1");
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
+
if (!isDisabled) {
|
|
56
|
+
filteredItems.push(item);
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -76,9 +79,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
76
79
|
const items = getItems();
|
|
77
80
|
items.forEach((item) => {
|
|
78
81
|
item.setAttribute("role", "menuitem");
|
|
79
|
-
|
|
82
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
83
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
84
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
80
85
|
item.setAttribute("aria-haspopup", "menu");
|
|
81
|
-
item.setAttribute("aria-controls",
|
|
86
|
+
item.setAttribute("aria-controls", submenuId);
|
|
87
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
88
|
+
item.setAttribute("aria-expanded", "false");
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
});
|
|
84
92
|
}
|
|
@@ -87,24 +95,29 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
87
95
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
88
96
|
elementItems.item(nextIndex).focus();
|
|
89
97
|
}
|
|
98
|
+
function focusItemAtIndex(items, index) {
|
|
99
|
+
if (items.length === 0) return;
|
|
100
|
+
items[index]?.focus();
|
|
101
|
+
}
|
|
90
102
|
function hasSubmenu(menuItem) {
|
|
91
103
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
92
104
|
}
|
|
93
105
|
intializeMenuItems();
|
|
94
106
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
95
107
|
switch (event.key) {
|
|
96
|
-
case "ArrowUp":
|
|
97
108
|
case "ArrowLeft": {
|
|
98
109
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
99
110
|
event.preventDefault();
|
|
100
111
|
closeMenu();
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "ArrowUp": {
|
|
103
117
|
event.preventDefault();
|
|
104
118
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
105
119
|
break;
|
|
106
120
|
}
|
|
107
|
-
case "ArrowDown":
|
|
108
121
|
case "ArrowRight": {
|
|
109
122
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
110
123
|
event.preventDefault();
|
|
@@ -114,10 +127,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
114
127
|
return;
|
|
115
128
|
}
|
|
116
129
|
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "ArrowDown": {
|
|
117
133
|
event.preventDefault();
|
|
118
134
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
119
135
|
break;
|
|
120
136
|
}
|
|
137
|
+
case "Home": {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case "End": {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
const items = getFilteredItems();
|
|
145
|
+
focusItemAtIndex(items, items.length - 1);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
121
148
|
case "Escape": {
|
|
122
149
|
event.preventDefault();
|
|
123
150
|
closeMenu();
|
|
@@ -130,7 +157,18 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
130
157
|
case "Enter":
|
|
131
158
|
case " ": {
|
|
132
159
|
event.preventDefault();
|
|
160
|
+
if (hasSubmenu(menuItem)) {
|
|
161
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
162
|
+
if (submenuId) {
|
|
163
|
+
openSubmenu(submenuId);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
133
167
|
menuItem.click();
|
|
168
|
+
closeMenu();
|
|
169
|
+
if (onOpenChange) {
|
|
170
|
+
onOpenChange(false);
|
|
171
|
+
}
|
|
134
172
|
break;
|
|
135
173
|
}
|
|
136
174
|
case "Tab": {
|
|
@@ -237,6 +275,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
237
275
|
}
|
|
238
276
|
}
|
|
239
277
|
function closeMenu() {
|
|
278
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
240
279
|
setAria(false);
|
|
241
280
|
menuDiv.style.display = "none";
|
|
242
281
|
removeListeners();
|
|
@@ -268,7 +307,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
268
307
|
}
|
|
269
308
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
270
309
|
document.addEventListener("click", handleClickOutside);
|
|
271
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
272
310
|
function cleanup() {
|
|
273
311
|
removeListeners();
|
|
274
312
|
triggerButton.removeEventListener("click", handleTriggerClick);
|
package/dist/src/menu/index.js
CHANGED
|
@@ -45,11 +45,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
45
45
|
for (let i = 0; i < allItems.length; i++) {
|
|
46
46
|
const item = allItems.item(i);
|
|
47
47
|
const isNested = isItemInNestedSubmenu(item);
|
|
48
|
+
const isDisabled = item.getAttribute("aria-disabled") === "true";
|
|
48
49
|
if (!isNested) {
|
|
49
50
|
if (!item.hasAttribute("tabindex")) {
|
|
50
51
|
item.setAttribute("tabindex", "-1");
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
if (!isDisabled) {
|
|
54
|
+
filteredItems.push(item);
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -74,9 +77,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
74
77
|
const items = getItems();
|
|
75
78
|
items.forEach((item) => {
|
|
76
79
|
item.setAttribute("role", "menuitem");
|
|
77
|
-
|
|
80
|
+
const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
|
|
81
|
+
const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
|
|
82
|
+
if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
|
|
78
83
|
item.setAttribute("aria-haspopup", "menu");
|
|
79
|
-
item.setAttribute("aria-controls",
|
|
84
|
+
item.setAttribute("aria-controls", submenuId);
|
|
85
|
+
if (!item.hasAttribute("aria-expanded")) {
|
|
86
|
+
item.setAttribute("aria-expanded", "false");
|
|
87
|
+
}
|
|
80
88
|
}
|
|
81
89
|
});
|
|
82
90
|
}
|
|
@@ -85,24 +93,29 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
85
93
|
const nextIndex = (currentIndex + direction + len) % len;
|
|
86
94
|
elementItems.item(nextIndex).focus();
|
|
87
95
|
}
|
|
96
|
+
function focusItemAtIndex(items, index) {
|
|
97
|
+
if (items.length === 0) return;
|
|
98
|
+
items[index]?.focus();
|
|
99
|
+
}
|
|
88
100
|
function hasSubmenu(menuItem) {
|
|
89
101
|
return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
|
|
90
102
|
}
|
|
91
103
|
intializeMenuItems();
|
|
92
104
|
function handleItemsKeydown(event, menuItem, menuItemIndex) {
|
|
93
105
|
switch (event.key) {
|
|
94
|
-
case "ArrowUp":
|
|
95
106
|
case "ArrowLeft": {
|
|
96
107
|
if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
|
|
97
108
|
event.preventDefault();
|
|
98
109
|
closeMenu();
|
|
99
110
|
return;
|
|
100
111
|
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "ArrowUp": {
|
|
101
115
|
event.preventDefault();
|
|
102
116
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
|
|
103
117
|
break;
|
|
104
118
|
}
|
|
105
|
-
case "ArrowDown":
|
|
106
119
|
case "ArrowRight": {
|
|
107
120
|
if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
|
|
108
121
|
event.preventDefault();
|
|
@@ -112,10 +125,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
112
125
|
return;
|
|
113
126
|
}
|
|
114
127
|
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case "ArrowDown": {
|
|
115
131
|
event.preventDefault();
|
|
116
132
|
moveFocus(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
|
|
117
133
|
break;
|
|
118
134
|
}
|
|
135
|
+
case "Home": {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
focusItemAtIndex(getFilteredItems(), 0);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "End": {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
const items = getFilteredItems();
|
|
143
|
+
focusItemAtIndex(items, items.length - 1);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
119
146
|
case "Escape": {
|
|
120
147
|
event.preventDefault();
|
|
121
148
|
closeMenu();
|
|
@@ -128,7 +155,18 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
128
155
|
case "Enter":
|
|
129
156
|
case " ": {
|
|
130
157
|
event.preventDefault();
|
|
158
|
+
if (hasSubmenu(menuItem)) {
|
|
159
|
+
const submenuId = menuItem.getAttribute("aria-controls");
|
|
160
|
+
if (submenuId) {
|
|
161
|
+
openSubmenu(submenuId);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
131
165
|
menuItem.click();
|
|
166
|
+
closeMenu();
|
|
167
|
+
if (onOpenChange) {
|
|
168
|
+
onOpenChange(false);
|
|
169
|
+
}
|
|
132
170
|
break;
|
|
133
171
|
}
|
|
134
172
|
case "Tab": {
|
|
@@ -235,6 +273,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
function closeMenu() {
|
|
276
|
+
submenuInstances.forEach((instance) => instance.closeMenu());
|
|
238
277
|
setAria(false);
|
|
239
278
|
menuDiv.style.display = "none";
|
|
240
279
|
removeListeners();
|
|
@@ -266,7 +305,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
|
|
|
266
305
|
}
|
|
267
306
|
triggerButton.addEventListener("click", handleTriggerClick);
|
|
268
307
|
document.addEventListener("click", handleClickOutside);
|
|
269
|
-
triggerButton.setAttribute("data-menu-initialized", "true");
|
|
270
308
|
function cleanup() {
|
|
271
309
|
removeListeners();
|
|
272
310
|
triggerButton.removeEventListener("click", handleTriggerClick);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"created": "11-02-2026",
|
|
6
6
|
"lastUpdated": "15-03-2026",
|
|
7
|
-
"description": "ARIA Menu interaction contract. Validates the ARIA and interaction contract for a custom menu component following the ARIA Authoring Practices Guide menu with popup pattern
|
|
7
|
+
"description": "ARIA Menu interaction contract. Validates the ARIA and interaction contract for a custom menu component following the ARIA Authoring Practices Guide menu with popup pattern",
|
|
8
8
|
"source": {
|
|
9
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/",
|
|
10
10
|
"wcag": ["2.2 AA"]
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"trigger": "[data-test-id=menu-trigger]",
|
|
17
17
|
"container": "[role=menu]",
|
|
18
18
|
"items": "[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]",
|
|
19
|
-
"submenuTrigger": "[role=menu] [role=menuitem][aria-haspopup=menu][aria-
|
|
20
|
-
"submenu": "[role=menu][
|
|
19
|
+
"submenuTrigger": "[role=menu] [role=menuitem][aria-haspopup=true], [role=menu] [role=menuitemradio][aria-haspopup=true], [role=menu] [role=menuitemcheckbox][aria-haspopup=true], [role=menu] [role=menuitem][aria-haspopup=menu], [role=menu] [role=menuitemradio][aria-haspopup=menu], [role=menu] [role=menuitemcheckbox][aria-haspopup=menu]",
|
|
20
|
+
"submenu": "[role=menu] [role=menu]",
|
|
21
|
+
"submenuItems": "[role=menu] [role=menu] > [role=menuitem], [role=menu] [role=menu] > [role=menuitemradio], [role=menu] [role=menu] > [role=menuitemcheckbox]",
|
|
22
|
+
"leafItem": "[role=menu] [role=menuitem]:not([aria-haspopup]), [role=menu] [role=menuitemradio]:not([aria-haspopup]), [role=menu] [role=menuitemcheckbox]:not([aria-haspopup])",
|
|
21
23
|
"focusable": "[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]",
|
|
22
24
|
"relative": "[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]",
|
|
23
25
|
"popup": "[role=menu]"
|
|
@@ -34,18 +36,33 @@
|
|
|
34
36
|
"type": "aria-reference",
|
|
35
37
|
"from": "trigger",
|
|
36
38
|
"attribute": "aria-controls",
|
|
37
|
-
"to": "container"
|
|
39
|
+
"to": "container",
|
|
40
|
+
"failureMessage": "Menu trigger does not conform to the ARIA Menu pattern as specified in APG 1.2. Menu trigger should have aria-controls attribute that points to the menu it controls."
|
|
38
41
|
},
|
|
39
42
|
{
|
|
40
43
|
"type": "aria-reference",
|
|
41
44
|
"from": "container",
|
|
42
45
|
"attribute": "aria-labelledby",
|
|
43
|
-
"to": "trigger"
|
|
46
|
+
"to": "trigger",
|
|
47
|
+
"isOptional": true,
|
|
48
|
+
"failureMessage": "The APG 1.2 recommends a menu container to have aria-labelledby attribute that references the trigger that controls the menu."
|
|
44
49
|
},
|
|
45
50
|
{
|
|
46
51
|
"type": "contains",
|
|
47
52
|
"parent": "container",
|
|
48
53
|
"child": "items"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "contains",
|
|
57
|
+
"parent": "submenu",
|
|
58
|
+
"child": "submenuItems"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "aria-reference",
|
|
62
|
+
"from": "submenuTrigger",
|
|
63
|
+
"attribute": "aria-controls",
|
|
64
|
+
"to": "submenu",
|
|
65
|
+
"failureMessage": "Submenu trigger does not conform to the ARIA Menu pattern as specified in APG 1.2. Submenu trigger should have aria-controls attribute that references the submenu it controls."
|
|
49
66
|
}
|
|
50
67
|
],
|
|
51
68
|
|
|
@@ -83,21 +100,28 @@
|
|
|
83
100
|
"assertion": "toHaveAttribute",
|
|
84
101
|
"attribute": "role",
|
|
85
102
|
"expectedValue": "menu",
|
|
86
|
-
"failureMessage": "Menu container doesn't conform to the ARIA Menu pattern as specified in APG 1.2. Menu container should have role of 'menu'"
|
|
103
|
+
"failureMessage": "Menu container doesn't conform to the ARIA Menu pattern as specified in APG 1.2. Menu container should have role of 'menu'."
|
|
87
104
|
},
|
|
88
105
|
{
|
|
89
106
|
"target": "items",
|
|
90
107
|
"assertion": "toHaveAttribute",
|
|
91
108
|
"attribute": "role",
|
|
92
109
|
"expectedValue": "menuitem | menuitemcheckbox | menuitemradio",
|
|
93
|
-
"failureMessage": "Menu items do not conform to the ARIA Menu pattern as specified in APG 1.2. Menu items should have role of 'menuitem', 'menuitemcheckbox', or 'menuitemradio'"
|
|
110
|
+
"failureMessage": "Menu items do not conform to the ARIA Menu pattern as specified in APG 1.2. Menu items should have role of 'menuitem', 'menuitemcheckbox', or 'menuitemradio'."
|
|
94
111
|
},
|
|
95
112
|
{
|
|
96
113
|
"target": "items",
|
|
97
114
|
"assertion": "toHaveAttribute",
|
|
98
115
|
"attribute": "tabindex",
|
|
99
116
|
"expectedValue": "-1",
|
|
100
|
-
"failureMessage": "Menu items do not conform to the ARIA Menu pattern as specified in APG 1.2. Menu items' tabindex should have value of '-1'. This ensures the menu items are excluded from the page's Tab sequence"
|
|
117
|
+
"failureMessage": "Menu items do not conform to the ARIA Menu pattern as specified in APG 1.2. Menu items' tabindex should have value of '-1'. This ensures the menu items are excluded from the page's Tab sequence."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"target": "submenuTrigger",
|
|
121
|
+
"assertion": "toHaveAttribute",
|
|
122
|
+
"attribute": "aria-haspopup",
|
|
123
|
+
"expectedValue":"true | menu",
|
|
124
|
+
"failureMessage": "Submenu trigger does not conform to the ARIA Menu pattern as specified in APG 1.2. Submenu trigger should have aria-haspop set to 'true' or 'menu'."
|
|
101
125
|
}
|
|
102
126
|
]
|
|
103
127
|
}
|
|
@@ -105,7 +129,7 @@
|
|
|
105
129
|
|
|
106
130
|
"dynamic": [
|
|
107
131
|
{
|
|
108
|
-
"description": "Clicking the trigger opens the menu, updates ARIA
|
|
132
|
+
"description": "Clicking the trigger opens the menu, updates ARIA expanded, and focuses the first interactive item in the menu.",
|
|
109
133
|
"action": [
|
|
110
134
|
{ "type": "click", "target": "trigger" }
|
|
111
135
|
],
|
|
@@ -131,7 +155,7 @@
|
|
|
131
155
|
]
|
|
132
156
|
},
|
|
133
157
|
{
|
|
134
|
-
"description": "Clicking the trigger again closes the menu and returns focus to trigger.",
|
|
158
|
+
"description": "Clicking the trigger again closes the menu, updates ARIA expanded, and returns focus to trigger.",
|
|
135
159
|
"action": [
|
|
136
160
|
{ "type": "click", "target": "trigger" },
|
|
137
161
|
{ "type": "click", "target": "trigger" }
|
|
@@ -157,7 +181,7 @@
|
|
|
157
181
|
]
|
|
158
182
|
},
|
|
159
183
|
{
|
|
160
|
-
"description": "Pressing Enter on trigger opens the menu and focuses first item.",
|
|
184
|
+
"description": "Pressing Enter on trigger opens the menu, updates ARIA expanded, and focuses first item.",
|
|
161
185
|
"action": [
|
|
162
186
|
{ "type": "keypress", "target": "trigger", "key": "Enter" }
|
|
163
187
|
],
|
|
@@ -183,7 +207,7 @@
|
|
|
183
207
|
]
|
|
184
208
|
},
|
|
185
209
|
{
|
|
186
|
-
"description": "Pressing Space on trigger opens the menu and focuses first item.",
|
|
210
|
+
"description": "Pressing Space on trigger opens the menu, updates ARIA expanded, and focuses first item.",
|
|
187
211
|
"action": [
|
|
188
212
|
{ "type": "keypress", "target": "trigger", "key": "Space" }
|
|
189
213
|
],
|
|
@@ -219,7 +243,7 @@
|
|
|
219
243
|
"target": "relative",
|
|
220
244
|
"assertion": "toHaveFocus",
|
|
221
245
|
"expectedValue": "second",
|
|
222
|
-
"failureMessage": "
|
|
246
|
+
"failureMessage": "Down Arrow should move focus to next menu item."
|
|
223
247
|
}
|
|
224
248
|
]
|
|
225
249
|
},
|
|
@@ -235,43 +259,44 @@
|
|
|
235
259
|
"target": "relative",
|
|
236
260
|
"assertion": "toHaveFocus",
|
|
237
261
|
"expectedValue": "first",
|
|
238
|
-
"failureMessage": "
|
|
262
|
+
"failureMessage": "Up Arrow should move focus to previous menu item."
|
|
239
263
|
}
|
|
240
264
|
]
|
|
241
265
|
},
|
|
242
266
|
{
|
|
243
|
-
"description": "
|
|
267
|
+
"description": "Up Arrow from first item wraps to last.",
|
|
244
268
|
"action": [
|
|
245
269
|
{ "type": "click", "target": "trigger" },
|
|
246
|
-
{ "type": "keypress", "target": "focusable", "key": "
|
|
270
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowUp" }
|
|
271
|
+
|
|
247
272
|
],
|
|
248
273
|
"assertions": [
|
|
249
274
|
{
|
|
250
275
|
"target": "relative",
|
|
251
276
|
"assertion": "toHaveFocus",
|
|
252
|
-
"expectedValue": "
|
|
253
|
-
"failureMessage": "
|
|
277
|
+
"expectedValue": "last",
|
|
278
|
+
"failureMessage": "Up Arrow should wrap focus from first item wraps to last."
|
|
254
279
|
}
|
|
255
280
|
]
|
|
256
281
|
},
|
|
257
282
|
{
|
|
258
|
-
"description": "
|
|
283
|
+
"description": "Down Arrow from last item wraps to first item.",
|
|
259
284
|
"action": [
|
|
260
285
|
{ "type": "click", "target": "trigger" },
|
|
261
|
-
{ "type": "keypress", "target": "focusable", "key": "
|
|
262
|
-
{ "type": "keypress", "target": "focusable", "key": "
|
|
286
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowUp" },
|
|
287
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" }
|
|
263
288
|
],
|
|
264
289
|
"assertions": [
|
|
265
290
|
{
|
|
266
291
|
"target": "relative",
|
|
267
292
|
"assertion": "toHaveFocus",
|
|
268
293
|
"expectedValue": "first",
|
|
269
|
-
"failureMessage": "
|
|
294
|
+
"failureMessage": "Down Arrow should wrap focus from last item wraps to first item."
|
|
270
295
|
}
|
|
271
296
|
]
|
|
272
297
|
},
|
|
273
298
|
{
|
|
274
|
-
"description": "Escape closes the menu and returns focus to trigger.",
|
|
299
|
+
"description": "Escape closes the menu, updates ARIA expanded, and returns focus to trigger.",
|
|
275
300
|
"action": [
|
|
276
301
|
{ "type": "click", "target": "trigger" },
|
|
277
302
|
{ "type": "keypress", "target": "focusable", "key": "Escape" }
|
|
@@ -297,7 +322,7 @@
|
|
|
297
322
|
]
|
|
298
323
|
},
|
|
299
324
|
{
|
|
300
|
-
"description": "Pressing Enter on trigger when menu is open closes the menu.",
|
|
325
|
+
"description": "Pressing Enter on trigger when menu is open closes the menu and updates ARIA expanded.",
|
|
301
326
|
"action": [
|
|
302
327
|
{ "type": "click", "target": "trigger" },
|
|
303
328
|
{ "type": "keypress", "target": "trigger", "key": "Enter" }
|
|
@@ -318,7 +343,7 @@
|
|
|
318
343
|
]
|
|
319
344
|
},
|
|
320
345
|
{
|
|
321
|
-
"description": "Pressing Space on trigger when menu is open closes the menu.",
|
|
346
|
+
"description": "Pressing Space on trigger when menu is open closes the menu and updates ARIA expanded.",
|
|
322
347
|
"action": [
|
|
323
348
|
{ "type": "click", "target": "trigger" },
|
|
324
349
|
{ "type": "keypress", "target": "trigger", "key": "Space" }
|
|
@@ -339,7 +364,7 @@
|
|
|
339
364
|
]
|
|
340
365
|
},
|
|
341
366
|
{
|
|
342
|
-
"description": "Clicking outside the menu closes it and returns focus to trigger.",
|
|
367
|
+
"description": "Clicking outside the menu closes it, updates ARIA expanded, and returns focus to trigger.",
|
|
343
368
|
"action": [
|
|
344
369
|
{ "type": "click", "target": "trigger" },
|
|
345
370
|
{ "type": "click", "target": "document" }
|
|
@@ -365,7 +390,7 @@
|
|
|
365
390
|
]
|
|
366
391
|
},
|
|
367
392
|
{
|
|
368
|
-
"description": "Right Arrow on menuitem with submenu opens the submenu",
|
|
393
|
+
"description": "Right Arrow on menuitem with submenu opens the submenu, updates ARIA expanded, and focuses the first interactive element of the menu.",
|
|
369
394
|
"action": [
|
|
370
395
|
{ "type": "click", "target": "trigger" },
|
|
371
396
|
{ "type": "keypress", "target": "submenuTrigger", "key": "ArrowRight" }
|
|
@@ -374,12 +399,24 @@
|
|
|
374
399
|
{
|
|
375
400
|
"target": "submenu",
|
|
376
401
|
"assertion": "toBeVisible",
|
|
377
|
-
"failureMessage": "Submenu should open when Right Arrow pressed on item with submenu"
|
|
402
|
+
"failureMessage": "Submenu should open when Right Arrow pressed on item with submenu."
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"target": "submenuTrigger",
|
|
406
|
+
"assertion": "toHaveAttribute",
|
|
407
|
+
"attribute": "aria-expanded",
|
|
408
|
+
"expectedValue": "true",
|
|
409
|
+
"failureMessage": "Submenu trigger's aria-expanded should be true after Right Arrow open the submenu."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
"target": "submenuItems",
|
|
413
|
+
"assertion": "toHaveFocus",
|
|
414
|
+
"failureMessage": "First interactive item in the submenu should have focus after Right Arrow open the submenu."
|
|
378
415
|
}
|
|
379
416
|
]
|
|
380
417
|
},
|
|
381
418
|
{
|
|
382
|
-
"description": "Tab on a menuitem closes the menu and update aria-expanded",
|
|
419
|
+
"description": "Tab on a menuitem closes the menu and update aria-expanded.",
|
|
383
420
|
"action": [
|
|
384
421
|
{ "type": "click", "target": "trigger" },
|
|
385
422
|
{ "type": "keypress", "target": "focusable", "key": "Tab" }
|
|
@@ -396,11 +433,23 @@
|
|
|
396
433
|
"attribute": "aria-expanded",
|
|
397
434
|
"expectedValue": "false",
|
|
398
435
|
"failureMessage": "Trigger's aria-expanded should be false after Tab closes the menu."
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"target": "submenu",
|
|
439
|
+
"assertion": "notToBeVisible",
|
|
440
|
+
"failureMessage": "Shift+Tab should close all submenus, not just top-level menu."
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"target": "submenuTrigger",
|
|
444
|
+
"assertion": "toHaveAttribute",
|
|
445
|
+
"attribute": "aria-expanded",
|
|
446
|
+
"expectedValue": "false",
|
|
447
|
+
"failureMessage": "Submenu trigger's aria-expanded should be false after Shift+Tab closes the submenu."
|
|
399
448
|
}
|
|
400
449
|
]
|
|
401
450
|
},
|
|
402
451
|
{
|
|
403
|
-
"description": "Shift+Tab on a menuitem closes the menu and update aria-expanded",
|
|
452
|
+
"description": "Shift+Tab on a menuitem closes the menu and update aria-expanded.",
|
|
404
453
|
"action": [
|
|
405
454
|
{ "type": "click", "target": "trigger" },
|
|
406
455
|
{ "type": "keypress", "target": "focusable", "key": "Shift+Tab" }
|
|
@@ -417,6 +466,70 @@
|
|
|
417
466
|
"attribute": "aria-expanded",
|
|
418
467
|
"expectedValue": "false",
|
|
419
468
|
"failureMessage": "Trigger's aria-expanded should be false after Shift+Tab closes the menu."
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
"target": "submenu",
|
|
472
|
+
"assertion": "notToBeVisible",
|
|
473
|
+
"failureMessage": "Shift+Tab should close all submenus, not just top-level menu."
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"target": "submenuTrigger",
|
|
477
|
+
"assertion": "toHaveAttribute",
|
|
478
|
+
"attribute": "aria-expanded",
|
|
479
|
+
"expectedValue": "false",
|
|
480
|
+
"failureMessage": "Submenu trigger's aria-expanded should be false after Shift+Tab closes the submenu."
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
"description": "Enter on a menuitem without submenu activates the item and closes the menu.",
|
|
486
|
+
"action": [
|
|
487
|
+
{ "type": "click", "target": "trigger" },
|
|
488
|
+
{ "type": "keypress", "target": "leafItem", "key": "Enter" }
|
|
489
|
+
],
|
|
490
|
+
"assertions": [
|
|
491
|
+
{
|
|
492
|
+
"target": "container",
|
|
493
|
+
"assertion": "notToBeVisible",
|
|
494
|
+
"failureMessage": "Menu should close after Enter on a menuitem without submenu."
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"target": "trigger",
|
|
498
|
+
"assertion": "toHaveAttribute",
|
|
499
|
+
"attribute": "aria-expanded",
|
|
500
|
+
"expectedValue": "false",
|
|
501
|
+
"failureMessage": "Trigger's aria-expanded should be false after Enter on a menuitem without submenu."
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"description": "Home moves focus to first item.",
|
|
507
|
+
"action": [
|
|
508
|
+
{ "type": "click", "target": "trigger" },
|
|
509
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" },
|
|
510
|
+
{ "type": "keypress", "target": "focusable", "key": "Home" }
|
|
511
|
+
],
|
|
512
|
+
"assertions": [
|
|
513
|
+
{
|
|
514
|
+
"target": "relative",
|
|
515
|
+
"assertion": "toHaveFocus",
|
|
516
|
+
"expectedValue": "first",
|
|
517
|
+
"failureMessage": "First interactive item in the menu should have focus after pressing Home."
|
|
518
|
+
}
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"description": "End moves focus to last item.",
|
|
523
|
+
"action": [
|
|
524
|
+
{ "type": "click", "target": "trigger" },
|
|
525
|
+
{ "type": "keypress", "target": "focusable", "key": "End" }
|
|
526
|
+
],
|
|
527
|
+
"assertions": [
|
|
528
|
+
{
|
|
529
|
+
"target": "relative",
|
|
530
|
+
"assertion": "toHaveFocus",
|
|
531
|
+
"expectedValue": "last",
|
|
532
|
+
"failureMessage": "Last interactive item in the menu should have focus after pressing End."
|
|
420
533
|
}
|
|
421
534
|
]
|
|
422
535
|
}
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
]
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
|
-
"description": "Right Arrow moves focus to next tab and activates it (automatic activation).",
|
|
108
|
+
"description": "Right Arrow moves focus to next tab and activates it (automatic activation) in horizontally oriented tabs.",
|
|
109
109
|
"isVertical": false,
|
|
110
110
|
"action": [
|
|
111
111
|
{ "type": "focus", "target": "focusable" },
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
]
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
|
-
"description": "Left Arrow moves focus to previous tab and activates it (wraps to last).",
|
|
148
|
+
"description": "Left Arrow moves focus to previous tab and activates it (wraps to last) in horizontally oriented tabs.",
|
|
149
149
|
"isVertical": false,
|
|
150
150
|
"action": [
|
|
151
151
|
{ "type": "focus", "target": "focusable" },
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"target": "relative",
|
|
157
157
|
"assertion": "toHaveFocus",
|
|
158
158
|
"expectedValue": "last",
|
|
159
|
-
"failureMessage": "Focus should wrap to last tab after pressing Left Arrow on first tab."
|
|
159
|
+
"failureMessage": "Focus should wrap to last tab after pressing Left Arrow on first tab in horizontally oriented tabs."
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"target": "relative",
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
]
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
|
-
"description": "Down Arrow moves focus to next tab and activates it
|
|
180
|
+
"description": "Down Arrow moves focus to next tab and activates it in vertically oriented tabs.",
|
|
181
181
|
"isVertical": true,
|
|
182
182
|
"action": [
|
|
183
183
|
{ "type": "focus", "target": "focusable" },
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"target": "relative",
|
|
189
189
|
"assertion": "toHaveFocus",
|
|
190
190
|
"expectedValue": "second",
|
|
191
|
-
"failureMessage": "Focus should move to second tab after pressing Down Arrow
|
|
191
|
+
"failureMessage": "Focus should move to second tab after pressing Down Arrow in vertically oriented tabs."
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"target": "relative",
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
]
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
|
-
"description": "Up Arrow moves focus to previous tab and activates it (wraps to last
|
|
204
|
+
"description": "Up Arrow moves focus to previous tab and activates it (wraps to last) in vertically oriented tabs.",
|
|
205
205
|
"isVertical": true,
|
|
206
206
|
"action": [
|
|
207
207
|
{ "type": "focus", "target": "focusable" },
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
"target": "relative",
|
|
213
213
|
"assertion": "toHaveFocus",
|
|
214
214
|
"expectedValue": "last",
|
|
215
|
-
"failureMessage": "Focus should wrap to last tab after pressing Up Arrow on first tab
|
|
215
|
+
"failureMessage": "Focus should wrap to last tab after pressing Up Arrow on first tab in vertically oriented tabs."
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
"target": "relative",
|