@sit-onyx/headless 1.0.0-alpha.11 → 1.0.0-alpha.12
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/package.json +1 -1
- package/src/composables/comboBox/createComboBox.ts +5 -0
- package/src/composables/menuButton/TestMenuButton.vue +2 -8
- package/src/composables/menuButton/createMenuButton.ct.ts +56 -1
- package/src/composables/menuButton/createMenuButton.ts +73 -17
- package/src/composables/outsideClick.ts +1 -1
package/package.json
CHANGED
|
@@ -173,6 +173,11 @@ export const createComboBox = createBuilder(
|
|
|
173
173
|
};
|
|
174
174
|
|
|
175
175
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
176
|
+
if (event.key === "Enter") {
|
|
177
|
+
// prevent submitting on pressing enter when the combo box is used inside a <form>
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
}
|
|
180
|
+
|
|
176
181
|
if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
|
|
177
182
|
onToggle?.();
|
|
178
183
|
if (event.key === " ") {
|
|
@@ -12,11 +12,7 @@ const activeItem = ref<string>();
|
|
|
12
12
|
const {
|
|
13
13
|
elements: { button, menu, menuItem, listItem, flyout },
|
|
14
14
|
state: { isExpanded },
|
|
15
|
-
} = createMenuButton({
|
|
16
|
-
onSelect: (value) => {
|
|
17
|
-
activeItem.value = value;
|
|
18
|
-
},
|
|
19
|
-
});
|
|
15
|
+
} = createMenuButton({});
|
|
20
16
|
</script>
|
|
21
17
|
|
|
22
18
|
<template>
|
|
@@ -24,9 +20,7 @@ const {
|
|
|
24
20
|
<div v-bind="flyout">
|
|
25
21
|
<ul v-show="isExpanded" v-bind="menu">
|
|
26
22
|
<li v-for="item in items" v-bind="listItem" :key="item.value" title="item">
|
|
27
|
-
<a v-bind="menuItem({ active: activeItem === item.value
|
|
28
|
-
item.label
|
|
29
|
-
}}</a>
|
|
23
|
+
<a v-bind="menuItem({ active: activeItem === item.value })" href="#">{{ item.label }}</a>
|
|
30
24
|
</li>
|
|
31
25
|
</ul>
|
|
32
26
|
</div>
|
|
@@ -24,7 +24,12 @@ export type MenuButtonTestingOptions = {
|
|
|
24
24
|
* Playwright utility for executing accessibility testing for a navigation menu.
|
|
25
25
|
* Will check aria attributes and keyboard shortcuts as defined in https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links.
|
|
26
26
|
*/
|
|
27
|
-
export const menuButtonTesting = async ({
|
|
27
|
+
export const menuButtonTesting = async ({
|
|
28
|
+
page,
|
|
29
|
+
button,
|
|
30
|
+
menu,
|
|
31
|
+
menuItems,
|
|
32
|
+
}: MenuButtonTestingOptions) => {
|
|
28
33
|
const menuId = await menu.getAttribute("id");
|
|
29
34
|
expect(menuId).toBeDefined();
|
|
30
35
|
await expect(
|
|
@@ -51,4 +56,54 @@ export const menuButtonTesting = async ({ button, menu }: MenuButtonTestingOptio
|
|
|
51
56
|
button,
|
|
52
57
|
'flyout menu must have an "aria-expanded" attribute set to true',
|
|
53
58
|
).toHaveAttribute("aria-expanded", "true");
|
|
59
|
+
|
|
60
|
+
const firstItem = menuItems[0].getByRole("menuitem");
|
|
61
|
+
const secondItem = menuItems[1].getByRole("menuitem");
|
|
62
|
+
const lastItem = menuItems[menuItems.length - 1].getByRole("menuitem");
|
|
63
|
+
|
|
64
|
+
await page.keyboard.press("Tab");
|
|
65
|
+
await expect(button, "Button should be focused when pressing tab key").toBeFocused();
|
|
66
|
+
|
|
67
|
+
await button.press("ArrowDown");
|
|
68
|
+
await expect(
|
|
69
|
+
firstItem,
|
|
70
|
+
"First item should be focused when pressing arrow down key",
|
|
71
|
+
).toBeFocused();
|
|
72
|
+
|
|
73
|
+
await menu.press("ArrowDown");
|
|
74
|
+
await expect(
|
|
75
|
+
secondItem,
|
|
76
|
+
"Second item should be focused when pressing arrow down key",
|
|
77
|
+
).toBeFocused();
|
|
78
|
+
|
|
79
|
+
await menu.press("ArrowUp");
|
|
80
|
+
await expect(firstItem, "First item should be focused when pressing arrow up key").toBeFocused();
|
|
81
|
+
|
|
82
|
+
await menu.press("ArrowRight");
|
|
83
|
+
await expect(
|
|
84
|
+
secondItem,
|
|
85
|
+
"Second item should be focused when pressing arrow right key",
|
|
86
|
+
).toBeFocused();
|
|
87
|
+
|
|
88
|
+
await menu.press("ArrowLeft");
|
|
89
|
+
await expect(
|
|
90
|
+
firstItem,
|
|
91
|
+
"First item should be focused when pressing arrow left key",
|
|
92
|
+
).toBeFocused();
|
|
93
|
+
|
|
94
|
+
await page.keyboard.press("Tab");
|
|
95
|
+
await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
|
|
96
|
+
|
|
97
|
+
await page.keyboard.press("Tab");
|
|
98
|
+
|
|
99
|
+
await menu.press("Home");
|
|
100
|
+
await expect(firstItem, "First item should be focused when pressing home key").toBeFocused();
|
|
101
|
+
|
|
102
|
+
await page.keyboard.press("Tab");
|
|
103
|
+
await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
|
|
104
|
+
|
|
105
|
+
await page.keyboard.press("Tab");
|
|
106
|
+
|
|
107
|
+
await menu.press("End");
|
|
108
|
+
await expect(lastItem, "Last item should be focused when pressing end key").toBeFocused();
|
|
54
109
|
};
|
|
@@ -3,17 +3,13 @@ import { createBuilder } from "../../utils/builder";
|
|
|
3
3
|
import { createId } from "../../utils/id";
|
|
4
4
|
import { debounce } from "../../utils/timer";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
onSelect: (value: string) => void;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const createMenuButton = createBuilder((options: CreateMenuButtonOptions) => {
|
|
6
|
+
/**
|
|
7
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
|
|
8
|
+
*/
|
|
9
|
+
export const createMenuButton = createBuilder(() => {
|
|
14
10
|
const menuId = createId("menu");
|
|
15
11
|
const buttonId = createId("menu-button");
|
|
16
|
-
const isExpanded = ref
|
|
12
|
+
const isExpanded = ref(false);
|
|
17
13
|
|
|
18
14
|
/**
|
|
19
15
|
* Debounced expanded state that will only be toggled after a given timeout.
|
|
@@ -32,6 +28,67 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
32
28
|
};
|
|
33
29
|
});
|
|
34
30
|
|
|
31
|
+
const focusRelativeItem = (next: "next" | "prev" | "first" | "last") => {
|
|
32
|
+
const currentMenuItem = document.activeElement as HTMLElement;
|
|
33
|
+
|
|
34
|
+
// Either the current focus is on a "menuitem", then we can just get the parent menu.
|
|
35
|
+
// Or the current focus is on the button, then we can get the connected menu using the menuId
|
|
36
|
+
const currentMenu =
|
|
37
|
+
currentMenuItem?.closest('[role="menu"]') || document.getElementById(menuId);
|
|
38
|
+
if (!currentMenu) return;
|
|
39
|
+
|
|
40
|
+
const menuItems = [...currentMenu.querySelectorAll<HTMLElement>('[role="menuitem"]')];
|
|
41
|
+
let nextIndex = 0;
|
|
42
|
+
|
|
43
|
+
if (currentMenuItem) {
|
|
44
|
+
const currentIndex = menuItems.indexOf(currentMenuItem);
|
|
45
|
+
switch (next) {
|
|
46
|
+
case "next":
|
|
47
|
+
nextIndex = currentIndex + 1;
|
|
48
|
+
break;
|
|
49
|
+
case "prev":
|
|
50
|
+
nextIndex = currentIndex - 1;
|
|
51
|
+
break;
|
|
52
|
+
case "first":
|
|
53
|
+
nextIndex = 0;
|
|
54
|
+
break;
|
|
55
|
+
case "last":
|
|
56
|
+
nextIndex = menuItems.length - 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nextMenuItem = menuItems[nextIndex];
|
|
62
|
+
nextMenuItem?.focus();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
66
|
+
switch (event.key) {
|
|
67
|
+
case "ArrowDown":
|
|
68
|
+
case "ArrowRight":
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
focusRelativeItem("next");
|
|
71
|
+
break;
|
|
72
|
+
case "ArrowUp":
|
|
73
|
+
case "ArrowLeft":
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
focusRelativeItem("prev");
|
|
76
|
+
break;
|
|
77
|
+
case "Home":
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
focusRelativeItem("first");
|
|
80
|
+
break;
|
|
81
|
+
case "End":
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
focusRelativeItem("last");
|
|
84
|
+
break;
|
|
85
|
+
case " ":
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
(event.target as HTMLElement).click();
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
35
92
|
return {
|
|
36
93
|
state: { isExpanded },
|
|
37
94
|
elements: {
|
|
@@ -43,11 +100,9 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
43
100
|
"aria-haspopup": true,
|
|
44
101
|
id: buttonId,
|
|
45
102
|
...hoverEvents.value,
|
|
103
|
+
onKeydown: handleKeydown,
|
|
46
104
|
}) as const,
|
|
47
105
|
),
|
|
48
|
-
listItem: {
|
|
49
|
-
role: "none",
|
|
50
|
-
},
|
|
51
106
|
flyout: {
|
|
52
107
|
...hoverEvents.value,
|
|
53
108
|
},
|
|
@@ -55,14 +110,15 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
55
110
|
id: menuId,
|
|
56
111
|
role: "menu",
|
|
57
112
|
"aria-labelledby": buttonId,
|
|
113
|
+
onKeydown: handleKeydown,
|
|
114
|
+
},
|
|
115
|
+
listItem: {
|
|
116
|
+
role: "none",
|
|
58
117
|
},
|
|
59
|
-
menuItem: (data: { active?: boolean;
|
|
118
|
+
menuItem: (data: { active?: boolean; disabled?: boolean }) => ({
|
|
60
119
|
"aria-current": data.active ? "page" : undefined,
|
|
120
|
+
"aria-disabled": data.disabled,
|
|
61
121
|
role: "menuitem",
|
|
62
|
-
tabindex: -1,
|
|
63
|
-
onClick: () => {
|
|
64
|
-
options.onSelect(data.value);
|
|
65
|
-
},
|
|
66
122
|
}),
|
|
67
123
|
},
|
|
68
124
|
};
|
|
@@ -28,7 +28,7 @@ export const useOutsideClick = (options: UseOutsideClickOptions) => {
|
|
|
28
28
|
const component = options.queryComponent();
|
|
29
29
|
if (!component || !(event.target instanceof Node)) return;
|
|
30
30
|
|
|
31
|
-
const isOutsideClick = !
|
|
31
|
+
const isOutsideClick = !component.contains(event.target);
|
|
32
32
|
if (isOutsideClick) options.onOutsideClick();
|
|
33
33
|
};
|
|
34
34
|
|