@sit-onyx/headless 1.0.0-beta.21 → 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 +16 -7
- 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
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './composables/comboBox/createComboBox.testing.js';
|
|
2
|
+
export * from './composables/listbox/createListbox.testing.js';
|
|
3
|
+
export * from './composables/menuButton/createMenuButton.testing.js';
|
|
4
|
+
export * from './composables/navigationMenu/createMenu.testing.js';
|
|
5
|
+
export * from './composables/tabs/createTabs.testing.js';
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/experimental-ct-vue";
|
|
2
|
+
const expectToOpen = async (keyCombo, combobox, listbox, checkActive) => {
|
|
3
|
+
await closeCombobox(combobox, listbox);
|
|
4
|
+
await combobox.press(keyCombo);
|
|
5
|
+
await expect(listbox, `Listbox should be opened after pressing ${keyCombo}.`).toBeVisible();
|
|
6
|
+
if (checkActive) {
|
|
7
|
+
const active = await checkActive();
|
|
8
|
+
expect(active, "Given option should be active").toBeTruthy();
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const expectToClose = async (keyCombo, combobox, listbox, selectedLocator) => {
|
|
12
|
+
await openCombobox(combobox, listbox);
|
|
13
|
+
await combobox.press(keyCombo);
|
|
14
|
+
await expect(listbox, `Listbox should be closed after pressing ${keyCombo}.`).toBeHidden();
|
|
15
|
+
await expect(combobox).toBeFocused();
|
|
16
|
+
await openCombobox(combobox, listbox);
|
|
17
|
+
if (selectedLocator) {
|
|
18
|
+
await expectToBeSelected(selectedLocator());
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const comboboxTesting = async (_page, listbox, combobox, button, options) => {
|
|
22
|
+
await expect(listbox, "Typically, the initial state of a combobox is collapsed.").toBeHidden();
|
|
23
|
+
await expect(combobox, "In the collapsed state, the combobox element is visible.").toBeVisible();
|
|
24
|
+
await expect(
|
|
25
|
+
button,
|
|
26
|
+
"In the collapsed state, the optional button element is visible."
|
|
27
|
+
).toBeVisible();
|
|
28
|
+
await button.click();
|
|
29
|
+
await expect(
|
|
30
|
+
combobox,
|
|
31
|
+
"A combobox is said to be expanded when the combobox element shows its current value"
|
|
32
|
+
).toHaveValue("");
|
|
33
|
+
await expect(
|
|
34
|
+
listbox,
|
|
35
|
+
"A combobox is said to be expanded when the associated popup is visible"
|
|
36
|
+
).toBeVisible();
|
|
37
|
+
await button.click();
|
|
38
|
+
await expect(
|
|
39
|
+
combobox,
|
|
40
|
+
"Authors MUST set aria-expanded to false when it is collapsed."
|
|
41
|
+
).toHaveAttribute("aria-expanded", "false");
|
|
42
|
+
await button.click();
|
|
43
|
+
await expect(
|
|
44
|
+
combobox,
|
|
45
|
+
"Authors MUST set aria-expanded to true when it is expanded."
|
|
46
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
47
|
+
await button.click();
|
|
48
|
+
await button.focus();
|
|
49
|
+
await expect(button, "authors SHOULD ensure that the button is focusable").toBeFocused();
|
|
50
|
+
await expect(
|
|
51
|
+
button,
|
|
52
|
+
"authors SHOULD ensure that the button is not included in the page Tab sequence"
|
|
53
|
+
).toHaveAttribute("tabindex", "-1");
|
|
54
|
+
await expect(
|
|
55
|
+
combobox.getByRole("button"),
|
|
56
|
+
"authors SHOULD ensure that the button is not a descendant of the element with role combobox"
|
|
57
|
+
).toHaveCount(0);
|
|
58
|
+
const firstElement = options.first();
|
|
59
|
+
await combobox.focus();
|
|
60
|
+
expectToOpen("ArrowDown", combobox, listbox);
|
|
61
|
+
const firstId = await (await firstElement.elementHandle()).getAttribute("id");
|
|
62
|
+
expect(typeof firstId).toBe("string");
|
|
63
|
+
await expect(
|
|
64
|
+
combobox,
|
|
65
|
+
"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."
|
|
66
|
+
).toHaveAttribute("aria-activedescendant", firstId);
|
|
67
|
+
await expect(
|
|
68
|
+
combobox,
|
|
69
|
+
"When a descendant of the popup element is active, authors MAY ensure that the focus remains on the combobox element"
|
|
70
|
+
).toBeFocused();
|
|
71
|
+
};
|
|
72
|
+
const closeCombobox = async (combobox, listbox) => {
|
|
73
|
+
await combobox.press("Escape");
|
|
74
|
+
return expect(listbox, "Listbox should be collapsed again").toBeHidden();
|
|
75
|
+
};
|
|
76
|
+
const openCombobox = async (combobox, listbox) => {
|
|
77
|
+
await combobox.press("Home");
|
|
78
|
+
return expect(listbox, "Listbox should be open again").toBeVisible();
|
|
79
|
+
};
|
|
80
|
+
const expectToBeSelected = async (selectedItem) => expect(selectedItem, "Option should be selected").toHaveAttribute("aria-selected", "true");
|
|
81
|
+
const comboboxSelectOnlyTesting = async (page, listbox, combobox, isActive) => {
|
|
82
|
+
await expect(listbox, "Initial state of a combobox is collapsed.").toBeHidden();
|
|
83
|
+
await combobox.focus();
|
|
84
|
+
await test.step("Test opening keys", async () => {
|
|
85
|
+
await expectToOpen(
|
|
86
|
+
"ArrowUp",
|
|
87
|
+
combobox,
|
|
88
|
+
listbox,
|
|
89
|
+
() => isActive(page.getByRole("option").first())
|
|
90
|
+
);
|
|
91
|
+
await expectToOpen("Alt+ArrowDown", combobox, listbox);
|
|
92
|
+
await expectToOpen("Space", combobox, listbox);
|
|
93
|
+
await expectToOpen("Enter", combobox, listbox);
|
|
94
|
+
await expectToOpen("Home", combobox, listbox, () => isActive(page.getByRole("option").first()));
|
|
95
|
+
await expectToOpen("End", combobox, listbox, () => isActive(page.getByRole("option").last()));
|
|
96
|
+
await expectToOpen("ArrowDown", combobox, listbox);
|
|
97
|
+
await expectToOpen("a", combobox, listbox);
|
|
98
|
+
});
|
|
99
|
+
await test.step("Selecting with Enter", async () => {
|
|
100
|
+
await expectToClose("Enter", combobox, listbox, () => page.getByRole("option").first());
|
|
101
|
+
await expectToClose(" ", combobox, listbox, () => page.getByRole("option").first());
|
|
102
|
+
await expectToClose("Escape", combobox, listbox, () => page.getByRole("option").first());
|
|
103
|
+
});
|
|
104
|
+
await test.step("Activating with End", async () => {
|
|
105
|
+
await openCombobox(combobox, listbox);
|
|
106
|
+
await combobox.press("End");
|
|
107
|
+
const active = await isActive(listbox.getByRole("option").last());
|
|
108
|
+
expect(active, "Given option should be active").toBeTruthy();
|
|
109
|
+
await expect(combobox).toBeFocused();
|
|
110
|
+
});
|
|
111
|
+
await test.step("Activating with Home", async () => {
|
|
112
|
+
await openCombobox(combobox, listbox);
|
|
113
|
+
await combobox.press("Home");
|
|
114
|
+
const active = await isActive(listbox.getByRole("option").first());
|
|
115
|
+
expect(active, "Given option should be active").toBeTruthy();
|
|
116
|
+
await expect(combobox).toBeFocused();
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const listboxTesting = async ({
|
|
120
|
+
page,
|
|
121
|
+
listbox,
|
|
122
|
+
options,
|
|
123
|
+
isOptionActive
|
|
124
|
+
}) => {
|
|
125
|
+
const expectOptionToBeActive = async (locator, message) => {
|
|
126
|
+
expect(await isOptionActive(locator), message).toBeTruthy();
|
|
127
|
+
const optionId = await locator.getAttribute("id");
|
|
128
|
+
expect(optionId).toBeDefined();
|
|
129
|
+
await expect(
|
|
130
|
+
listbox,
|
|
131
|
+
"listbox should have set aria-activedescendant to the ID of the currently visually active option"
|
|
132
|
+
).toHaveAttribute("aria-activedescendant", optionId);
|
|
133
|
+
};
|
|
134
|
+
await expect(listbox).toBeVisible();
|
|
135
|
+
await expect(
|
|
136
|
+
listbox,
|
|
137
|
+
'listbox must have a "aria-label" attribute with an existing id'
|
|
138
|
+
).toHaveAttribute("aria-label");
|
|
139
|
+
await listbox.getAttribute("aria-label").then((label) => expect(page.locator(`#${label}`)).toBeDefined());
|
|
140
|
+
await expect(listbox, "listbox must have role attribute with value listbox").toHaveAttribute(
|
|
141
|
+
"role",
|
|
142
|
+
"listbox"
|
|
143
|
+
);
|
|
144
|
+
for (const option of await options.all()) {
|
|
145
|
+
await expect(option, "option must have arial-label attribute").toHaveAttribute("aria-label");
|
|
146
|
+
await expect(option, "option must have role attribute with value option").toHaveAttribute(
|
|
147
|
+
"role",
|
|
148
|
+
"option"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
await page.keyboard.press("Tab");
|
|
152
|
+
await expect(listbox, "Listbox should be focused when pressing tab key").toBeFocused();
|
|
153
|
+
await listbox.press("ArrowDown");
|
|
154
|
+
await expectOptionToBeActive(
|
|
155
|
+
options.first(),
|
|
156
|
+
"Pressing arrow down key when no option is active should activate the first option"
|
|
157
|
+
);
|
|
158
|
+
await expect(
|
|
159
|
+
listbox,
|
|
160
|
+
"When option is visually active, DOM focus should still be on the listbox"
|
|
161
|
+
).toBeFocused();
|
|
162
|
+
await listbox.press("ArrowDown");
|
|
163
|
+
await expectOptionToBeActive(
|
|
164
|
+
options.nth(1),
|
|
165
|
+
"Pressing arrow down key should activate the next option"
|
|
166
|
+
);
|
|
167
|
+
await listbox.press(" ");
|
|
168
|
+
await expect(
|
|
169
|
+
options.nth(1),
|
|
170
|
+
"Pressing space key should select the currently active option"
|
|
171
|
+
).toHaveAttribute("aria-selected", "true");
|
|
172
|
+
await listbox.press("ArrowUp");
|
|
173
|
+
await expectOptionToBeActive(
|
|
174
|
+
options.first(),
|
|
175
|
+
"Pressing arrow up key should activate the previous option"
|
|
176
|
+
);
|
|
177
|
+
await listbox.press("End");
|
|
178
|
+
await expectOptionToBeActive(options.last(), "Pressing End key should activate the last option");
|
|
179
|
+
const secondOptionText = await options.nth(1).textContent();
|
|
180
|
+
expect(secondOptionText).toBeDefined();
|
|
181
|
+
const firstCharacter = secondOptionText.charAt(0);
|
|
182
|
+
await listbox.press(firstCharacter);
|
|
183
|
+
await expectOptionToBeActive(
|
|
184
|
+
listbox.getByLabel(firstCharacter).first(),
|
|
185
|
+
"Pressing any other printable character should activate the fist option starting with the pressed key"
|
|
186
|
+
);
|
|
187
|
+
await listbox.press("Home");
|
|
188
|
+
await expectOptionToBeActive(
|
|
189
|
+
options.first(),
|
|
190
|
+
"Pressing Home key should activate the first option"
|
|
191
|
+
);
|
|
192
|
+
const firstOptionHeight = await options.first().evaluate((element) => element.clientHeight);
|
|
193
|
+
await listbox.evaluate((element, height) => {
|
|
194
|
+
element.style.height = `${height}px`;
|
|
195
|
+
element.style.overflow = "hidden";
|
|
196
|
+
}, firstOptionHeight);
|
|
197
|
+
await expect(options.nth(1)).not.toBeInViewport();
|
|
198
|
+
await listbox.press("ArrowDown");
|
|
199
|
+
await expect(
|
|
200
|
+
options.nth(1),
|
|
201
|
+
"activating an option should scroll it into viewport if not visible"
|
|
202
|
+
).toBeInViewport();
|
|
203
|
+
await listbox.evaluate((element) => {
|
|
204
|
+
element.style.height = "";
|
|
205
|
+
element.style.overflow = "";
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
const menuButtonTesting = async ({
|
|
209
|
+
page,
|
|
210
|
+
button,
|
|
211
|
+
menu,
|
|
212
|
+
menuItems
|
|
213
|
+
}) => {
|
|
214
|
+
await expect(
|
|
215
|
+
button,
|
|
216
|
+
'navigation menu should have an "aria-haspopup" attribute set to true'
|
|
217
|
+
).toHaveAttribute("aria-haspopup", "true");
|
|
218
|
+
await expect(button).toBeVisible();
|
|
219
|
+
await expect(button, "button must have arial-controls attribute").toHaveAttribute(
|
|
220
|
+
"aria-controls"
|
|
221
|
+
);
|
|
222
|
+
await expect(button, "button must have aria-expanded attribute").toHaveAttribute(
|
|
223
|
+
"aria-expanded",
|
|
224
|
+
"false"
|
|
225
|
+
);
|
|
226
|
+
await page.keyboard.press("Tab");
|
|
227
|
+
await expect(button, "Button should be focused when pressing tab key").toBeFocused();
|
|
228
|
+
const firstItem = menuItems.first();
|
|
229
|
+
const secondItem = menuItems.nth(1);
|
|
230
|
+
const lastItem = menuItems.last();
|
|
231
|
+
await page.keyboard.press("Enter");
|
|
232
|
+
await expect(button, "button must have aria-expanded attribute").toHaveAttribute(
|
|
233
|
+
"aria-expanded",
|
|
234
|
+
"true"
|
|
235
|
+
);
|
|
236
|
+
await button.press("ArrowDown");
|
|
237
|
+
await expect(
|
|
238
|
+
firstItem,
|
|
239
|
+
"First item should be focused when pressing arrow down key"
|
|
240
|
+
).toBeFocused();
|
|
241
|
+
await menu.press("ArrowDown");
|
|
242
|
+
await expect(
|
|
243
|
+
secondItem,
|
|
244
|
+
"Second item should be focused when pressing arrow down key"
|
|
245
|
+
).toBeFocused();
|
|
246
|
+
await menu.press("ArrowUp");
|
|
247
|
+
await expect(firstItem, "First item should be focused when pressing arrow up key").toBeFocused();
|
|
248
|
+
await page.keyboard.press("Tab");
|
|
249
|
+
await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
|
|
250
|
+
await page.keyboard.press("Tab");
|
|
251
|
+
await menu.press("Home");
|
|
252
|
+
await expect(firstItem, "First item should be focused when pressing home key").toBeFocused();
|
|
253
|
+
await page.keyboard.press("Tab");
|
|
254
|
+
await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
|
|
255
|
+
await page.keyboard.press("Tab");
|
|
256
|
+
await menu.press("End");
|
|
257
|
+
await expect(lastItem, "Last item should be focused when pressing end key").toBeFocused();
|
|
258
|
+
};
|
|
259
|
+
const navigationTesting = async ({ nav, buttons }) => {
|
|
260
|
+
await expect(nav).toHaveRole("navigation");
|
|
261
|
+
await expect(nav).toHaveAttribute("aria-label");
|
|
262
|
+
await buttons.first().focus();
|
|
263
|
+
await nav.press("ArrowRight");
|
|
264
|
+
await expect(buttons.nth(1)).toBeFocused();
|
|
265
|
+
await nav.press("ArrowLeft");
|
|
266
|
+
await expect(buttons.nth(0)).toBeFocused();
|
|
267
|
+
};
|
|
268
|
+
const tabsTesting = async (options) => {
|
|
269
|
+
await expect(options.tablist, 'tablist element must have role "tablist"').toHaveRole("tablist");
|
|
270
|
+
await expect(options.tablist, "tablist must have an accessible label").toHaveAttribute(
|
|
271
|
+
"aria-label"
|
|
272
|
+
);
|
|
273
|
+
const firstTab = options.tablist.getByRole("tab").first();
|
|
274
|
+
const secondTab = options.tablist.getByRole("tab").nth(1);
|
|
275
|
+
const lastTab = options.tablist.getByRole("tab").last();
|
|
276
|
+
const { tabId, panelId } = await expectTabAttributes(firstTab, true);
|
|
277
|
+
await expectPanelAttributes(options.page.locator(`#${panelId}`), tabId);
|
|
278
|
+
await secondTab.click();
|
|
279
|
+
const { tabId: tabId2, panelId: panelId2 } = await expectTabAttributes(secondTab, true);
|
|
280
|
+
await expectPanelAttributes(options.page.locator(`#${panelId2}`), tabId2);
|
|
281
|
+
await expect(secondTab, "second tab should be focused").toBeFocused();
|
|
282
|
+
await expect(options.page.getByRole("tabpanel"), "should hide previous panel").toHaveCount(1);
|
|
283
|
+
await options.page.keyboard.press("ArrowLeft");
|
|
284
|
+
await expect(firstTab, "should focus previous tab when pressing arrow left").toBeFocused();
|
|
285
|
+
await options.page.keyboard.press("End");
|
|
286
|
+
await expect(lastTab, "should focus last tab when pressing End").toBeFocused();
|
|
287
|
+
await options.page.keyboard.press("ArrowRight");
|
|
288
|
+
await expect(
|
|
289
|
+
firstTab,
|
|
290
|
+
"should focus first tab when last tab is focused and pressing arrow right"
|
|
291
|
+
).toBeFocused();
|
|
292
|
+
await options.page.keyboard.press("ArrowRight");
|
|
293
|
+
await expect(secondTab, "should focus next tab when pressing arrow right").toBeFocused();
|
|
294
|
+
await options.page.keyboard.press("Home");
|
|
295
|
+
await expect(firstTab, "should focus first tab when pressing Home").toBeFocused();
|
|
296
|
+
await options.page.keyboard.press("ArrowLeft");
|
|
297
|
+
await expect(
|
|
298
|
+
lastTab,
|
|
299
|
+
"should focus last tab when first tab is focused and pressing arrow left"
|
|
300
|
+
).toBeFocused();
|
|
301
|
+
await options.page.keyboard.press("Enter");
|
|
302
|
+
const { tabId: tabIdLast, panelId: panelIdLast } = await expectTabAttributes(lastTab, true);
|
|
303
|
+
await expectPanelAttributes(options.page.locator(`#${panelIdLast}`), tabIdLast);
|
|
304
|
+
await firstTab.focus();
|
|
305
|
+
await options.page.keyboard.press("Space");
|
|
306
|
+
const { tabId: tabIdFirst, panelId: panelIdFirst } = await expectTabAttributes(firstTab, true);
|
|
307
|
+
await expectPanelAttributes(options.page.locator(`#${panelIdFirst}`), tabIdFirst);
|
|
308
|
+
await firstTab.click();
|
|
309
|
+
await secondTab.evaluate((element) => element.ariaDisabled = "true");
|
|
310
|
+
await expect(secondTab, "should disable second tab when setting aria-disabled").toBeDisabled();
|
|
311
|
+
await options.page.keyboard.press("ArrowRight");
|
|
312
|
+
await expect(secondTab, "should not focus second tab if its aria-disabled").not.toBeFocused();
|
|
313
|
+
await expect(
|
|
314
|
+
options.tablist.getByRole("tab").nth(2),
|
|
315
|
+
"should focus next tab after disabled one when pressing arrow right"
|
|
316
|
+
).toBeFocused();
|
|
317
|
+
await options.page.keyboard.press("ArrowLeft");
|
|
318
|
+
await expect(
|
|
319
|
+
firstTab,
|
|
320
|
+
"should focus tab before disabled one when pressing arrow left"
|
|
321
|
+
).toBeFocused();
|
|
322
|
+
await secondTab.evaluate((element) => element.ariaDisabled = null);
|
|
323
|
+
await firstTab.evaluate((element) => element.ariaDisabled = "true");
|
|
324
|
+
await options.page.keyboard.press("Home");
|
|
325
|
+
await expect(
|
|
326
|
+
secondTab,
|
|
327
|
+
"should focus second tab when pressing Home if first tab is disabled"
|
|
328
|
+
).toBeFocused();
|
|
329
|
+
await firstTab.evaluate((element) => element.ariaDisabled = null);
|
|
330
|
+
await lastTab.evaluate((element) => element.ariaDisabled = "true");
|
|
331
|
+
await firstTab.focus();
|
|
332
|
+
await options.page.keyboard.press("End");
|
|
333
|
+
await expect(
|
|
334
|
+
options.tablist.getByRole("tab").nth(-2),
|
|
335
|
+
"should focus second last tab when pressing End if last tab is disabled"
|
|
336
|
+
).toBeFocused();
|
|
337
|
+
};
|
|
338
|
+
const expectTabAttributes = async (tab, selected) => {
|
|
339
|
+
await expect(tab, 'tab must have role "tab"').toHaveRole("tab");
|
|
340
|
+
await expect(tab, "tab must have an ID").toHaveAttribute("id");
|
|
341
|
+
await expect(tab, 'tab must have "aria-selected" set').toHaveAttribute(
|
|
342
|
+
"aria-selected",
|
|
343
|
+
String(selected)
|
|
344
|
+
);
|
|
345
|
+
await expect(tab, 'tab must have "aria-controls" set').toHaveAttribute("aria-controls");
|
|
346
|
+
{
|
|
347
|
+
await expect(tab, "selected tab should be focusable").toHaveAttribute("tabindex", "0");
|
|
348
|
+
}
|
|
349
|
+
const tabId = await tab.getAttribute("id");
|
|
350
|
+
const panelId = await tab.getAttribute("aria-controls");
|
|
351
|
+
return { tabId, panelId };
|
|
352
|
+
};
|
|
353
|
+
const expectPanelAttributes = async (panel, tabId) => {
|
|
354
|
+
await expect(panel, "panel should be visible").toBeVisible();
|
|
355
|
+
await expect(panel, 'panel must have role "tabpanel"').toHaveRole("tabpanel");
|
|
356
|
+
await expect(panel, "panel must have an ID").toHaveAttribute("id");
|
|
357
|
+
await expect(panel, 'panel must have "aria-labelledby" set').toHaveAttribute(
|
|
358
|
+
"aria-labelledby",
|
|
359
|
+
tabId
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
export {
|
|
363
|
+
comboboxSelectOnlyTesting,
|
|
364
|
+
comboboxTesting,
|
|
365
|
+
listboxTesting,
|
|
366
|
+
menuButtonTesting,
|
|
367
|
+
navigationTesting,
|
|
368
|
+
tabsTesting
|
|
369
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ComponentPublicInstance, HTMLAttributes, MaybeRef, Ref, WritableComputedRef } from 'vue';
|
|
2
|
+
import { IfDefined } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Properties as they can be used by `v-bind` on an HTML element.
|
|
5
|
+
* This includes generic html attributes and the vue reserved `ref` property.
|
|
6
|
+
* `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
|
|
7
|
+
*/
|
|
8
|
+
export type VBindAttributes<A extends HTMLAttributes = HTMLAttributes, E extends Element = Element> = A & {
|
|
9
|
+
ref?: VueTemplateRef<E>;
|
|
10
|
+
};
|
|
11
|
+
export type IteratedHeadlessElementFunc<A extends HTMLAttributes, T extends Record<string, unknown>> = (opts: T) => VBindAttributes<A>;
|
|
12
|
+
export type HeadlessElementAttributes<A extends HTMLAttributes> = VBindAttributes<A> | IteratedHeadlessElementFunc<A, any>;
|
|
13
|
+
export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
|
|
14
|
+
export type HeadlessState = Record<string, Ref>;
|
|
15
|
+
export type HeadlessComposable<Elements extends HeadlessElements, State extends HeadlessState | undefined = undefined, Internals extends object | undefined = undefined> = {
|
|
16
|
+
elements: Elements;
|
|
17
|
+
} & IfDefined<"internals", Internals> & IfDefined<"state", State>;
|
|
18
|
+
/**
|
|
19
|
+
* We use this identity function to ensure the correct typings of the headless composables
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
|
|
23
|
+
* const tooltipId = useId();
|
|
24
|
+
* const isVisible = ref(initialVisible);
|
|
25
|
+
*
|
|
26
|
+
* const hoverEvents = {
|
|
27
|
+
* onMouseover: () => (isVisible.value = true),
|
|
28
|
+
* onMouseout: () => (isVisible.value = false),
|
|
29
|
+
* onFocusin: () => (isVisible.value = true),
|
|
30
|
+
* onFocusout: () => (isVisible.value = false),
|
|
31
|
+
* };
|
|
32
|
+
*
|
|
33
|
+
* return {
|
|
34
|
+
* elements: {
|
|
35
|
+
* trigger: {
|
|
36
|
+
* "aria-describedby": tooltipId,
|
|
37
|
+
* ...hoverEvents,
|
|
38
|
+
* },
|
|
39
|
+
* tooltip: {
|
|
40
|
+
* role: "tooltip",
|
|
41
|
+
* id: tooltipId,
|
|
42
|
+
* tabindex: "-1",
|
|
43
|
+
* ...hoverEvents,
|
|
44
|
+
* },
|
|
45
|
+
* },
|
|
46
|
+
* state: {
|
|
47
|
+
* isVisible,
|
|
48
|
+
* },
|
|
49
|
+
* };
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare const createBuilder: <Args extends unknown[] = unknown[], Elements extends HeadlessElements = HeadlessElements, State extends HeadlessState | undefined = undefined, Internals extends object | undefined = undefined>(builder: (...args: Args) => HeadlessComposable<Elements, State, Internals>) => (...args: Args) => HeadlessComposable<Elements, State, Internals>;
|
|
55
|
+
type VueTemplateRefElement<E extends Element> = E | (ComponentPublicInstance & {
|
|
56
|
+
$el: E;
|
|
57
|
+
}) | null;
|
|
58
|
+
type VueTemplateRef<E extends Element> = Ref<VueTemplateRefElement<E>>;
|
|
59
|
+
export declare const HeadlessElRefSymbol: unique symbol;
|
|
60
|
+
export type HeadlessElRef<E extends Element> = WritableComputedRef<E> & {
|
|
61
|
+
/**
|
|
62
|
+
* type differentiator
|
|
63
|
+
* ensures that only `createElRef` can be used for headless element ref bindings
|
|
64
|
+
*/
|
|
65
|
+
[HeadlessElRefSymbol]: true;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Creates a special writeable computed that references a DOM Element.
|
|
69
|
+
* Vue Component references will be unwrapped.
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* createBuilder() => {
|
|
73
|
+
* const buttonRef = createElRef<HtmlButtonElement>();
|
|
74
|
+
* return {
|
|
75
|
+
* elements: {
|
|
76
|
+
* button: {
|
|
77
|
+
* ref: buttonRef,
|
|
78
|
+
* },
|
|
79
|
+
* }
|
|
80
|
+
* };
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function createElRef<E extends Element>(): HeadlessElRef<E>;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PressedKey = string | Partial<Pick<KeyboardEvent, "altKey" | "key" | "ctrlKey" | "metaKey" | "shiftKey" | "code">>;
|
|
2
|
+
/**
|
|
3
|
+
* Check if a specified key was pressed.
|
|
4
|
+
* @param event The KeyboardEvent
|
|
5
|
+
* @param key The key, either the [key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) as a string (e.g. "m")
|
|
6
|
+
* or an object with the relevant key parameters, e.g. `{ key: "m", altKey: true }`
|
|
7
|
+
* @returns true, if the key was pressed with the specified parameters
|
|
8
|
+
*/
|
|
9
|
+
export declare const wasKeyPressed: (event: KeyboardEvent, key: PressedKey) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if the `key` property of a KeyboardEvent is a printable character.
|
|
12
|
+
*
|
|
13
|
+
* There is no standardized or specified algorithm to check for [named keys](https://www.w3.org/TR/uievents-key/#named-key-attribute-values) vs printable characters.
|
|
14
|
+
* For this check we use the provided list provided by the standard, which might be incomplete.
|
|
15
|
+
*/
|
|
16
|
+
export declare const isPrintableCharacter: (key: string) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Based on https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
|
19
|
+
*
|
|
20
|
+
* Extracted using
|
|
21
|
+
* ```js
|
|
22
|
+
* copy(JSON.stringify([...document.querySelectorAll(".key-table-key")].map(e => e.innerText.replaceAll("\"", ""))));
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const NAMED_KEYS: readonly ["Unidentified", "Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock", "Meta", "NumLock", "ScrollLock", "Shift", "Symbol", "SymbolLock", "Hyper", "Super", "Enter", "Tab", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", "End", "Home", "PageDown", "PageUp", "Backspace", "Clear", "Copy", "CrSel", "Cut", "Delete", "EraseEof", "ExSel", "Insert", "Paste", "Redo", "Undo", "Accept", "Again", "Attn", "Cancel", "ContextMenu", "Escape", "Execute", "Find", "Help", "Pause", "Play", "Props", "Select", "ZoomIn", "ZoomOut", "BrightnessDown", "BrightnessUp", "Eject", "LogOff", "Power", "PowerOff", "PrintScreen", "Hibernate", "Standby", "WakeUp", "AllCandidates", "Alphanumeric", "CodeInput", "Compose", "Convert", "Dead", "FinalMode", "GroupFirst", "GroupLast", "GroupNext", "GroupPrevious", "ModeChange", "NextCandidate", "NonConvert", "PreviousCandidate", "Process", "SingleCandidate", "HangulMode", "HanjaMode", "JunjaMode", "Eisu", "Hankaku", "Hiragana", "HiraganaKatakana", "KanaMode", "KanjiMode", "Katakana", "Romaji", "Zenkaku", "ZenkakuHankaku", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "Soft1", "Soft2", "Soft3", "Soft4", "ChannelDown", "ChannelUp", "Close", "MailForward", "MailReply", "MailSend", "MediaClose", "MediaFastForward", "MediaPause", "MediaPlay", "MediaPlayPause", "MediaRecord", "MediaRewind", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "New", "Open", "Print", "Save", "SpellCheck", "Key11", "Key12", "AudioBalanceLeft", "AudioBalanceRight", "AudioBassBoostDown", "AudioBassBoostToggle", "AudioBassBoostUp", "AudioFaderFront", "AudioFaderRear", "AudioSurroundModeNext", "AudioTrebleDown", "AudioTrebleUp", "AudioVolumeDown", "AudioVolumeUp", "AudioVolumeMute", "MicrophoneToggle", "MicrophoneVolumeDown", "MicrophoneVolumeUp", "MicrophoneVolumeMute", "SpeechCorrectionList", "SpeechInputToggle", "LaunchApplication1", "LaunchApplication2", "LaunchCalendar", "LaunchContacts", "LaunchMail", "LaunchMediaPlayer", "LaunchMusicPlayer", "LaunchPhone", "LaunchScreenSaver", "LaunchSpreadsheet", "LaunchWebBrowser", "LaunchWebCam", "LaunchWordProcessor", "BrowserBack", "BrowserFavorites", "BrowserForward", "BrowserHome", "BrowserRefresh", "BrowserSearch", "BrowserStop", "AppSwitch", "Call", "Camera", "CameraFocus", "EndCall", "GoBack", "GoHome", "HeadsetHook", "LastNumberRedial", "Notification", "MannerMode", "VoiceDial", "TV", "TV3DMode", "TVAntennaCable", "TVAudioDescription", "TVAudioDescriptionMixDown", "TVAudioDescriptionMixUp", "TVContentsMenu", "TVDataService", "TVInput", "TVInputComponent1", "TVInputComponent2", "TVInputComposite1", "TVInputComposite2", "TVInputHDMI1", "TVInputHDMI2", "TVInputHDMI3", "TVInputHDMI4", "TVInputVGA1", "TVMediaContext", "TVNetwork", "TVNumberEntry", "TVPower", "TVRadioService", "TVSatellite", "TVSatelliteBS", "TVSatelliteCS", "TVSatelliteToggle", "TVTerrestrialAnalog", "TVTerrestrialDigital", "TVTimer", "AVRInput", "AVRPower", "ColorF0Red", "ColorF1Green", "ColorF2Yellow", "ColorF3Blue", "ColorF4Grey", "ColorF5Brown", "ClosedCaptionToggle", "Dimmer", "DisplaySwap", "DVR", "Exit", "FavoriteClear0", "FavoriteClear1", "FavoriteClear2", "FavoriteClear3", "FavoriteRecall0", "FavoriteRecall1", "FavoriteRecall2", "FavoriteRecall3", "FavoriteStore0", "FavoriteStore1", "FavoriteStore2", "FavoriteStore3", "Guide", "GuideNextDay", "GuidePreviousDay", "Info", "InstantReplay", "Link", "ListProgram", "LiveContent", "Lock", "MediaApps", "MediaAudioTrack", "MediaLast", "MediaSkipBackward", "MediaSkipForward", "MediaStepBackward", "MediaStepForward", "MediaTopMenu", "NavigateIn", "NavigateNext", "NavigateOut", "NavigatePrevious", "NextFavoriteChannel", "NextUserProfile", "OnDemand", "Pairing", "PinPDown", "PinPMove", "PinPToggle", "PinPUp", "PlaySpeedDown", "PlaySpeedReset", "PlaySpeedUp", "RandomToggle", "RcLowBattery", "RecordSpeedNext", "RfBypass", "ScanChannelsToggle", "ScreenModeNext", "Settings", "SplitScreenToggle", "STBInput", "STBPower", "Subtitle", "Teletext", "VideoModeNext", "Wink", "ZoomToggle", "AudioVolumeDown", "AudioVolumeUp", "AudioVolumeMute", "BrowserBack", "BrowserForward", "ChannelDown", "ChannelUp", "ContextMenu", "Eject", "End", "Enter", "Home", "MediaFastForward", "MediaPlay", "MediaPlayPause", "MediaRecord", "MediaRewind", "MediaStop", "MediaNextTrack", "MediaPause", "MediaPreviousTrack", "Power", "Unidentified"];
|
|
26
|
+
export declare const NAMED_KEYS_SET: Set<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MaybeRefOrGetter } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Debounces a given callback which will only be called when not called for the given timeout.
|
|
4
|
+
*
|
|
5
|
+
* @returns Callback to reset the debounce timer.
|
|
6
|
+
*/
|
|
7
|
+
export declare const debounce: <TArgs extends unknown[]>(handler: (...args: TArgs) => void, timeout: MaybeRefOrGetter<number>) => {
|
|
8
|
+
(...lastArgs: TArgs): void;
|
|
9
|
+
abort(): void;
|
|
10
|
+
};
|
|
@@ -8,25 +8,17 @@
|
|
|
8
8
|
* const _error: IfDefined<"b", undefined> & { a: number } = { a: 1, b: 2 };
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type IfDefined<Key extends string, TValue> =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
[key in Key]: TValue;
|
|
15
|
-
}
|
|
16
|
-
: unknown;
|
|
17
|
-
|
|
11
|
+
export type IfDefined<Key extends string, TValue> = TValue extends NonNullable<unknown> ? {
|
|
12
|
+
[key in Key]: TValue;
|
|
13
|
+
} : unknown;
|
|
18
14
|
/**
|
|
19
15
|
* Wraps type `TValue` in an array, if `TMultiple` is true.
|
|
20
16
|
*/
|
|
21
|
-
export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple extends true
|
|
22
|
-
? TValue[]
|
|
23
|
-
: TValue;
|
|
24
|
-
|
|
17
|
+
export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple extends true ? TValue[] : TValue;
|
|
25
18
|
/**
|
|
26
19
|
* A type that can be wrapped in an array.
|
|
27
20
|
*/
|
|
28
21
|
export type Arrayable<T> = T | Array<T>;
|
|
29
|
-
|
|
30
22
|
/**
|
|
31
23
|
* Either the actual value or a nullish one.
|
|
32
24
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mocks the following vue lifecycle functions:
|
|
3
|
+
* - onBeforeMount
|
|
4
|
+
* - onMounted
|
|
5
|
+
* - onBeforeUnmount
|
|
6
|
+
* - onUnmounted
|
|
7
|
+
*
|
|
8
|
+
* `onBeforeMount` and `onMounted` callbacks are executed immediately.
|
|
9
|
+
* `onBeforeUnmount` and `onUnmounted` are executed when the returned callback is run.
|
|
10
|
+
* @returns a callback to trigger the run of `onBeforeUnmount` and `onUnmounted`
|
|
11
|
+
*/
|
|
12
|
+
export declare const mockVueLifecycle: () => () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sit-onyx/headless",
|
|
3
3
|
"description": "Headless composables for Vue",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.22",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Schwarz IT KG",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
"node": ">=20"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"dist"
|
|
13
13
|
],
|
|
14
|
-
"types": "./
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
-
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./playwright": {
|
|
21
|
+
"types": "./dist/playwright.d.ts",
|
|
22
|
+
"default": "./dist/playwright.js"
|
|
23
|
+
}
|
|
18
24
|
},
|
|
19
25
|
"homepage": "https://onyx.schwarz/development/packages/headless.html",
|
|
20
26
|
"repository": {
|
|
@@ -27,15 +33,18 @@
|
|
|
27
33
|
},
|
|
28
34
|
"peerDependencies": {
|
|
29
35
|
"typescript": ">= 5",
|
|
30
|
-
"vue": ">= 3.5.0"
|
|
36
|
+
"vue": ">= 3.5.0",
|
|
37
|
+
"@playwright/experimental-ct-vue": "1.51.1",
|
|
38
|
+
"@playwright/test": "1.51.1"
|
|
31
39
|
},
|
|
32
40
|
"devDependencies": {
|
|
33
41
|
"@vue/compiler-dom": "3.5.16",
|
|
42
|
+
"vite-plugin-dts": "^4.5.4",
|
|
34
43
|
"vue": "3.5.16",
|
|
35
44
|
"@sit-onyx/shared": "^1.0.0-beta.4"
|
|
36
45
|
},
|
|
37
46
|
"scripts": {
|
|
38
|
-
"build": "
|
|
47
|
+
"build": "vite build",
|
|
39
48
|
"test": "vitest",
|
|
40
49
|
"test:playwright": "playwright install && playwright test"
|
|
41
50
|
}
|