@sit-onyx/headless 1.0.0-beta.21 → 1.0.0-beta.23

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.
Files changed (86) hide show
  1. package/dist/composables/comboBox/SelectOnlyCombobox.d.vue.ts +299 -0
  2. package/dist/composables/comboBox/TestCombobox.ct.d.ts +1 -0
  3. package/dist/composables/comboBox/TestCombobox.d.vue.ts +299 -0
  4. package/dist/composables/comboBox/createComboBox.d.ts +370 -0
  5. package/dist/composables/comboBox/createComboBox.testing.d.ts +10 -0
  6. package/dist/composables/helpers/useDismissible.d.ts +10 -0
  7. package/dist/composables/helpers/useGlobalListener.d.ts +10 -0
  8. package/dist/composables/helpers/useGlobalListener.spec.d.ts +1 -0
  9. package/dist/composables/helpers/useOutsideClick.d.ts +26 -0
  10. package/dist/composables/helpers/useOutsideClick.spec.d.ts +1 -0
  11. package/dist/composables/helpers/useTypeAhead.d.ts +11 -0
  12. package/dist/composables/helpers/useTypeAhead.spec.d.ts +1 -0
  13. package/dist/composables/listbox/TestListbox.ct.d.ts +1 -0
  14. package/dist/composables/listbox/TestListbox.d.vue.ts +2 -0
  15. package/dist/composables/listbox/createListbox.d.ts +102 -0
  16. package/dist/composables/listbox/createListbox.testing.d.ts +24 -0
  17. package/dist/composables/menuButton/TestMenuButton.ct.d.ts +1 -0
  18. package/dist/composables/menuButton/TestMenuButton.d.vue.ts +2 -0
  19. package/dist/composables/menuButton/createMenuButton.d.ts +78 -0
  20. package/dist/composables/menuButton/createMenuButton.testing.d.ts +24 -0
  21. package/dist/composables/navigationMenu/TestMenu.ct.d.ts +1 -0
  22. package/dist/composables/navigationMenu/TestMenu.d.vue.ts +2 -0
  23. package/dist/composables/navigationMenu/createMenu.d.ts +21 -0
  24. package/dist/composables/navigationMenu/createMenu.testing.d.ts +16 -0
  25. package/dist/composables/tabs/TestTabs.ct.d.ts +1 -0
  26. package/dist/composables/tabs/TestTabs.d.vue.ts +2 -0
  27. package/dist/composables/tabs/createTabs.d.ts +48 -0
  28. package/dist/composables/tabs/createTabs.testing.d.ts +13 -0
  29. package/dist/composables/tooltip/createToggletip.d.ts +36 -0
  30. package/dist/composables/tooltip/createTooltip.d.ts +42 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +1089 -0
  33. package/dist/playwright.d.ts +5 -0
  34. package/dist/playwright.js +369 -0
  35. package/dist/utils/builder.d.ts +85 -0
  36. package/dist/utils/keyboard.d.ts +26 -0
  37. package/dist/utils/keyboard.spec.d.ts +1 -0
  38. package/dist/utils/math.d.ts +6 -0
  39. package/dist/utils/math.spec.d.ts +1 -0
  40. package/dist/utils/object.d.ts +5 -0
  41. package/dist/utils/object.spec.d.ts +1 -0
  42. package/dist/utils/timer.d.ts +10 -0
  43. package/{src/utils/types.ts → dist/utils/types.d.ts} +4 -12
  44. package/dist/utils/vitest.d.ts +12 -0
  45. package/package.json +18 -9
  46. package/src/composables/comboBox/SelectOnlyCombobox.vue +0 -90
  47. package/src/composables/comboBox/TestCombobox.ct.tsx +0 -24
  48. package/src/composables/comboBox/TestCombobox.vue +0 -84
  49. package/src/composables/comboBox/createComboBox.testing.ts +0 -168
  50. package/src/composables/comboBox/createComboBox.ts +0 -280
  51. package/src/composables/helpers/useDismissible.ts +0 -19
  52. package/src/composables/helpers/useGlobalListener.spec.ts +0 -93
  53. package/src/composables/helpers/useGlobalListener.ts +0 -64
  54. package/src/composables/helpers/useOutsideClick.spec.ts +0 -117
  55. package/src/composables/helpers/useOutsideClick.ts +0 -69
  56. package/src/composables/helpers/useTypeAhead.spec.ts +0 -29
  57. package/src/composables/helpers/useTypeAhead.ts +0 -26
  58. package/src/composables/listbox/TestListbox.ct.tsx +0 -17
  59. package/src/composables/listbox/TestListbox.vue +0 -92
  60. package/src/composables/listbox/createListbox.testing.ts +0 -141
  61. package/src/composables/listbox/createListbox.ts +0 -234
  62. package/src/composables/menuButton/TestMenuButton.ct.tsx +0 -14
  63. package/src/composables/menuButton/TestMenuButton.vue +0 -29
  64. package/src/composables/menuButton/createMenuButton.testing.ts +0 -91
  65. package/src/composables/menuButton/createMenuButton.ts +0 -206
  66. package/src/composables/navigationMenu/TestMenu.ct.tsx +0 -12
  67. package/src/composables/navigationMenu/TestMenu.vue +0 -16
  68. package/src/composables/navigationMenu/createMenu.testing.ts +0 -37
  69. package/src/composables/navigationMenu/createMenu.ts +0 -55
  70. package/src/composables/tabs/TestTabs.ct.tsx +0 -12
  71. package/src/composables/tabs/TestTabs.vue +0 -28
  72. package/src/composables/tabs/createTabs.testing.ts +0 -151
  73. package/src/composables/tabs/createTabs.ts +0 -129
  74. package/src/composables/tooltip/createToggletip.ts +0 -58
  75. package/src/composables/tooltip/createTooltip.ts +0 -71
  76. package/src/index.ts +0 -11
  77. package/src/playwright.ts +0 -5
  78. package/src/utils/builder.ts +0 -135
  79. package/src/utils/keyboard.spec.ts +0 -53
  80. package/src/utils/keyboard.ts +0 -351
  81. package/src/utils/math.spec.ts +0 -14
  82. package/src/utils/math.ts +0 -6
  83. package/src/utils/object.spec.ts +0 -33
  84. package/src/utils/object.ts +0 -8
  85. package/src/utils/timer.ts +0 -22
  86. package/src/utils/vitest.ts +0 -36
