@sit-onyx/headless 0.1.0-alpha.7 → 0.1.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.
Files changed (65) hide show
  1. package/README.md +2 -8
  2. package/dist/composables/comboBox/SelectOnlyCombobox.d.vue.ts +299 -0
  3. package/dist/composables/comboBox/TestCombobox.ct.d.ts +1 -0
  4. package/dist/composables/comboBox/TestCombobox.d.vue.ts +299 -0
  5. package/dist/composables/comboBox/createComboBox.d.ts +370 -0
  6. package/dist/composables/comboBox/createComboBox.testing.d.ts +10 -0
  7. package/dist/composables/helpers/useDismissible.d.ts +10 -0
  8. package/dist/composables/helpers/useGlobalListener.d.ts +10 -0
  9. package/dist/composables/helpers/useGlobalListener.spec.d.ts +1 -0
  10. package/dist/composables/helpers/useOutsideClick.d.ts +26 -0
  11. package/dist/composables/helpers/useOutsideClick.spec.d.ts +1 -0
  12. package/dist/composables/helpers/useTypeAhead.d.ts +11 -0
  13. package/dist/composables/helpers/useTypeAhead.spec.d.ts +1 -0
  14. package/dist/composables/listbox/TestListbox.ct.d.ts +1 -0
  15. package/dist/composables/listbox/TestListbox.d.vue.ts +2 -0
  16. package/dist/composables/listbox/createListbox.d.ts +102 -0
  17. package/dist/composables/listbox/createListbox.testing.d.ts +24 -0
  18. package/dist/composables/menuButton/TestMenuButton.ct.d.ts +1 -0
  19. package/dist/composables/menuButton/TestMenuButton.d.vue.ts +2 -0
  20. package/dist/composables/menuButton/createMenuButton.d.ts +78 -0
  21. package/dist/composables/menuButton/createMenuButton.testing.d.ts +24 -0
  22. package/dist/composables/navigationMenu/TestMenu.ct.d.ts +1 -0
  23. package/dist/composables/navigationMenu/TestMenu.d.vue.ts +2 -0
  24. package/dist/composables/navigationMenu/createMenu.d.ts +21 -0
  25. package/dist/composables/navigationMenu/createMenu.testing.d.ts +16 -0
  26. package/dist/composables/tabs/TestTabs.ct.d.ts +1 -0
  27. package/dist/composables/tabs/TestTabs.d.vue.ts +2 -0
  28. package/dist/composables/tabs/createTabs.d.ts +48 -0
  29. package/dist/composables/tabs/createTabs.testing.d.ts +13 -0
  30. package/dist/composables/tooltip/createToggletip.d.ts +36 -0
  31. package/dist/composables/tooltip/createTooltip.d.ts +42 -0
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.js +780 -0
  34. package/dist/playwright.d.ts +5 -0
  35. package/dist/playwright.js +369 -0
  36. package/dist/utils/builder.d.ts +85 -0
  37. package/dist/utils/keyboard.d.ts +17 -0
  38. package/dist/utils/keyboard.spec.d.ts +1 -0
  39. package/dist/utils/math.d.ts +6 -0
  40. package/dist/utils/math.spec.d.ts +1 -0
  41. package/dist/utils/object.d.ts +5 -0
  42. package/dist/utils/object.spec.d.ts +1 -0
  43. package/dist/utils/timer.d.ts +10 -0
  44. package/dist/utils/types.d.ts +25 -0
  45. package/dist/utils/vitest.d.ts +12 -0
  46. package/package.json +24 -8
  47. package/src/composables/comboBox/TestCombobox.ct.tsx +0 -13
  48. package/src/composables/comboBox/TestCombobox.vue +0 -76
  49. package/src/composables/comboBox/createComboBox.ct.ts +0 -72
  50. package/src/composables/comboBox/createComboBox.ts +0 -163
  51. package/src/composables/listbox/TestListbox.ct.tsx +0 -17
  52. package/src/composables/listbox/TestListbox.vue +0 -91
  53. package/src/composables/listbox/createListbox.ct.ts +0 -141
  54. package/src/composables/listbox/createListbox.ts +0 -209
  55. package/src/composables/tooltip/createTooltip.ts +0 -150
  56. package/src/composables/typeAhead.spec.ts +0 -29
  57. package/src/composables/typeAhead.ts +0 -26
  58. package/src/index.ts +0 -4
  59. package/src/playwright.ts +0 -2
  60. package/src/utils/builder.ts +0 -37
  61. package/src/utils/id.ts +0 -14
  62. package/src/utils/keyboard.spec.ts +0 -18
  63. package/src/utils/keyboard.ts +0 -328
  64. package/src/utils/timer.ts +0 -15
  65. package/src/utils/types.ts +0 -8
