@sit-onyx/headless 1.0.0-beta.20 → 1.0.0-beta.22
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/composables/comboBox/SelectOnlyCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/TestCombobox.ct.d.ts +1 -0
- package/dist/composables/comboBox/TestCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/createComboBox.d.ts +370 -0
- package/dist/composables/comboBox/createComboBox.testing.d.ts +10 -0
- package/dist/composables/helpers/useDismissible.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.spec.d.ts +1 -0
- package/dist/composables/helpers/useOutsideClick.d.ts +26 -0
- package/dist/composables/helpers/useOutsideClick.spec.d.ts +1 -0
- package/dist/composables/helpers/useTypeAhead.d.ts +11 -0
- package/dist/composables/helpers/useTypeAhead.spec.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.ct.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.d.vue.ts +2 -0
- package/dist/composables/listbox/createListbox.d.ts +102 -0
- package/dist/composables/listbox/createListbox.testing.d.ts +24 -0
- package/dist/composables/menuButton/TestMenuButton.ct.d.ts +1 -0
- package/dist/composables/menuButton/TestMenuButton.d.vue.ts +2 -0
- package/dist/composables/menuButton/createMenuButton.d.ts +78 -0
- package/dist/composables/menuButton/createMenuButton.testing.d.ts +24 -0
- package/dist/composables/navigationMenu/TestMenu.ct.d.ts +1 -0
- package/dist/composables/navigationMenu/TestMenu.d.vue.ts +2 -0
- package/dist/composables/navigationMenu/createMenu.d.ts +21 -0
- package/dist/composables/navigationMenu/createMenu.testing.d.ts +16 -0
- package/dist/composables/tabs/TestTabs.ct.d.ts +1 -0
- package/dist/composables/tabs/TestTabs.d.vue.ts +2 -0
- package/dist/composables/tabs/createTabs.d.ts +48 -0
- package/dist/composables/tabs/createTabs.testing.d.ts +13 -0
- package/dist/composables/tooltip/createToggletip.d.ts +36 -0
- package/dist/composables/tooltip/createTooltip.d.ts +42 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1088 -0
- package/dist/playwright.d.ts +5 -0
- package/dist/playwright.js +369 -0
- package/dist/utils/builder.d.ts +85 -0
- package/dist/utils/keyboard.d.ts +26 -0
- package/dist/utils/keyboard.spec.d.ts +1 -0
- package/dist/utils/math.d.ts +6 -0
- package/dist/utils/math.spec.d.ts +1 -0
- package/dist/utils/object.d.ts +5 -0
- package/dist/utils/object.spec.d.ts +1 -0
- package/dist/utils/timer.d.ts +10 -0
- package/{src/utils/types.ts → dist/utils/types.d.ts} +4 -12
- package/dist/utils/vitest.d.ts +12 -0
- package/package.json +20 -8
- package/src/composables/comboBox/SelectOnlyCombobox.vue +0 -90
- package/src/composables/comboBox/TestCombobox.ct.tsx +0 -24
- package/src/composables/comboBox/TestCombobox.vue +0 -84
- package/src/composables/comboBox/createComboBox.testing.ts +0 -168
- package/src/composables/comboBox/createComboBox.ts +0 -280
- package/src/composables/helpers/useDismissible.ts +0 -19
- package/src/composables/helpers/useGlobalListener.spec.ts +0 -93
- package/src/composables/helpers/useGlobalListener.ts +0 -64
- package/src/composables/helpers/useOutsideClick.spec.ts +0 -117
- package/src/composables/helpers/useOutsideClick.ts +0 -69
- package/src/composables/helpers/useTypeAhead.spec.ts +0 -29
- package/src/composables/helpers/useTypeAhead.ts +0 -26
- package/src/composables/listbox/TestListbox.ct.tsx +0 -17
- package/src/composables/listbox/TestListbox.vue +0 -92
- package/src/composables/listbox/createListbox.testing.ts +0 -141
- package/src/composables/listbox/createListbox.ts +0 -234
- package/src/composables/menuButton/TestMenuButton.ct.tsx +0 -14
- package/src/composables/menuButton/TestMenuButton.vue +0 -29
- package/src/composables/menuButton/createMenuButton.testing.ts +0 -91
- package/src/composables/menuButton/createMenuButton.ts +0 -206
- package/src/composables/navigationMenu/TestMenu.ct.tsx +0 -12
- package/src/composables/navigationMenu/TestMenu.vue +0 -16
- package/src/composables/navigationMenu/createMenu.testing.ts +0 -37
- package/src/composables/navigationMenu/createMenu.ts +0 -55
- package/src/composables/tabs/TestTabs.ct.tsx +0 -12
- package/src/composables/tabs/TestTabs.vue +0 -28
- package/src/composables/tabs/createTabs.testing.ts +0 -151
- package/src/composables/tabs/createTabs.ts +0 -129
- package/src/composables/tooltip/createToggletip.ts +0 -58
- package/src/composables/tooltip/createTooltip.ts +0 -71
- package/src/index.ts +0 -11
- package/src/playwright.ts +0 -5
- package/src/utils/builder.ts +0 -135
- package/src/utils/keyboard.spec.ts +0 -53
- package/src/utils/keyboard.ts +0 -351
- package/src/utils/math.spec.ts +0 -14
- package/src/utils/math.ts +0 -6
- package/src/utils/object.spec.ts +0 -33
- package/src/utils/object.ts +0 -8
- package/src/utils/timer.ts +0 -22
- package/src/utils/vitest.ts +0 -36
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useTemplateRef } from "vue";
|
|
3
|
-
import { createComboBox } from "./createComboBox";
|
|
4
|
-
|
|
5
|
-
const options = ["a", "b", "c", "d"];
|
|
6
|
-
const isExpanded = ref(false);
|
|
7
|
-
const comboboxRef = useTemplateRef("combobox");
|
|
8
|
-
const activeOption = ref("");
|
|
9
|
-
const selectedOption = ref("");
|
|
10
|
-
const selectedIndex = computed<number | undefined>(() => {
|
|
11
|
-
const index = options.indexOf(activeOption.value);
|
|
12
|
-
return index !== -1 ? index : undefined;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const onActivateFirst = () => (activeOption.value = options[0]);
|
|
16
|
-
const onActivateLast = () => (activeOption.value = options[options.length - 1]);
|
|
17
|
-
const onActivateNext = () => {
|
|
18
|
-
if (selectedIndex.value === undefined) {
|
|
19
|
-
return onActivateFirst();
|
|
20
|
-
}
|
|
21
|
-
activeOption.value = options[selectedIndex.value + (1 % (options.length - 1))];
|
|
22
|
-
};
|
|
23
|
-
const onActivatePrevious = () => (activeOption.value = options[(selectedIndex.value ?? 0) - 1]);
|
|
24
|
-
const onSelect = (newValue: string) => (selectedOption.value = newValue);
|
|
25
|
-
const onToggle = () => (isExpanded.value = !isExpanded.value);
|
|
26
|
-
const onTypeAhead = () => {};
|
|
27
|
-
|
|
28
|
-
const comboBox = createComboBox({
|
|
29
|
-
autocomplete: "none",
|
|
30
|
-
label: "some label",
|
|
31
|
-
listLabel: "List",
|
|
32
|
-
activeOption,
|
|
33
|
-
isExpanded,
|
|
34
|
-
templateRef: comboboxRef,
|
|
35
|
-
onToggle,
|
|
36
|
-
onTypeAhead,
|
|
37
|
-
onActivateFirst,
|
|
38
|
-
onActivateLast,
|
|
39
|
-
onActivateNext,
|
|
40
|
-
onActivatePrevious,
|
|
41
|
-
onSelect,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const {
|
|
45
|
-
elements: { input, listbox, button, option },
|
|
46
|
-
} = comboBox;
|
|
47
|
-
|
|
48
|
-
defineExpose({ comboBox });
|
|
49
|
-
</script>
|
|
50
|
-
|
|
51
|
-
<template>
|
|
52
|
-
<div ref="combobox">
|
|
53
|
-
<input
|
|
54
|
-
v-bind="input"
|
|
55
|
-
v-model="selectedOption"
|
|
56
|
-
readonly
|
|
57
|
-
@keydown.arrow-down="isExpanded = true"
|
|
58
|
-
/>
|
|
59
|
-
|
|
60
|
-
<button v-bind="button" type="button">
|
|
61
|
-
<template v-if="isExpanded">⬆️</template>
|
|
62
|
-
<template v-else>⬇️</template>
|
|
63
|
-
</button>
|
|
64
|
-
<ul class="listbox" v-bind="listbox" :class="{ hidden: !isExpanded }">
|
|
65
|
-
<li
|
|
66
|
-
v-for="e in options"
|
|
67
|
-
:key="e"
|
|
68
|
-
v-bind="option({ value: e, label: e, disabled: false, selected: e === selectedOption })"
|
|
69
|
-
:class="{ active: e === activeOption }"
|
|
70
|
-
>
|
|
71
|
-
{{ e }}
|
|
72
|
-
</li>
|
|
73
|
-
</ul>
|
|
74
|
-
</div>
|
|
75
|
-
</template>
|
|
76
|
-
|
|
77
|
-
<style>
|
|
78
|
-
.hidden {
|
|
79
|
-
display: none;
|
|
80
|
-
}
|
|
81
|
-
.active {
|
|
82
|
-
outline: 2px solid black;
|
|
83
|
-
}
|
|
84
|
-
[aria-selected="true"] {
|
|
85
|
-
background-color: red;
|
|
86
|
-
}
|
|
87
|
-
.listbox {
|
|
88
|
-
width: 400px;
|
|
89
|
-
}
|
|
90
|
-
</style>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from "@playwright/experimental-ct-vue";
|
|
2
|
-
import { comboboxSelectOnlyTesting, comboboxTesting } from "./createComboBox.testing";
|
|
3
|
-
import SelectOnlyCombobox from "./SelectOnlyCombobox.vue";
|
|
4
|
-
import TestCombobox from "./TestCombobox.vue";
|
|
5
|
-
|
|
6
|
-
test("combobox", async ({ mount, page }) => {
|
|
7
|
-
await mount(<TestCombobox />);
|
|
8
|
-
const listbox = page.getByRole("listbox");
|
|
9
|
-
const combobox = page.getByRole("combobox");
|
|
10
|
-
const button = page.getByRole("button");
|
|
11
|
-
const options = page.getByRole("option");
|
|
12
|
-
|
|
13
|
-
await comboboxTesting(page, listbox, combobox, button, options);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("select only combobox", async ({ mount, page }) => {
|
|
17
|
-
await mount(<SelectOnlyCombobox />);
|
|
18
|
-
const listbox = page.getByRole("listbox");
|
|
19
|
-
const combobox = page.getByRole("combobox");
|
|
20
|
-
|
|
21
|
-
await comboboxSelectOnlyTesting(page, listbox, combobox, (loc) =>
|
|
22
|
-
loc.evaluate((e) => e.classList.contains("active")),
|
|
23
|
-
);
|
|
24
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useTemplateRef } from "vue";
|
|
3
|
-
import { createComboBox } from "./createComboBox";
|
|
4
|
-
|
|
5
|
-
const options = ["a", "b", "c", "d"];
|
|
6
|
-
const isExpanded = ref(false);
|
|
7
|
-
const searchTerm = ref("");
|
|
8
|
-
const comboboxRef = useTemplateRef("combobox");
|
|
9
|
-
const activeOption = ref("");
|
|
10
|
-
const filteredOptions = computed(() => options.filter((v) => v.includes(searchTerm.value)));
|
|
11
|
-
const selectedIndex = computed<number | undefined>(() => {
|
|
12
|
-
const index = filteredOptions.value.indexOf(activeOption.value);
|
|
13
|
-
return index !== -1 ? index : undefined;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const onActivateFirst = () => (activeOption.value = filteredOptions.value[0]);
|
|
17
|
-
const onActivateLast = () =>
|
|
18
|
-
(activeOption.value = filteredOptions.value[filteredOptions.value.length - 1]);
|
|
19
|
-
const onActivateNext = () => {
|
|
20
|
-
if (selectedIndex.value === undefined) {
|
|
21
|
-
return onActivateFirst();
|
|
22
|
-
}
|
|
23
|
-
activeOption.value =
|
|
24
|
-
filteredOptions.value[selectedIndex.value + (1 % (filteredOptions.value.length - 1))];
|
|
25
|
-
};
|
|
26
|
-
const onActivatePrevious = () =>
|
|
27
|
-
(activeOption.value = filteredOptions.value[(selectedIndex.value ?? 0) - 1]);
|
|
28
|
-
const onSelect = (newValue: string) => (searchTerm.value = newValue);
|
|
29
|
-
const onAutocomplete = (input: string) => (searchTerm.value = input);
|
|
30
|
-
const onToggle = () => (isExpanded.value = !isExpanded.value);
|
|
31
|
-
|
|
32
|
-
const comboBox = createComboBox({
|
|
33
|
-
autocomplete: "list",
|
|
34
|
-
label: "some label",
|
|
35
|
-
listLabel: "List",
|
|
36
|
-
activeOption,
|
|
37
|
-
isExpanded,
|
|
38
|
-
templateRef: comboboxRef,
|
|
39
|
-
onToggle,
|
|
40
|
-
onAutocomplete,
|
|
41
|
-
onActivateFirst,
|
|
42
|
-
onActivateLast,
|
|
43
|
-
onActivateNext,
|
|
44
|
-
onActivatePrevious,
|
|
45
|
-
onSelect,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const {
|
|
49
|
-
elements: { input, listbox, button, option },
|
|
50
|
-
} = comboBox;
|
|
51
|
-
|
|
52
|
-
defineExpose({ comboBox });
|
|
53
|
-
</script>
|
|
54
|
-
|
|
55
|
-
<template>
|
|
56
|
-
<div ref="combobox">
|
|
57
|
-
<input v-bind="input" v-model="searchTerm" @keydown.arrow-down="isExpanded = true" />
|
|
58
|
-
|
|
59
|
-
<button v-bind="button" type="button">
|
|
60
|
-
<template v-if="isExpanded"> ⬆️ </template>
|
|
61
|
-
<template v-else> ⬇️ </template>
|
|
62
|
-
</button>
|
|
63
|
-
<ul v-bind="listbox" :class="{ list: true, hidden: !isExpanded }">
|
|
64
|
-
<li
|
|
65
|
-
v-for="e in filteredOptions"
|
|
66
|
-
:key="e"
|
|
67
|
-
v-bind="option({ value: e, label: e, disabled: false })"
|
|
68
|
-
:style="{ 'background-color': e === activeOption ? 'red' : undefined }"
|
|
69
|
-
>
|
|
70
|
-
{{ e }}
|
|
71
|
-
</li>
|
|
72
|
-
</ul>
|
|
73
|
-
</div>
|
|
74
|
-
</template>
|
|
75
|
-
|
|
76
|
-
<style>
|
|
77
|
-
.list {
|
|
78
|
-
width: 400px;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.hidden {
|
|
82
|
-
display: none;
|
|
83
|
-
}
|
|
84
|
-
</style>
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "@playwright/experimental-ct-vue";
|
|
2
|
-
import type { Locator, Page } from "@playwright/test";
|
|
3
|
-
|
|
4
|
-
const expectToOpen = async (
|
|
5
|
-
keyCombo: string,
|
|
6
|
-
combobox: Locator,
|
|
7
|
-
listbox: Locator,
|
|
8
|
-
checkActive?: () => Promise<boolean>,
|
|
9
|
-
) => {
|
|
10
|
-
await closeCombobox(combobox, listbox);
|
|
11
|
-
await combobox.press(keyCombo);
|
|
12
|
-
await expect(listbox, `Listbox should be opened after pressing ${keyCombo}.`).toBeVisible();
|
|
13
|
-
if (checkActive) {
|
|
14
|
-
const active = await checkActive();
|
|
15
|
-
expect(active, "Given option should be active").toBeTruthy();
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const expectToClose = async (
|
|
20
|
-
keyCombo: string,
|
|
21
|
-
combobox: Locator,
|
|
22
|
-
listbox: Locator,
|
|
23
|
-
selectedLocator?: () => Locator,
|
|
24
|
-
) => {
|
|
25
|
-
await openCombobox(combobox, listbox);
|
|
26
|
-
await combobox.press(keyCombo);
|
|
27
|
-
await expect(listbox, `Listbox should be closed after pressing ${keyCombo}.`).toBeHidden();
|
|
28
|
-
await expect(combobox).toBeFocused();
|
|
29
|
-
await openCombobox(combobox, listbox);
|
|
30
|
-
if (selectedLocator) {
|
|
31
|
-
await expectToBeSelected(selectedLocator());
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Test an implementation of the combobox based on https://w3c.github.io/aria/#combobox
|
|
37
|
-
*/
|
|
38
|
-
export const comboboxTesting = async (
|
|
39
|
-
_page: Page,
|
|
40
|
-
listbox: Locator,
|
|
41
|
-
combobox: Locator,
|
|
42
|
-
button: Locator,
|
|
43
|
-
options: Locator,
|
|
44
|
-
) => {
|
|
45
|
-
await expect(listbox, "Typically, the initial state of a combobox is collapsed.").toBeHidden();
|
|
46
|
-
|
|
47
|
-
await expect(combobox, "In the collapsed state, the combobox element is visible.").toBeVisible();
|
|
48
|
-
await expect(
|
|
49
|
-
button,
|
|
50
|
-
"In the collapsed state, the optional button element is visible.",
|
|
51
|
-
).toBeVisible();
|
|
52
|
-
|
|
53
|
-
await button.click(); // toggle to be expanded
|
|
54
|
-
await expect(
|
|
55
|
-
combobox,
|
|
56
|
-
"A combobox is said to be expanded when the combobox element shows its current value",
|
|
57
|
-
).toHaveValue("");
|
|
58
|
-
await expect(
|
|
59
|
-
listbox,
|
|
60
|
-
"A combobox is said to be expanded when the associated popup is visible",
|
|
61
|
-
).toBeVisible();
|
|
62
|
-
await button.click(); // toggle to be closed
|
|
63
|
-
|
|
64
|
-
await expect(
|
|
65
|
-
combobox,
|
|
66
|
-
"Authors MUST set aria-expanded to false when it is collapsed.",
|
|
67
|
-
).toHaveAttribute("aria-expanded", "false");
|
|
68
|
-
await button.click(); // toggle to be expanded
|
|
69
|
-
await expect(
|
|
70
|
-
combobox,
|
|
71
|
-
"Authors MUST set aria-expanded to true when it is expanded.",
|
|
72
|
-
).toHaveAttribute("aria-expanded", "true");
|
|
73
|
-
await button.click(); // toggle to be closed
|
|
74
|
-
|
|
75
|
-
await button.focus();
|
|
76
|
-
await expect(button, "authors SHOULD ensure that the button is focusable").toBeFocused();
|
|
77
|
-
await expect(
|
|
78
|
-
button,
|
|
79
|
-
"authors SHOULD ensure that the button is not included in the page Tab sequence",
|
|
80
|
-
).toHaveAttribute("tabindex", "-1");
|
|
81
|
-
await expect(
|
|
82
|
-
combobox.getByRole("button"),
|
|
83
|
-
"authors SHOULD ensure that the button is not a descendant of the element with role combobox",
|
|
84
|
-
).toHaveCount(0);
|
|
85
|
-
|
|
86
|
-
const firstElement = options.first();
|
|
87
|
-
|
|
88
|
-
// open and select first option
|
|
89
|
-
await combobox.focus();
|
|
90
|
-
expectToOpen("ArrowDown", combobox, listbox);
|
|
91
|
-
|
|
92
|
-
const firstId = await (await firstElement.elementHandle())!.getAttribute("id");
|
|
93
|
-
expect(typeof firstId).toBe("string");
|
|
94
|
-
await expect(
|
|
95
|
-
combobox,
|
|
96
|
-
"When a descendant of the popup element is active, authors MAY set aria-activedescendant on the combobox to a value that refers to the active element within the popup.",
|
|
97
|
-
).toHaveAttribute("aria-activedescendant", firstId as string);
|
|
98
|
-
await expect(
|
|
99
|
-
combobox,
|
|
100
|
-
"When a descendant of the popup element is active, authors MAY ensure that the focus remains on the combobox element",
|
|
101
|
-
).toBeFocused();
|
|
102
|
-
|
|
103
|
-
// Single Select Pattern
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const closeCombobox = async (combobox: Locator, listbox: Locator) => {
|
|
107
|
-
await combobox.press("Escape");
|
|
108
|
-
return expect(listbox, "Listbox should be collapsed again").toBeHidden();
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const openCombobox = async (combobox: Locator, listbox: Locator) => {
|
|
112
|
-
await combobox.press("Home");
|
|
113
|
-
return expect(listbox, "Listbox should be open again").toBeVisible();
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const expectToBeSelected = async (selectedItem: Locator) =>
|
|
117
|
-
expect(selectedItem, "Option should be selected").toHaveAttribute("aria-selected", "true");
|
|
118
|
-
|
|
119
|
-
export type CheckLocator = (option: Locator) => Promise<boolean>;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Test an implementation of the combobox based on https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
|
|
123
|
-
*/
|
|
124
|
-
export const comboboxSelectOnlyTesting = async (
|
|
125
|
-
page: Page,
|
|
126
|
-
listbox: Locator,
|
|
127
|
-
combobox: Locator,
|
|
128
|
-
isActive: CheckLocator,
|
|
129
|
-
) => {
|
|
130
|
-
await expect(listbox, "Initial state of a combobox is collapsed.").toBeHidden();
|
|
131
|
-
|
|
132
|
-
await combobox.focus();
|
|
133
|
-
|
|
134
|
-
await test.step("Test opening keys", async () => {
|
|
135
|
-
await expectToOpen("ArrowUp", combobox, listbox, () =>
|
|
136
|
-
isActive(page.getByRole("option").first()),
|
|
137
|
-
);
|
|
138
|
-
await expectToOpen("Alt+ArrowDown", combobox, listbox);
|
|
139
|
-
await expectToOpen("Space", combobox, listbox);
|
|
140
|
-
await expectToOpen("Enter", combobox, listbox);
|
|
141
|
-
await expectToOpen("Home", combobox, listbox, () => isActive(page.getByRole("option").first()));
|
|
142
|
-
await expectToOpen("End", combobox, listbox, () => isActive(page.getByRole("option").last()));
|
|
143
|
-
await expectToOpen("ArrowDown", combobox, listbox);
|
|
144
|
-
await expectToOpen("a", combobox, listbox);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
await test.step("Selecting with Enter", async () => {
|
|
148
|
-
await expectToClose("Enter", combobox, listbox, () => page.getByRole("option").first());
|
|
149
|
-
await expectToClose(" ", combobox, listbox, () => page.getByRole("option").first());
|
|
150
|
-
await expectToClose("Escape", combobox, listbox, () => page.getByRole("option").first());
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
await test.step("Activating with End", async () => {
|
|
154
|
-
await openCombobox(combobox, listbox);
|
|
155
|
-
await combobox.press("End");
|
|
156
|
-
const active = await isActive(listbox.getByRole("option").last());
|
|
157
|
-
expect(active, "Given option should be active").toBeTruthy();
|
|
158
|
-
await expect(combobox).toBeFocused();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await test.step("Activating with Home", async () => {
|
|
162
|
-
await openCombobox(combobox, listbox);
|
|
163
|
-
await combobox.press("Home");
|
|
164
|
-
const active = await isActive(listbox.getByRole("option").first());
|
|
165
|
-
expect(active, "Given option should be active").toBeTruthy();
|
|
166
|
-
await expect(combobox).toBeFocused();
|
|
167
|
-
});
|
|
168
|
-
};
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { computed, unref, useId, type MaybeRef, type Ref } from "vue";
|
|
2
|
-
import { createBuilder } from "../../utils/builder";
|
|
3
|
-
import { isPrintableCharacter, wasKeyPressed, type PressedKey } from "../../utils/keyboard";
|
|
4
|
-
import type { Nullable } from "../../utils/types";
|
|
5
|
-
import { useOutsideClick } from "../helpers/useOutsideClick";
|
|
6
|
-
import { useTypeAhead } from "../helpers/useTypeAhead";
|
|
7
|
-
import {
|
|
8
|
-
createListbox,
|
|
9
|
-
type CreateListboxOptions,
|
|
10
|
-
type ListboxValue,
|
|
11
|
-
} from "../listbox/createListbox";
|
|
12
|
-
|
|
13
|
-
/** See https://w3c.github.io/aria/#aria-autocomplete */
|
|
14
|
-
export type ComboboxAutoComplete = "none" | "list" | "both";
|
|
15
|
-
|
|
16
|
-
export const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
|
|
17
|
-
export const CLOSING_KEYS: PressedKey[] = [
|
|
18
|
-
"Escape",
|
|
19
|
-
{ key: "ArrowUp", altKey: true },
|
|
20
|
-
"Enter",
|
|
21
|
-
"Tab",
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const SELECTING_KEYS: PressedKey[] = ["Enter"];
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* if the a search input is included, space should not be used to select
|
|
28
|
-
* TODO: idea for the future: move this distinction to the listbox?
|
|
29
|
-
*/
|
|
30
|
-
const isSelectingKey = (event: KeyboardEvent, withSpace?: boolean) => {
|
|
31
|
-
const selectingKeys = withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS;
|
|
32
|
-
return isKeyOfGroup(event, selectingKeys);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const isKeyOfGroup = (event: KeyboardEvent, group: PressedKey[]) =>
|
|
36
|
-
group.some((key) => wasKeyPressed(event, key));
|
|
37
|
-
|
|
38
|
-
export type CreateComboboxOptions<
|
|
39
|
-
TValue extends ListboxValue,
|
|
40
|
-
TAutoComplete extends ComboboxAutoComplete,
|
|
41
|
-
TMultiple extends boolean = false,
|
|
42
|
-
> = {
|
|
43
|
-
autocomplete: MaybeRef<TAutoComplete>;
|
|
44
|
-
label: MaybeRef<string>;
|
|
45
|
-
/**
|
|
46
|
-
* Labels the listbox which displays the available options. E.g. the list label could be "Countries" for a combobox which is labelled "Country".
|
|
47
|
-
*/
|
|
48
|
-
listLabel: MaybeRef<string>;
|
|
49
|
-
/**
|
|
50
|
-
* Provides additional description for the listbox which displays the available options.
|
|
51
|
-
*/
|
|
52
|
-
listDescription?: MaybeRef<Nullable<string>>;
|
|
53
|
-
/**
|
|
54
|
-
* Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
|
|
55
|
-
*/
|
|
56
|
-
isExpanded: MaybeRef<boolean>;
|
|
57
|
-
/**
|
|
58
|
-
* If expanded, the active option is the currently highlighted option of the controlled listbox.
|
|
59
|
-
*/
|
|
60
|
-
activeOption: Ref<Nullable<TValue>>;
|
|
61
|
-
/**
|
|
62
|
-
* Template ref to the component root (required to close combobox on outside click).
|
|
63
|
-
*/
|
|
64
|
-
templateRef: Ref<Nullable<HTMLElement>>;
|
|
65
|
-
/**
|
|
66
|
-
* Hook when the popover should toggle.
|
|
67
|
-
*
|
|
68
|
-
* @param preventFocus If `true`, the parent combobox should not be focused (e.g. on outside click).
|
|
69
|
-
*/
|
|
70
|
-
onToggle?: (preventFocus?: boolean) => void;
|
|
71
|
-
/**
|
|
72
|
-
* Hook when an option is (un-)selected.
|
|
73
|
-
*/
|
|
74
|
-
onSelect?: (value: TValue) => void;
|
|
75
|
-
/**
|
|
76
|
-
* Hook when the first option should be activated.
|
|
77
|
-
*/
|
|
78
|
-
onActivateFirst?: () => void;
|
|
79
|
-
/**
|
|
80
|
-
* Hook when the last option should be activated.
|
|
81
|
-
*/
|
|
82
|
-
onActivateLast?: () => void;
|
|
83
|
-
/**
|
|
84
|
-
* Hook when the next option should be activated.
|
|
85
|
-
*/
|
|
86
|
-
onActivateNext?: (currentValue: TValue) => void;
|
|
87
|
-
/**
|
|
88
|
-
* Hook when the previous option should be activated.
|
|
89
|
-
*/
|
|
90
|
-
onActivatePrevious?: (currentValue: TValue) => void;
|
|
91
|
-
} & (TAutoComplete extends Exclude<ComboboxAutoComplete, "none">
|
|
92
|
-
? { onAutocomplete: (input: string) => void }
|
|
93
|
-
: { onAutocomplete?: undefined }) &
|
|
94
|
-
(TAutoComplete extends "none"
|
|
95
|
-
? { onTypeAhead: (input: string) => void }
|
|
96
|
-
: { onTypeAhead?: undefined }) &
|
|
97
|
-
Pick<
|
|
98
|
-
CreateListboxOptions<TValue, TMultiple>,
|
|
99
|
-
| "onActivateFirst"
|
|
100
|
-
| "onActivateLast"
|
|
101
|
-
| "onActivateNext"
|
|
102
|
-
| "onActivatePrevious"
|
|
103
|
-
| "onSelect"
|
|
104
|
-
| "multiple"
|
|
105
|
-
>;
|
|
106
|
-
|
|
107
|
-
export const createComboBox = createBuilder(
|
|
108
|
-
<
|
|
109
|
-
TValue extends ListboxValue,
|
|
110
|
-
TAutoComplete extends ComboboxAutoComplete,
|
|
111
|
-
TMultiple extends boolean = false,
|
|
112
|
-
>({
|
|
113
|
-
autocomplete: autocompleteRef,
|
|
114
|
-
onAutocomplete,
|
|
115
|
-
onTypeAhead,
|
|
116
|
-
multiple: multipleRef,
|
|
117
|
-
label,
|
|
118
|
-
listLabel,
|
|
119
|
-
listDescription,
|
|
120
|
-
isExpanded: isExpandedRef,
|
|
121
|
-
activeOption,
|
|
122
|
-
onToggle,
|
|
123
|
-
onSelect,
|
|
124
|
-
onActivateFirst,
|
|
125
|
-
onActivateLast,
|
|
126
|
-
onActivateNext,
|
|
127
|
-
onActivatePrevious,
|
|
128
|
-
templateRef,
|
|
129
|
-
}: CreateComboboxOptions<TValue, TAutoComplete, TMultiple>) => {
|
|
130
|
-
const controlsId = useId();
|
|
131
|
-
|
|
132
|
-
const autocomplete = computed(() => unref(autocompleteRef));
|
|
133
|
-
const isExpanded = computed(() => unref(isExpandedRef));
|
|
134
|
-
const multiple = computed(() => unref(multipleRef));
|
|
135
|
-
|
|
136
|
-
const handleInput = (event: Event) => {
|
|
137
|
-
const inputElement = event.target as HTMLInputElement;
|
|
138
|
-
|
|
139
|
-
if (autocomplete.value !== "none") {
|
|
140
|
-
onAutocomplete?.(inputElement.value);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const typeAhead = useTypeAhead((inputString) => onTypeAhead?.(inputString));
|
|
145
|
-
|
|
146
|
-
const handleSelect = (value: TValue) => {
|
|
147
|
-
onSelect?.(value);
|
|
148
|
-
if (!unref(multiple)) {
|
|
149
|
-
onToggle?.();
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const handleNavigation = (event: KeyboardEvent) => {
|
|
154
|
-
switch (event.key) {
|
|
155
|
-
case "ArrowUp":
|
|
156
|
-
event.preventDefault();
|
|
157
|
-
if (activeOption.value == undefined) {
|
|
158
|
-
return onActivateLast?.();
|
|
159
|
-
}
|
|
160
|
-
onActivatePrevious?.(activeOption.value);
|
|
161
|
-
break;
|
|
162
|
-
case "ArrowDown":
|
|
163
|
-
event.preventDefault();
|
|
164
|
-
if (activeOption.value == undefined) {
|
|
165
|
-
return onActivateFirst?.();
|
|
166
|
-
}
|
|
167
|
-
onActivateNext?.(activeOption.value);
|
|
168
|
-
break;
|
|
169
|
-
case "Home":
|
|
170
|
-
event.preventDefault();
|
|
171
|
-
onActivateFirst?.();
|
|
172
|
-
break;
|
|
173
|
-
case "End":
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
onActivateLast?.();
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const handleKeydown = (event: KeyboardEvent) => {
|
|
181
|
-
if (event.key === "Enter") {
|
|
182
|
-
// prevent submitting on pressing enter when the combo box is used inside a <form>
|
|
183
|
-
event.preventDefault();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
|
|
187
|
-
onToggle?.();
|
|
188
|
-
if (event.key === " ") {
|
|
189
|
-
event.preventDefault();
|
|
190
|
-
}
|
|
191
|
-
if (event.key === "End") {
|
|
192
|
-
return onActivateLast?.();
|
|
193
|
-
}
|
|
194
|
-
return onActivateFirst?.();
|
|
195
|
-
}
|
|
196
|
-
if (isSelectingKey(event, autocomplete.value === "none")) {
|
|
197
|
-
return handleSelect(activeOption.value!);
|
|
198
|
-
}
|
|
199
|
-
if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) {
|
|
200
|
-
return onToggle?.();
|
|
201
|
-
}
|
|
202
|
-
if (autocomplete.value === "none" && isPrintableCharacter(event.key)) {
|
|
203
|
-
!isExpanded.value && onToggle?.();
|
|
204
|
-
return typeAhead(event);
|
|
205
|
-
}
|
|
206
|
-
if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
|
|
207
|
-
!isExpanded.value && onToggle?.();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
return handleNavigation(event);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const autocompleteInput = computed(() => {
|
|
214
|
-
if (autocomplete.value === "none") return null;
|
|
215
|
-
return {
|
|
216
|
-
"aria-autocomplete": autocomplete.value,
|
|
217
|
-
type: "text",
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const {
|
|
222
|
-
elements: { option, group, listbox },
|
|
223
|
-
internals: { getOptionId },
|
|
224
|
-
} = createListbox({
|
|
225
|
-
label: listLabel,
|
|
226
|
-
description: listDescription,
|
|
227
|
-
multiple,
|
|
228
|
-
controlled: true,
|
|
229
|
-
activeOption,
|
|
230
|
-
isExpanded,
|
|
231
|
-
onSelect: handleSelect,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
useOutsideClick({
|
|
235
|
-
inside: templateRef,
|
|
236
|
-
onOutsideClick() {
|
|
237
|
-
if (!isExpanded.value) return;
|
|
238
|
-
onToggle?.(true);
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
elements: {
|
|
244
|
-
option,
|
|
245
|
-
group,
|
|
246
|
-
/**
|
|
247
|
-
* The listbox associated with the combobox.
|
|
248
|
-
*/
|
|
249
|
-
listbox: computed(() => ({
|
|
250
|
-
...listbox.value,
|
|
251
|
-
id: controlsId,
|
|
252
|
-
// preventDefault to not lose focus of the combobox
|
|
253
|
-
onMousedown: (e) => e.preventDefault(),
|
|
254
|
-
})),
|
|
255
|
-
/**
|
|
256
|
-
* An input that controls another element, that can dynamically pop-up to help the user set the value of the input.
|
|
257
|
-
* The input MAY be either a single-line text field that supports editing and typing or an element that only displays the current value of the combobox.
|
|
258
|
-
*/
|
|
259
|
-
input: computed(() => ({
|
|
260
|
-
role: "combobox",
|
|
261
|
-
"aria-expanded": isExpanded.value,
|
|
262
|
-
"aria-controls": controlsId,
|
|
263
|
-
"aria-label": unref(label),
|
|
264
|
-
"aria-activedescendant":
|
|
265
|
-
activeOption.value != undefined ? getOptionId(activeOption.value) : undefined,
|
|
266
|
-
onInput: handleInput,
|
|
267
|
-
onKeydown: handleKeydown,
|
|
268
|
-
...autocompleteInput.value,
|
|
269
|
-
})),
|
|
270
|
-
/**
|
|
271
|
-
* An optional button to control the visibility of the popup.
|
|
272
|
-
*/
|
|
273
|
-
button: computed(() => ({
|
|
274
|
-
tabindex: "-1",
|
|
275
|
-
onClick: () => onToggle?.(),
|
|
276
|
-
})),
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
},
|
|
280
|
-
);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { computed, type Ref } from "vue";
|
|
2
|
-
import { useGlobalEventListener } from "./useGlobalListener";
|
|
3
|
-
|
|
4
|
-
type UseDismissibleOptions = { isExpanded: Ref<boolean> };
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Composable that sets `isExpanded` to false, when the `Escape` key is pressed.
|
|
8
|
-
* Addresses the "dismissible" aspect of https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html
|
|
9
|
-
*/
|
|
10
|
-
export const useDismissible = ({ isExpanded }: UseDismissibleOptions) =>
|
|
11
|
-
useGlobalEventListener({
|
|
12
|
-
type: "keydown",
|
|
13
|
-
listener: (e) => {
|
|
14
|
-
if (e.key === "Escape") {
|
|
15
|
-
isExpanded.value = false;
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
disabled: computed(() => !isExpanded.value),
|
|
19
|
-
});
|