@@ -1,234 +0,0 @@
1
- import { computed, nextTick, ref, unref, useId, watchEffect, type MaybeRef, type Ref } from "vue";
2
- import { createBuilder, type VBindAttributes } from "../../utils/builder.js";
3
- import type { Nullable } from "../../utils/types.js";
4
- import { useTypeAhead } from "../helpers/useTypeAhead.js";
5
-
6
- export type ListboxValue = string | number | boolean;
7
-
8
- export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends boolean = false> = {
9
- /**
10
- * Aria label for the listbox.
11
- */
12
- label: MaybeRef<string>;
13
- /**
14
- * Aria description for the listbox.
15
- */
16
- description?: MaybeRef<Nullable<string>>;
17
- /**
18
- * Value of currently (visually) active option.
19
- */
20
- activeOption: Ref<Nullable<TValue>>;
21
- /**
22
- * Wether the listbox is controlled from the outside, e.g. by a combobox.
23
- * This disables keyboard events and makes the listbox not focusable.
24
- */
25
- controlled?: boolean;
26
- /**
27
- * Controls the opened/visible state of the listbox. When expanded the activeOption can be controlled via the keyboard.
28
- */
29
- isExpanded?: MaybeRef<boolean>;
30
- /**
31
- * Whether the listbox is multiselect.
32
- */
33
- multiple?: MaybeRef<Nullable<TMultiple>>;
34
- /**
35
- * Hook when an option is selected.
36
- */
37
- onSelect?: (value: TValue) => void;
38
- /**
39
- * Hook when the first option should be activated.
40
- */
41
- onActivateFirst?: () => void;
42
- /**
43
- * Hook when the last option should be activated.
44
- */
45
- onActivateLast?: () => void;
46
- /**
47
- * Hook when the next option should be activated.
48
- */
49
- onActivateNext?: (currentValue: TValue) => void;
50
- /**
51
- * Hook when the previous option should be activated.
52
- */
53
- onActivatePrevious?: (currentValue: TValue) => void;
54
- /**
55
- * Hook when the first option starting with the given label should be activated.
56
- */
57
- onTypeAhead?: (key: string) => void;
58
- } & (
59
- | {
60
- /**
61
- * Optional aria label for the listbox.
62
- */
63
- label?: MaybeRef<string>;
64
- /**
65
- * Wether the listbox is controlled from the outside, e.g. by a combobox.
66
- * This disables keyboard events and makes the listbox not focusable.
67
- */
68
- controlled: true;
69
- }
70
- | {
71
- /**
72
- * Aria label for the listbox.
73
- */
74
- label: MaybeRef<string>;
75
- controlled?: false;
76
- }
77
- );
78
-
79
- /**
80
- * Composable for creating a accessibility-conform listbox.
81
- * For supported keyboard shortcuts, see: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable/
82
- */
83
- export const createListbox = createBuilder(
84
- <TValue extends ListboxValue, TMultiple extends boolean = false>(
85
- options: CreateListboxOptions<TValue, TMultiple>,
86
- ) => {
87
- const isMultiselect = computed(() => unref(options.multiple) ?? false);
88
- const isExpanded = computed(() => unref(options.isExpanded) ?? false);
89
-
90
- /**
91
- * Map for option IDs. key = option value, key = ID for the HTML element
92
- */
93
- const descendantKeyIdMap = new Map<TValue, string>();
94
-
95
- const getOptionId = (value: TValue) => {
96
- if (!descendantKeyIdMap.has(value)) {
97
- descendantKeyIdMap.set(value, useId());
98
- }
99
- return descendantKeyIdMap.get(value)!;
100
- };
101
-
102
- /**
103
- * Whether the listbox element is focused.
104
- */
105
- const isFocused = ref(false);
106
-
107
- // scroll currently active option into view if needed
108
- watchEffect(async () => {
109
- if (
110
- !isExpanded.value ||
111
- options.activeOption.value == undefined ||
112
- (!isFocused.value && !options.controlled)
113
- ) {
114
- return;
115
- }
116
-
117
- const id = getOptionId(options.activeOption.value);
118
- await nextTick();
119
- document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
120
- });
121
-
122
- const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
123
-
124
- const handleKeydown = (event: KeyboardEvent) => {
125
- switch (event.key) {
126
- case " ":
127
- event.preventDefault();
128
- if (options.activeOption.value != undefined) {
129
- options.onSelect?.(options.activeOption.value);
130
- }
131
- break;
132
-
133
- case "ArrowUp":
134
- event.preventDefault();
135
- // if no option is active yet, activate the last option
136
- if (options.activeOption.value == undefined) {
137
- options.onActivateLast?.();
138
- return;
139
- }
140
-
141
- options.onActivatePrevious?.(options.activeOption.value);
142
- break;
143
-
144
- case "ArrowDown":
145
- event.preventDefault();
146
- // if no option is active yet, activate the first option
147
- if (options.activeOption.value == undefined) {
148
- options.onActivateFirst?.();
149
- return;
150
- }
151
-
152
- options.onActivateNext?.(options.activeOption.value);
153
- break;
154
-
155
- case "Home":
156
- event.preventDefault();
157
- options.onActivateFirst?.();
158
- break;
159
-
160
- case "End":
161
- event.preventDefault();
162
- options.onActivateLast?.();
163
- break;
164
-
165
- default:
166
- // if printable characters are pressed, the first option/text starting with the typed characters should be active
167
- typeAhead(event);
168
- }
169
- };
170
-
171
- const listbox = computed<VBindAttributes>(() =>
172
- options.controlled
173
- ? {
174
- role: "listbox",
175
- "aria-multiselectable": isMultiselect.value,
176
- "aria-label": unref(options.label),
177
- "aria-description": options.description,
178
- tabindex: "-1",
179
- }
180
- : {
181
- role: "listbox",
182
- "aria-multiselectable": isMultiselect.value,
183
- "aria-label": unref(options.label),
184
- "aria-description": options.description,
185
- tabindex: "0",
186
- "aria-activedescendant":
187
- options.activeOption.value != undefined
188
- ? getOptionId(options.activeOption.value)
189
- : undefined,
190
- onFocus: () => (isFocused.value = true),
191
- onBlur: () => (isFocused.value = false),
192
- onKeydown: handleKeydown,
193
- },
194
- );
195
-
196
- return {
197
- elements: {
198
- listbox,
199
- group: computed(() => {
200
- return (options: { label: string }) => ({
201
- role: "group",
202
- "aria-label": options.label,
203
- });
204
- }),
205
- option: computed(() => {
206
- return (data: {
207
- label: string;
208
- value: TValue;
209
- disabled?: boolean;
210
- selected?: boolean;
211
- }) => {
212
- const selected = data.selected ?? false;
213
-
214
- return {
215
- id: getOptionId(data.value),
216
- role: "option",
217
- "aria-label": data.label,
218
- "aria-disabled": data.disabled,
219
- "aria-checked": isMultiselect.value ? selected : undefined,
220
- "aria-selected": !isMultiselect.value ? selected : undefined,
221
- onClick: () => !data.disabled && options.onSelect?.(data.value),
222
- } as const;
223
- };
224
- }),
225
- },
226
- state: {
227
- isFocused,
228
- },
229
- internals: {
230
- getOptionId,
231
- },
232
- };
233
- },
234
- );
@@ -1,14 +0,0 @@
1
- import { test } from "@playwright/experimental-ct-vue";
2
- import { menuButtonTesting } from "./createMenuButton.testing.js";
3
- import TestMenuButton from "./TestMenuButton.vue";
4
-
5
- test("menuButton", async ({ mount, page }) => {
6
- await mount(<TestMenuButton />);
7
-
8
- await menuButtonTesting({
9
- page,
10
- button: page.getByRole("button"),
11
- menu: page.locator("ul"),
12
- menuItems: page.getByRole("menuitem"),
13
- });
14
- });
@@ -1,29 +0,0 @@
1
- <script lang="ts" setup>
2
- import { ref } from "vue";
3
- import { createMenuButton } from "./createMenuButton.js";
4
-
5
- const items = Array.from({ length: 10 }, (_, index) => {
6
- const id = index + 1;
7
- return { label: `Item ${id}`, value: `/href-${id}` };
8
- });
9
-
10
- const activeItem = ref<string>();
11
- const isExpanded = ref(false);
12
- const onToggle = () => (isExpanded.value = !isExpanded.value);
13
- const trigger = ref<"click" | "hover">("hover");
14
-
15
- const {
16
- elements: { root, button, menu, menuItem, listItem },
17
- } = createMenuButton({ isExpanded, onToggle, trigger });
18
- </script>
19
-
20
- <template>
21
- <div v-bind="root">
22
- <button v-bind="button" type="button">Toggle nav menu</button>
23
- <ul v-show="isExpanded" v-bind="menu">
24
- <li v-for="item in items" v-bind="listItem" :key="item.value">
25
- <a v-bind="menuItem({ active: activeItem === item.value })" href="#">{{ item.label }}</a>
26
- </li>
27
- </ul>
28
- </div>
29
- </template>
@@ -1,91 +0,0 @@
1
- import { expect } from "@playwright/experimental-ct-vue";
2
- import type { Locator, Page } from "@playwright/test";
3
-
4
- export type MenuButtonTestingOptions = {
5
- /**
6
- * Playwright page.
7
- */
8
- page: Page;
9
- /**
10
- * Locator for the button element.
11
- */
12
- button: Locator;
13
- /**
14
- * Menu, e.g. a `<ul>` element.
15
- */
16
- menu: Locator;
17
- /**
18
- * List items (at least 3).
19
- */
20
- menuItems: Locator;
21
- };
22
-
23
- /**
24
- * Playwright utility for executing accessibility testing for a navigation menu.
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
- */
27
- export const menuButtonTesting = async ({
28
- page,
29
- button,
30
- menu,
31
- menuItems,
32
- }: MenuButtonTestingOptions) => {
33
- await expect(
34
- button,
35
- 'navigation menu should have an "aria-haspopup" attribute set to true',
36
- ).toHaveAttribute("aria-haspopup", "true");
37
-
38
- await expect(button).toBeVisible();
39
-
40
- // ensure correct navigation menu aria attributes
41
- await expect(button, "button must have arial-controls attribute").toHaveAttribute(
42
- "aria-controls",
43
- );
44
- await expect(button, "button must have aria-expanded attribute").toHaveAttribute(
45
- "aria-expanded",
46
- "false",
47
- );
48
-
49
- await page.keyboard.press("Tab");
50
- await expect(button, "Button should be focused when pressing tab key").toBeFocused();
51
-
52
- const firstItem = menuItems.first();
53
- const secondItem = menuItems.nth(1);
54
- const lastItem = menuItems.last();
55
-
56
- await page.keyboard.press("Enter");
57
- await expect(button, "button must have aria-expanded attribute").toHaveAttribute(
58
- "aria-expanded",
59
- "true",
60
- );
61
- await button.press("ArrowDown");
62
- await expect(
63
- firstItem,
64
- "First item should be focused when pressing arrow down key",
65
- ).toBeFocused();
66
-
67
- await menu.press("ArrowDown");
68
- await expect(
69
- secondItem,
70
- "Second item should be focused when pressing arrow down key",
71
- ).toBeFocused();
72
-
73
- await menu.press("ArrowUp");
74
- await expect(firstItem, "First item should be focused when pressing arrow up key").toBeFocused();
75
-
76
- await page.keyboard.press("Tab");
77
- await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
78
-
79
- await page.keyboard.press("Tab");
80
-
81
- await menu.press("Home");
82
- await expect(firstItem, "First item should be focused when pressing home key").toBeFocused();
83
-
84
- await page.keyboard.press("Tab");
85
- await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
86
-
87
- await page.keyboard.press("Tab");
88
-
89
- await menu.press("End");
90
- await expect(lastItem, "Last item should be focused when pressing end key").toBeFocused();
91
- };
@@ -1,206 +0,0 @@
1
- import { computed, toValue, useId, watch, type MaybeRef, type Ref } from "vue";
2
- import { createBuilder, createElRef } from "../../utils/builder.js";
3
- import { debounce } from "../../utils/timer.js";
4
- import { useGlobalEventListener } from "../helpers/useGlobalListener.js";
5
- import { useOutsideClick } from "../helpers/useOutsideClick.js";
6
-
7
- type CreateMenuButtonOptions = {
8
- isExpanded: Readonly<Ref<boolean>>;
9
- trigger: Readonly<MaybeRef<"hover" | "click">>;
10
- onToggle: () => void;
11
- disabled?: Readonly<Ref<boolean>>;
12
- /**
13
- * Whether the menu button opens to the top or bottom. Defines the keyboard navigation behavior (e.g. Arrow up and down).
14
- *
15
- * @default "bottom"
16
- */
17
- position?: MaybeRef<"top" | "bottom">;
18
- };
19
-
20
- /**
21
- * Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
22
- */
23
- export const createMenuButton = createBuilder((options: CreateMenuButtonOptions) => {
24
- const rootId = useId();
25
- const menuId = useId();
26
- const rootRef = createElRef<HTMLElement>();
27
- const menuRef = createElRef<HTMLElement>();
28
- const buttonId = useId();
29
-
30
- const position = computed(() => toValue(options.position) ?? "bottom");
31
-
32
- useGlobalEventListener({
33
- type: "keydown",
34
- listener: (e) => e.key === "Escape" && setExpanded(false),
35
- disabled: computed(() => !options.isExpanded.value),
36
- });
37
-
38
- /**
39
- * Debounced expanded state that will only be toggled after a given timeout.
40
- */
41
- const updateDebouncedExpanded = debounce(() => options.onToggle(), 200);
42
- watch(options.isExpanded, () => updateDebouncedExpanded.abort()); // manually changing `isExpanded` should abort debounced action
43
-
44
- const setExpanded = (expanded: boolean, debounced = false) => {
45
- if (options.disabled?.value) return;
46
- if (expanded === options.isExpanded.value) {
47
- updateDebouncedExpanded.abort();
48
- return;
49
- }
50
- if (debounced) {
51
- updateDebouncedExpanded();
52
- return;
53
- }
54
- options.onToggle();
55
- };
56
-
57
- const focusRelativeItem = (next: "next" | "prev" | "first" | "last") => {
58
- const currentMenuItem = document.activeElement as HTMLElement;
59
-
60
- // Either the current focus is on a "menuitem", then we can just get the parent menu.
61
- // Or the current focus is on the button, then we can get the connected menu using the menuId
62
- const currentMenu = currentMenuItem?.closest('[role="menu"]') || menuRef.value;
63
- if (!currentMenu) return;
64
-
65
- const menuItems = Array.from(currentMenu.querySelectorAll<HTMLElement>('[role="menuitem"]'))
66
- // filter out nested children
67
- .filter((item) => item.closest('[role="menu"]') === currentMenu);
68
- if (position.value === "top") menuItems.reverse();
69
- let nextIndex = 0;
70
-
71
- if (currentMenuItem) {
72
- const currentIndex = menuItems.indexOf(currentMenuItem);
73
- switch (next) {
74
- case "next":
75
- nextIndex = currentIndex + 1;
76
- break;
77
- case "prev":
78
- nextIndex = currentIndex - 1;
79
- break;
80
- case "first":
81
- nextIndex = 0;
82
- break;
83
- case "last":
84
- nextIndex = menuItems.length - 1;
85
- break;
86
- }
87
- }
88
-
89
- const nextMenuItem = menuItems[nextIndex];
90
- nextMenuItem?.focus();
91
- };
92
-
93
- const handleKeydown = (event: KeyboardEvent) => {
94
- switch (event.key) {
95
- case "ArrowDown":
96
- event.preventDefault();
97
- focusRelativeItem(position.value === "bottom" ? "next" : "prev");
98
- break;
99
- case "ArrowUp":
100
- event.preventDefault();
101
- focusRelativeItem(position.value === "bottom" ? "prev" : "next");
102
- break;
103
- case "Home":
104
- event.preventDefault();
105
- focusRelativeItem("first");
106
- break;
107
- case "End":
108
- event.preventDefault();
109
- focusRelativeItem("last");
110
- break;
111
- case " ":
112
- case "Enter":
113
- if (event.target instanceof HTMLInputElement) break;
114
- event.preventDefault();
115
- (event.target as HTMLElement).click();
116
- break;
117
- case "Escape":
118
- event.preventDefault();
119
- setExpanded(false);
120
- break;
121
- }
122
- };
123
-
124
- const triggerEvents = computed(() => {
125
- if (toValue(options.trigger) !== "hover") return;
126
- return {
127
- onMouseenter: () => setExpanded(true),
128
- onMouseleave: () => setExpanded(false, true),
129
- };
130
- });
131
-
132
- useOutsideClick({
133
- inside: rootRef,
134
- onOutsideClick: () => setExpanded(false),
135
- disabled: computed(() => !options.isExpanded.value),
136
- checkOnTab: true,
137
- });
138
-
139
- return {
140
- elements: {
141
- root: computed(() => ({
142
- id: rootId,
143
- onKeydown: handleKeydown,
144
- ref: rootRef,
145
- ...triggerEvents.value,
146
- })),
147
- button: computed(
148
- () =>
149
- ({
150
- "aria-controls": menuId,
151
- "aria-expanded": options.isExpanded.value,
152
- "aria-haspopup": true,
153
- onFocus: () => setExpanded(true, true),
154
- onClick: () =>
155
- toValue(options.trigger) == "click"
156
- ? setExpanded(!options.isExpanded.value)
157
- : undefined,
158
- id: buttonId,
159
- disabled: options.disabled?.value,
160
- }) as const,
161
- ),
162
- menu: {
163
- id: menuId,
164
- ref: menuRef,
165
- role: "menu",
166
- "aria-labelledby": buttonId,
167
- onClick: () => setExpanded(false),
168
- },
169
- ...createMenuItems().elements,
170
- },
171
- };
172
- });
173
-
174
- type CreateMenuItemOptions = {
175
- /**
176
- * Called when the menu item should be opened (if it has nested children).
177
- */
178
- onOpen?: () => void;
179
- };
180
-
181
- export const createMenuItems = createBuilder((options?: CreateMenuItemOptions) => {
182
- const onKeydown = (event: KeyboardEvent) => {
183
- switch (event.key) {
184
- case "ArrowRight":
185
- case " ":
186
- case "Enter":
187
- event.preventDefault();
188
- options?.onOpen?.();
189
- break;
190
- }
191
- };
192
-
193
- return {
194
- elements: {
195
- listItem: {
196
- role: "none",
197
- },
198
- menuItem: (data: { active?: boolean; disabled?: boolean }) => ({
199
- "aria-current": data.active ? "page" : undefined,
200
- "aria-disabled": data.disabled,
201
- role: "menuitem",
202
- onKeydown,
203
- }),
204
- },
205
- };
206
- });
@@ -1,12 +0,0 @@
1
- import { test } from "@playwright/experimental-ct-vue";
2
- import { navigationTesting } from "./createMenu.testing.js";
3
- import TestMenu from "./TestMenu.vue";
4
-
5
- test("navigationMenu", async ({ mount, page }) => {
6
- await mount(<TestMenu />);
7
-
8
- await navigationTesting({
9
- buttons: page.getByRole("button"),
10
- nav: page.getByRole("navigation"),
11
- });
12
- });
@@ -1,16 +0,0 @@
1
- <script lang="ts" setup>
2
- import TestMenuButton from "../menuButton/TestMenuButton.vue";
3
- import { createNavigationMenu } from "./createMenu.js";
4
-
5
- const {
6
- elements: { nav },
7
- } = createNavigationMenu({ navigationName: "test menu" });
8
- </script>
9
-
10
- <template>
11
- <nav v-bind="nav">
12
- <TestMenuButton />
13
- <TestMenuButton />
14
- <TestMenuButton />
15
- </nav>
16
- </template>
@@ -1,37 +0,0 @@
1
- import { expect } from "@playwright/experimental-ct-vue";
2
- import type { Locator } from "@playwright/test";
3
-
4
- export type NavigationMenuTestingOptions = {
5
- /**
6
- * Locator for the navigation landmark.
7
- */
8
- nav: Locator;
9
- /**
10
- * Locator for the button elements.
11
- */
12
- buttons: Locator;
13
- };
14
-
15
- /**
16
- * Playwright utility for executing accessibility testing for a navigation menu.
17
- * Will check aria attributes and keyboard shortcuts as defined in https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
18
- */
19
- export const navigationTesting = async ({ nav, buttons }: NavigationMenuTestingOptions) => {
20
- /**
21
- * Navigation landmark should have label
22
- */
23
- await expect(nav).toHaveRole("navigation");
24
- await expect(nav).toHaveAttribute("aria-label");
25
-
26
- /**
27
- * Focus first button
28
- */
29
- await buttons.first().focus();
30
- /**
31
- * Move keyboard focus among top-level buttons using arrow keys
32
- */
33
- await nav.press("ArrowRight");
34
- await expect(buttons.nth(1)).toBeFocused();
35
- await nav.press("ArrowLeft");
36
- await expect(buttons.nth(0)).toBeFocused();
37
- };
@@ -1,55 +0,0 @@
1
- import { unref, useId, type MaybeRef } from "vue";
2
- import { createBuilder } from "../../utils/builder.js";
3
- import { MathUtils } from "../../utils/math.js";
4
-
5
- type CreateNavigationMenu = {
6
- /**
7
- * Name of the navigation landmark.
8
- * Usually this is the name of the website.
9
- */
10
- navigationName?: MaybeRef<string | undefined>;
11
- };
12
-
13
- /**
14
- * Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
15
- */
16
- export const createNavigationMenu = createBuilder(({ navigationName }: CreateNavigationMenu) => {
17
- const navId = useId();
18
-
19
- const getMenuButtons = () => {
20
- const nav = navId ? document.getElementById(navId) : undefined;
21
- if (!nav) return [];
22
- return Array.from(nav.querySelectorAll<HTMLElement>("button[aria-expanded][aria-controls]"));
23
- };
24
-
25
- const focusRelative = (trigger: HTMLElement, next: "next" | "previous") => {
26
- const menuButtons = getMenuButtons();
27
- const index = menuButtons.indexOf(trigger);
28
- if (index === -1) return;
29
- const nextIndex = MathUtils.clamp(
30
- index + (next === "next" ? 1 : -1),
31
- 0,
32
- menuButtons.length - 1,
33
- );
34
- menuButtons[nextIndex].focus();
35
- };
36
-
37
- return {
38
- elements: {
39
- nav: {
40
- "aria-label": unref(navigationName),
41
- id: navId,
42
- onKeydown: (event) => {
43
- switch (event.key) {
44
- case "ArrowRight":
45
- focusRelative(event.target as HTMLElement, "next");
46
- break;
47
- case "ArrowLeft":
48
- focusRelative(event.target as HTMLElement, "previous");
49
- break;
50
- }
51
- },
52
- },
53
- },
54
- };
55
- });