@@ -1,76 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, ref } from "vue";
3
- import { createComboBox } from "./createComboBox";
4
-
5
- const elements = ["a", "b", "c", "d"];
6
- const isExpanded = ref(false);
7
- const inputValue = ref("");
8
- const activeOption = ref("");
9
- const selectedIndex = computed<number | undefined>(() => {
10
- const index = elements.indexOf(activeOption.value);
11
- return index !== -1 ? index : undefined;
12
- });
13
-
14
- const onActivateFirst = () => (activeOption.value = elements[0]);
15
- const onActivateLast = () => (activeOption.value = elements[elements.length - 1]);
16
- const onActivateNext = () => {
17
- if (selectedIndex.value === undefined) {
18
- return onActivateFirst();
19
- }
20
- activeOption.value = elements[selectedIndex.value + (1 % (elements.length - 1))];
21
- };
22
- const onActivatePrevious = () => (activeOption.value = elements[(selectedIndex.value ?? 0) - 1]);
23
- const onSelect = (newValue: string) => (inputValue.value = newValue);
24
- const onToggle = () => (isExpanded.value = !isExpanded.value);
25
-
26
- const comboBox = createComboBox({
27
- listLabel: "List",
28
- activeOption,
29
- inputValue,
30
- isExpanded,
31
- onToggle,
32
- onActivateFirst,
33
- onActivateLast,
34
- onActivateNext,
35
- onActivatePrevious,
36
- onSelect,
37
- });
38
-
39
- const {
40
- elements: { input, label, listBox, button, option },
41
- } = comboBox;
42
-
43
- defineExpose({ comboBox });
44
- </script>
45
- <template>
46
- <div>
47
- <label v-bind="label">
48
- some label:
49
- <input
50
- v-bind="input"
51
- @keydown.arrow-down="isExpanded = true"
52
- @keydown.esc="isExpanded = false"
53
- />
54
- </label>
55
-
56
- <button v-bind="button">
57
- <template v-if="isExpanded">⬆️</template>
58
- <template v-else>⬇️</template>
59
- </button>
60
- <ul v-bind="listBox" :class="{ hidden: !isExpanded }" style="width: 400px">
61
- <li
62
- v-for="e in elements"
63
- :key="e"
64
- v-bind="option({ value: e, label: e, disabled: false })"
65
- :style="{ 'background-color': e === activeOption ? 'red' : undefined }"
66
- >
67
- {{ e }}
68
- </li>
69
- </ul>
70
- </div>
71
- </template>
72
- <style>
73
- .hidden {
74
- display: none;
75
- }
76
- </style>
@@ -1,72 +0,0 @@
1
- import { expect } from "@playwright/experimental-ct-vue";
2
- import type { Locator, Page } from "@playwright/test";
3
-
4
- /**
5
- * Test an implementation of the combobox based on https://w3c.github.io/aria/#combobox
6
- */
7
- export const comboboxTesting = async (
8
- page: Page,
9
- listbox: Locator,
10
- combobox: Locator,
11
- button: Locator,
12
- options: Locator,
13
- ) => {
14
- await expect(listbox, "Typically, the initial state of a combobox is collapsed.").toBeHidden();
15
-
16
- await expect(combobox, "In the collapsed state, the combobox element is visible.").toBeVisible();
17
- await expect(
18
- button,
19
- "In the collapsed state, the optional button element is visible.",
20
- ).toBeVisible();
21
-
22
- await button.click(); // toggle to be expanded
23
- await expect(
24
- combobox,
25
- "A combobox is said to be expanded when the combobox element shows its current value",
26
- ).toHaveValue("");
27
- await expect(
28
- listbox,
29
- "A combobox is said to be expanded when the associated popup is visible",
30
- ).toBeVisible();
31
- await button.click(); // toggle to be closed
32
-
33
- await expect(
34
- combobox,
35
- "Authors MUST set aria-expanded to false when it is collapsed.",
36
- ).toHaveAttribute("aria-expanded", "false");
37
- await button.click(); // toggle to be expanded
38
- await expect(
39
- combobox,
40
- "Authors MUST set aria-expanded to true when it is expanded.",
41
- ).toHaveAttribute("aria-expanded", "true");
42
- await button.click(); // toggle to be closed
43
-
44
- await button.focus();
45
- await expect(button, "authors SHOULD ensure that the button is focusable").toBeFocused();
46
- await expect(
47
- button,
48
- "authors SHOULD ensure that the button is not included in the page Tab sequence",
49
- ).toHaveAttribute("tabindex", "-1");
50
- await expect(
51
- combobox.getByRole("button"),
52
- "authors SHOULD ensure that the button is not a descendant of the element with role combobox",
53
- ).toHaveCount(0);
54
-
55
- const firstElement = options.first();
56
-
57
- // open and select first option
58
- await combobox.focus();
59
- await page.keyboard.press("ArrowDown");
60
- await page.keyboard.press("ArrowDown");
61
-
62
- const firstId = await (await firstElement.elementHandle())!.getAttribute("id");
63
- expect(typeof firstId).toBe("string");
64
- await expect(
65
- combobox,
66
- "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.",
67
- ).toHaveAttribute("aria-activedescendant", firstId as string);
68
- await expect(
69
- combobox,
70
- "When a descendant of the popup element is active, authors MAY ensure that the focus remains on the combobox element",
71
- ).toBeFocused();
72
- };
@@ -1,163 +0,0 @@
1
- import { computed, ref, type MaybeRef, type Ref } from "vue";
2
- import { createBuilder } from "../../utils/builder";
3
- import { createId } from "../../utils/id";
4
- import {
5
- createListbox,
6
- type CreateListboxOptions,
7
- type ListboxValue,
8
- } from "../listbox/createListbox";
9
-
10
- export type CreateComboboxOptions<
11
- TValue extends ListboxValue = ListboxValue,
12
- TMultiple extends boolean = false,
13
- > = {
14
- /**
15
- * Labels the listbox which displays the available options. E.g. the list label could be "Countries" for a combobox which is labelled "Country".
16
- */
17
- listLabel: MaybeRef<string>;
18
- /**
19
- * The current value of the combobox. Is updated when an option from the controlled listbox is selected or by typing into it.
20
- */
21
- inputValue: Ref<TValue | undefined>;
22
- /**
23
- * Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
24
- */
25
- isExpanded: Ref<boolean>;
26
- /**
27
- * If expanded, the active option is the currently highlighted option of the controlled listbox.
28
- */
29
- activeOption: Ref<TValue | undefined>;
30
- /**
31
- * Hook when the popover should toggle.
32
- */
33
- onToggle?: () => void;
34
- } & Pick<
35
- CreateListboxOptions<TValue, TMultiple>,
36
- "onActivateFirst" | "onActivateLast" | "onActivateNext" | "onActivatePrevious" | "onSelect"
37
- >;
38
-
39
- // TODO: https://w3c.github.io/aria/#aria-autocomplete
40
- // TODO: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
41
- // TODO: button as optional
42
-
43
- export const createComboBox = createBuilder(
44
- <TValue extends ListboxValue = ListboxValue, TMultiple extends boolean = false>({
45
- listLabel,
46
- inputValue,
47
- isExpanded,
48
- activeOption,
49
- onToggle,
50
- onSelect,
51
- onActivateFirst,
52
- onActivateLast,
53
- onActivateNext,
54
- onActivatePrevious,
55
- }: CreateComboboxOptions<TValue, TMultiple>) => {
56
- const inputValid = ref(true);
57
- const controlsId = createId("comboBox-control");
58
- const labelId = createId("comboBox-label");
59
-
60
- const handleInput = (event: Event) => {
61
- const inputElement = event.target as HTMLInputElement;
62
- inputValue.value = inputElement.value as TValue;
63
- inputValid.value = inputElement.validity.valid;
64
- };
65
-
66
- const handleBlur = () => {
67
- if (isExpanded.value) {
68
- onToggle?.();
69
- }
70
- };
71
-
72
- const handleKeydown = (event: KeyboardEvent) => {
73
- if (!isExpanded.value) {
74
- return;
75
- }
76
- switch (event.key) {
77
- case "Enter":
78
- event.preventDefault();
79
- if (activeOption.value) {
80
- onSelect?.(activeOption.value);
81
- inputValue.value = activeOption.value;
82
- }
83
- break;
84
- case "Escape":
85
- event.preventDefault();
86
- onToggle?.();
87
- break;
88
- case "ArrowUp":
89
- event.preventDefault();
90
- if (!activeOption.value) {
91
- return onActivateLast?.();
92
- }
93
- onActivatePrevious?.(activeOption.value);
94
- break;
95
- case "ArrowDown":
96
- event.preventDefault();
97
- if (!activeOption.value) {
98
- return onActivateFirst?.();
99
- }
100
- onActivateNext?.(activeOption.value);
101
- break;
102
- case "Home":
103
- event.preventDefault();
104
- onActivateFirst?.();
105
- break;
106
- case "End":
107
- event.preventDefault();
108
- onActivateLast?.();
109
- break;
110
- }
111
- };
112
-
113
- const {
114
- elements: { option, group, listbox },
115
- internals: { getOptionId },
116
- } = createListbox({
117
- label: listLabel,
118
- controlled: true,
119
- activeOption,
120
- selectedOption: activeOption,
121
- onSelect,
122
- });
123
-
124
- return {
125
- elements: {
126
- option,
127
- group,
128
- label: {
129
- id: labelId,
130
- },
131
- /**
132
- * The listbox associated with the combobox.
133
- */
134
- listBox: computed(() => ({
135
- ...listbox.value,
136
- id: controlsId,
137
- })),
138
- /**
139
- * An input that controls another element, that can dynamically pop-up to help the user set the value of the input.
140
- * 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.
141
- */
142
- input: computed(() => ({
143
- value: inputValue.value,
144
- role: "combobox",
145
- "aria-expanded": isExpanded.value,
146
- "aria-controls": controlsId,
147
- "aria-labelledby": labelId,
148
- "aria-activedescendant": activeOption.value ? getOptionId(activeOption.value) : undefined,
149
- onInput: handleInput,
150
- onKeydown: handleKeydown,
151
- onBlur: handleBlur,
152
- })),
153
- /**
154
- * An optional button to control the visibility of the popup.
155
- */
156
- button: computed(() => ({
157
- tabindex: "-1",
158
- onClick: onToggle,
159
- })),
160
- },
161
- };
162
- },
163
- );
@@ -1,17 +0,0 @@
1
- import { test } from "@playwright/experimental-ct-vue";
2
- import TestListbox from "./TestListbox.vue";
3
- import { listboxTesting } from "./createListbox.ct";
4
-
5
- test("listbox", async ({ mount, page }) => {
6
- await mount(<TestListbox />);
7
-
8
- await listboxTesting({
9
- page,
10
- listbox: page.getByRole("listbox"),
11
- options: page.getByRole("option"),
12
- isOptionActive: async (locator) => {
13
- const className = await locator.getAttribute("class");
14
- return className?.includes("focused") ?? false;
15
- },
16
- });
17
- });
@@ -1,91 +0,0 @@
1
- <script lang="ts" setup>
2
- import { ref } from "vue";
3
- import { createListbox } from "./createListbox";
4
-
5
- type Options = (typeof options)[number];
6
-
7
- const selectedOption = ref<Options>();
8
- const activeOption = ref<Options>();
9
-
10
- const options = [
11
- "Apple",
12
- "Banana",
13
- "Mango",
14
- "Kiwi",
15
- "Orange",
16
- "Papaya",
17
- "Apricot",
18
- "Lemon",
19
- "Cranberry",
20
- "Avocado",
21
- "Cherry",
22
- "Coconut",
23
- "Lychee",
24
- "Melon",
25
- "Raspberry",
26
- "Strawberry",
27
- ] as const;
28
-
29
- const {
30
- elements: { listbox, option: headlessOption },
31
- } = createListbox({
32
- label: "Test listbox",
33
- selectedOption,
34
- activeOption,
35
- onSelect: (id) => {
36
- selectedOption.value = selectedOption.value === id ? undefined : id;
37
- },
38
- onActivateFirst: () => (activeOption.value = options[0]),
39
- onActivateLast: () => (activeOption.value = options.at(-1)),
40
- onActivateNext: (currentValue) => {
41
- const currentIndex = options.findIndex((i) => i === currentValue);
42
- if (currentIndex < options.length - 1) {
43
- activeOption.value = options[currentIndex + 1];
44
- }
45
- },
46
- onActivatePrevious: (currentValue) => {
47
- const currentIndex = options.findIndex((i) => i === currentValue);
48
- if (currentIndex > 0) activeOption.value = options[currentIndex - 1];
49
- },
50
- onTypeAhead: (label) => {
51
- const firstMatch = options.find((i) => {
52
- return i.toLowerCase().trim().startsWith(label.toLowerCase());
53
- });
54
- if (!firstMatch) return;
55
- activeOption.value = firstMatch;
56
- },
57
- });
58
- </script>
59
-
60
- <template>
61
- <ul v-bind="listbox">
62
- <li
63
- v-for="option in options"
64
- :key="option"
65
- v-bind="
66
- headlessOption({
67
- value: option,
68
- label: option,
69
- selected: option === selectedOption,
70
- })
71
- "
72
- :class="{ focused: option === activeOption, selected: option === selectedOption }"
73
- >
74
- {{ option }}
75
- </li>
76
- </ul>
77
- </template>
78
-
79
- <style lang="scss" scoped>
80
- li {
81
- height: 1.5rem;
82
- }
83
-
84
- .focused {
85
- background-color: orange;
86
- }
87
-
88
- .selected {
89
- background-color: red;
90
- }
91
- </style>
@@ -1,141 +0,0 @@
1
- import { expect } from "@playwright/experimental-ct-vue";
2
- import type { Locator, Page } from "@playwright/test";
3
-
4
- export type ListboxTestingOptions = {
5
- /**
6
- * Playwright page.
7
- */
8
- page: Page;
9
- /**
10
- * Locator for the listbox element.
11
- */
12
- listbox: Locator;
13
- /**
14
- * Options (at least 3).
15
- */
16
- options: Locator;
17
- /**
18
- * Function that returns whether the given option locator is visually active.
19
- */
20
- isOptionActive: (locator: Locator) => Promise<boolean>;
21
- };
22
-
23
- /**
24
- * Playwright utility for executing accessibility testing for a listbox.
25
- * Will check aria attributes and keyboard shortcuts as defined in https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable.
26
- */
27
- export const listboxTesting = async ({
28
- page,
29
- listbox,
30
- options,
31
- isOptionActive,
32
- }: ListboxTestingOptions) => {
33
- const expectOptionToBeActive = async (locator: Locator, message: string) => {
34
- expect(await isOptionActive(locator), message).toBeTruthy();
35
- const optionId = await locator.getAttribute("id");
36
- expect(optionId).toBeDefined();
37
- await expect(
38
- listbox,
39
- "listbox should have set aria-activedescendant to the ID of the currently visually active option",
40
- ).toHaveAttribute("aria-activedescendant", optionId!);
41
- };
42
-
43
- await expect(listbox).toBeVisible();
44
-
45
- // ensure correct listbox aria attributes
46
- await expect(
47
- listbox,
48
- 'listbox must have a "aria-label" attribute with an existing id',
49
- ).toHaveAttribute("aria-label");
50
-
51
- await listbox
52
- .getAttribute("aria-label")
53
- .then((label) => expect(page.locator(`#${label}`)).toBeDefined());
54
-
55
- await expect(listbox, "listbox must have role attribute with value listbox").toHaveAttribute(
56
- "role",
57
- "listbox",
58
- );
59
-
60
- // ensure that all options have correct aria attributes
61
- for (const option of await options.all()) {
62
- await expect(option, "option must have arial-label attribute").toHaveAttribute("aria-label");
63
- await expect(option, "option must have role attribute with value option").toHaveAttribute(
64
- "role",
65
- "option",
66
- );
67
- }
68
-
69
- await page.keyboard.press("Tab");
70
- await expect(listbox, "Listbox should be focused when pressing tab key").toBeFocused();
71
-
72
- await listbox.press("ArrowDown");
73
-
74
- await expectOptionToBeActive(
75
- options.first(),
76
- "Pressing arrow down key when no option is active should activate the first option",
77
- );
78
- await expect(
79
- listbox,
80
- "When option is visually active, DOM focus should still be on the listbox",
81
- ).toBeFocused();
82
-
83
- await listbox.press("ArrowDown");
84
- await expectOptionToBeActive(
85
- options.nth(1),
86
- "Pressing arrow down key should activate the next option",
87
- );
88
-
89
- await listbox.press(" ");
90
- await expect(
91
- options.nth(1),
92
- "Pressing space key should select the currently active option",
93
- ).toHaveAttribute("aria-selected", "true");
94
-
95
- await listbox.press("ArrowUp");
96
- await expectOptionToBeActive(
97
- options.first(),
98
- "Pressing arrow up key should activate the previous option",
99
- );
100
-
101
- await listbox.press("End");
102
- await expectOptionToBeActive(options.last(), "Pressing End key should activate the last option");
103
-
104
- const secondOptionText = await options.nth(1).textContent();
105
- expect(secondOptionText).toBeDefined();
106
-
107
- const firstCharacter = secondOptionText!.charAt(0);
108
- await listbox.press(firstCharacter);
109
-
110
- await expectOptionToBeActive(
111
- listbox.getByLabel(firstCharacter).first(),
112
- "Pressing any other printable character should activate the fist option starting with the pressed key",
113
- );
114
-
115
- await listbox.press("Home");
116
- await expectOptionToBeActive(
117
- options.first(),
118
- "Pressing Home key should activate the first option",
119
- );
120
-
121
- const firstOptionHeight = await options.first().evaluate((element) => element.clientHeight);
122
-
123
- await listbox.evaluate((element, height) => {
124
- element.style.height = `${height}px`;
125
- element.style.overflow = "hidden";
126
- }, firstOptionHeight);
127
-
128
- await expect(options.nth(1)).not.toBeInViewport();
129
-
130
- await listbox.press("ArrowDown");
131
- await expect(
132
- options.nth(1),
133
- "activating an option should scroll it into viewport if not visible",
134
- ).toBeInViewport();
135
-
136
- // reset temporary styles
137
- await listbox.evaluate((element) => {
138
- element.style.height = "";
139
- element.style.overflow = "";
140
- });
141
- };