@sit-onyx/headless 1.0.0-alpha.8 → 1.0.0-beta.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 +3 -5
- package/package.json +1 -1
- package/src/composables/comboBox/TestCombobox.ct.tsx +2 -2
- package/src/composables/comboBox/createComboBox.ct.ts +1 -1
- package/src/composables/comboBox/createComboBox.ts +24 -11
- package/src/composables/listbox/createListbox.ts +13 -4
- package/src/composables/menuButton/TestMenuButton.ct.tsx +14 -0
- package/src/composables/menuButton/TestMenuButton.vue +27 -0
- package/src/composables/menuButton/createMenuButton.ct.ts +109 -0
- package/src/composables/menuButton/createMenuButton.ts +133 -0
- package/src/composables/outsideClick.ts +1 -1
- package/src/index.ts +1 -0
- package/src/playwright.ts +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<div align="center">
|
|
1
|
+
<div align="center" style="text-align: center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-light.svg">
|
|
4
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg">
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-light.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" type="image/svg+xml" srcset="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg">
|
|
5
5
|
<img alt="onyx logo" src="https://raw.githubusercontent.com/SchwarzIT/onyx/main/.github/onyx-logo-dark.svg" width="160px">
|
|
6
6
|
</picture>
|
|
7
7
|
</div>
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
A composable headless library for Vue created by [Schwarz IT](https://it.schwarz).
|
|
14
14
|
|
|
15
|
-
> **Work in progress**: This library is currently in early / active development.
|
|
16
|
-
|
|
17
15
|
<br />
|
|
18
16
|
|
|
19
17
|
## Documentation
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from "@playwright/experimental-ct-vue";
|
|
2
|
-
import
|
|
3
|
-
import { comboboxTesting, comboboxSelectOnlyTesting } from "./createComboBox.ct";
|
|
2
|
+
import { comboboxSelectOnlyTesting, comboboxTesting } from "./createComboBox.ct";
|
|
4
3
|
import SelectOnlyCombobox from "./SelectOnlyCombobox.vue";
|
|
4
|
+
import TestCombobox from "./TestCombobox.vue";
|
|
5
5
|
|
|
6
6
|
test("combobox", async ({ mount, page }) => {
|
|
7
7
|
await mount(<TestCombobox />);
|
|
@@ -36,7 +36,7 @@ const expectToClose = async (
|
|
|
36
36
|
* Test an implementation of the combobox based on https://w3c.github.io/aria/#combobox
|
|
37
37
|
*/
|
|
38
38
|
export const comboboxTesting = async (
|
|
39
|
-
|
|
39
|
+
_page: Page,
|
|
40
40
|
listbox: Locator,
|
|
41
41
|
combobox: Locator,
|
|
42
42
|
button: Locator,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, unref, type MaybeRef, type Ref } from "vue";
|
|
2
2
|
import { createBuilder } from "../../utils/builder";
|
|
3
3
|
import { createId } from "../../utils/id";
|
|
4
4
|
import { isPrintableCharacter, wasKeyPressed, type PressedKey } from "../../utils/keyboard";
|
|
@@ -12,8 +12,13 @@ import { useTypeAhead } from "../typeAhead";
|
|
|
12
12
|
|
|
13
13
|
export type ComboboxAutoComplete = "none" | "list" | "both";
|
|
14
14
|
|
|
15
|
-
const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
|
|
16
|
-
const CLOSING_KEYS: PressedKey[] = [
|
|
15
|
+
export const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
|
|
16
|
+
export const CLOSING_KEYS: PressedKey[] = [
|
|
17
|
+
"Escape",
|
|
18
|
+
{ key: "ArrowUp", altKey: true },
|
|
19
|
+
"Enter",
|
|
20
|
+
"Tab",
|
|
21
|
+
];
|
|
17
22
|
const SELECTING_KEYS_SINGLE: PressedKey[] = ["Enter", " "];
|
|
18
23
|
const SELECTING_KEYS_MULTIPLE: PressedKey[] = ["Enter"];
|
|
19
24
|
|
|
@@ -54,8 +59,10 @@ export type CreateComboboxOptions<
|
|
|
54
59
|
templateRef: Ref<HTMLElement | undefined>;
|
|
55
60
|
/**
|
|
56
61
|
* Hook when the popover should toggle.
|
|
62
|
+
*
|
|
63
|
+
* @param preventFocus If `true`, the parent combobox should not be focused (e.g. on outside click).
|
|
57
64
|
*/
|
|
58
|
-
onToggle?: () => void;
|
|
65
|
+
onToggle?: (preventFocus?: boolean) => void;
|
|
59
66
|
/**
|
|
60
67
|
* Hook when an option is (un-)selected.
|
|
61
68
|
*/
|
|
@@ -115,7 +122,6 @@ export const createComboBox = createBuilder(
|
|
|
115
122
|
onActivatePrevious,
|
|
116
123
|
templateRef,
|
|
117
124
|
}: CreateComboboxOptions<TValue, TAutoComplete, TMultiple>) => {
|
|
118
|
-
const inputValid = ref(true);
|
|
119
125
|
const controlsId = createId("comboBox-control");
|
|
120
126
|
|
|
121
127
|
const autocomplete = computed(() => unref(autocompleteRef));
|
|
@@ -124,10 +130,6 @@ export const createComboBox = createBuilder(
|
|
|
124
130
|
|
|
125
131
|
const handleInput = (event: Event) => {
|
|
126
132
|
const inputElement = event.target as HTMLInputElement;
|
|
127
|
-
inputValid.value = inputElement.validity.valid;
|
|
128
|
-
if (!unref(isExpanded)) {
|
|
129
|
-
onToggle?.();
|
|
130
|
-
}
|
|
131
133
|
|
|
132
134
|
if (autocomplete.value !== "none") {
|
|
133
135
|
onAutocomplete?.(inputElement.value);
|
|
@@ -171,6 +173,11 @@ export const createComboBox = createBuilder(
|
|
|
171
173
|
};
|
|
172
174
|
|
|
173
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
|
+
|
|
174
181
|
if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
|
|
175
182
|
onToggle?.();
|
|
176
183
|
if (event.key === " ") {
|
|
@@ -191,6 +198,10 @@ export const createComboBox = createBuilder(
|
|
|
191
198
|
!isExpanded.value && onToggle?.();
|
|
192
199
|
return typeAhead(event);
|
|
193
200
|
}
|
|
201
|
+
if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
|
|
202
|
+
!isExpanded.value && onToggle?.();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
194
205
|
return handleNavigation(event);
|
|
195
206
|
};
|
|
196
207
|
|
|
@@ -217,7 +228,7 @@ export const createComboBox = createBuilder(
|
|
|
217
228
|
queryComponent: () => templateRef.value,
|
|
218
229
|
onOutsideClick() {
|
|
219
230
|
if (!isExpanded.value) return;
|
|
220
|
-
onToggle?.();
|
|
231
|
+
onToggle?.(true);
|
|
221
232
|
},
|
|
222
233
|
});
|
|
223
234
|
|
|
@@ -231,6 +242,8 @@ export const createComboBox = createBuilder(
|
|
|
231
242
|
listbox: computed(() => ({
|
|
232
243
|
...listbox.value,
|
|
233
244
|
id: controlsId,
|
|
245
|
+
// preventDefault to not lose focus of the combobox
|
|
246
|
+
onMousedown: (e) => e.preventDefault(),
|
|
234
247
|
})),
|
|
235
248
|
/**
|
|
236
249
|
* An input that controls another element, that can dynamically pop-up to help the user set the value of the input.
|
|
@@ -253,7 +266,7 @@ export const createComboBox = createBuilder(
|
|
|
253
266
|
*/
|
|
254
267
|
button: computed(() => ({
|
|
255
268
|
tabindex: "-1",
|
|
256
|
-
onClick: onToggle,
|
|
269
|
+
onClick: () => onToggle?.(),
|
|
257
270
|
})),
|
|
258
271
|
},
|
|
259
272
|
};
|
|
@@ -185,15 +185,24 @@ export const createListbox = createBuilder(
|
|
|
185
185
|
});
|
|
186
186
|
}),
|
|
187
187
|
option: computed(() => {
|
|
188
|
-
return (data: {
|
|
189
|
-
|
|
188
|
+
return (data: {
|
|
189
|
+
label: string;
|
|
190
|
+
value: TValue;
|
|
191
|
+
disabled?: boolean;
|
|
192
|
+
selected?: boolean;
|
|
193
|
+
}) => {
|
|
194
|
+
const selected = data.selected ?? false;
|
|
195
|
+
|
|
196
|
+
return {
|
|
190
197
|
id: getOptionId(data.value),
|
|
191
198
|
role: "option",
|
|
192
199
|
"aria-label": data.label,
|
|
193
200
|
"aria-disabled": data.disabled,
|
|
194
|
-
|
|
201
|
+
"aria-checked": isMultiselect.value ? selected : undefined,
|
|
202
|
+
"aria-selected": !isMultiselect.value ? selected : undefined,
|
|
195
203
|
onClick: () => !data.disabled && options.onSelect?.(data.value),
|
|
196
|
-
}
|
|
204
|
+
} as const;
|
|
205
|
+
};
|
|
197
206
|
}),
|
|
198
207
|
},
|
|
199
208
|
state: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { test } from "@playwright/experimental-ct-vue";
|
|
2
|
+
import { menuButtonTesting } from "./createMenuButton.ct";
|
|
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: await page.locator("li").all(),
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { createMenuButton } from "./createMenuButton";
|
|
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
|
+
|
|
12
|
+
const {
|
|
13
|
+
elements: { button, menu, menuItem, listItem, flyout },
|
|
14
|
+
state: { isExpanded },
|
|
15
|
+
} = createMenuButton({});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<button v-bind="button">Toggle nav menu</button>
|
|
20
|
+
<div v-bind="flyout">
|
|
21
|
+
<ul v-show="isExpanded" v-bind="menu">
|
|
22
|
+
<li v-for="item in items" v-bind="listItem" :key="item.value" title="item">
|
|
23
|
+
<a v-bind="menuItem({ active: activeItem === item.value })" href="#">{{ item.label }}</a>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
const menuId = await menu.getAttribute("id");
|
|
34
|
+
expect(menuId).toBeDefined();
|
|
35
|
+
await expect(
|
|
36
|
+
button,
|
|
37
|
+
"navigation menu should have set the list ID to the aria-controls",
|
|
38
|
+
).toHaveAttribute("aria-controls", menuId!);
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
button,
|
|
42
|
+
'navigation menu should have an "aria-haspopup" attribute set to true',
|
|
43
|
+
).toHaveAttribute("aria-haspopup", "true");
|
|
44
|
+
|
|
45
|
+
await expect(button).toBeVisible();
|
|
46
|
+
|
|
47
|
+
// ensure correct navigation menu aria attributes
|
|
48
|
+
await expect(
|
|
49
|
+
button,
|
|
50
|
+
'flyout menu must have an "aria-expanded" attribute set to false',
|
|
51
|
+
).toHaveAttribute("aria-expanded", "false");
|
|
52
|
+
|
|
53
|
+
button.hover();
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
button,
|
|
57
|
+
'flyout menu must have an "aria-expanded" attribute set to true',
|
|
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();
|
|
109
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { createBuilder } from "../../utils/builder";
|
|
3
|
+
import { createId } from "../../utils/id";
|
|
4
|
+
import { debounce } from "../../utils/timer";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
|
|
8
|
+
*/
|
|
9
|
+
export const createMenuButton = createBuilder(() => {
|
|
10
|
+
const menuId = createId("menu");
|
|
11
|
+
const buttonId = createId("menu-button");
|
|
12
|
+
const isExpanded = ref(false);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Debounced expanded state that will only be toggled after a given timeout.
|
|
16
|
+
*/
|
|
17
|
+
const updateDebouncedExpanded = debounce(
|
|
18
|
+
(expanded: boolean) => (isExpanded.value = expanded),
|
|
19
|
+
200,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const hoverEvents = computed(() => {
|
|
23
|
+
return {
|
|
24
|
+
onMouseover: () => updateDebouncedExpanded(true),
|
|
25
|
+
onMouseout: () => updateDebouncedExpanded(false),
|
|
26
|
+
onFocusin: () => (isExpanded.value = true),
|
|
27
|
+
onFocusout: () => (isExpanded.value = false),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
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
|
+
|
|
92
|
+
return {
|
|
93
|
+
state: { isExpanded },
|
|
94
|
+
elements: {
|
|
95
|
+
button: computed(
|
|
96
|
+
() =>
|
|
97
|
+
({
|
|
98
|
+
"aria-controls": menuId,
|
|
99
|
+
"aria-expanded": isExpanded.value,
|
|
100
|
+
"aria-haspopup": true,
|
|
101
|
+
id: buttonId,
|
|
102
|
+
...hoverEvents.value,
|
|
103
|
+
onKeydown: handleKeydown,
|
|
104
|
+
}) as const,
|
|
105
|
+
),
|
|
106
|
+
flyout: {
|
|
107
|
+
...hoverEvents.value,
|
|
108
|
+
},
|
|
109
|
+
menu: {
|
|
110
|
+
id: menuId,
|
|
111
|
+
role: "menu",
|
|
112
|
+
"aria-labelledby": buttonId,
|
|
113
|
+
onKeydown: handleKeydown,
|
|
114
|
+
},
|
|
115
|
+
...createMenuItem({}).elements,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const createMenuItem = createBuilder(() => {
|
|
121
|
+
return {
|
|
122
|
+
elements: {
|
|
123
|
+
listItem: {
|
|
124
|
+
role: "none",
|
|
125
|
+
},
|
|
126
|
+
menuItem: (data: { active?: boolean; disabled?: boolean }) => ({
|
|
127
|
+
"aria-current": data.active ? "page" : undefined,
|
|
128
|
+
"aria-disabled": data.disabled,
|
|
129
|
+
role: "menuitem",
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
@@ -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
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./composables/comboBox/createComboBox";
|
|
2
2
|
export * from "./composables/listbox/createListbox";
|
|
3
|
+
export * from "./composables/menuButton/createMenuButton";
|
|
3
4
|
export * from "./composables/tooltip/createTooltip";
|
|
4
5
|
export { createId } from "./utils/id";
|
|
5
6
|
export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard";
|
package/src/playwright.ts
CHANGED