@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,209 +0,0 @@
1
- import { computed, ref, unref, watchEffect, type MaybeRef, type Ref } from "vue";
2
- import { createId } from "../..";
3
- import { createBuilder, type HeadlessElementAttributes } from "../../utils/builder";
4
- import { useTypeAhead } from "../typeAhead";
5
-
6
- export type ListboxValue = string | number | boolean;
7
-
8
- export type ListboxModelValue<
9
- TValue extends ListboxValue = ListboxValue,
10
- TMultiple extends boolean = false,
11
- > = TMultiple extends true ? TValue[] : TValue;
12
-
13
- export type CreateListboxOptions<
14
- TValue extends ListboxValue = ListboxValue,
15
- TMultiple extends boolean = false,
16
- > = {
17
- /**
18
- * Aria label for the listbox.
19
- */
20
- label: MaybeRef<string>;
21
- /**
22
- * Value of currently selected option.
23
- */
24
- selectedOption: Ref<ListboxModelValue<TValue, TMultiple> | undefined>;
25
- /**
26
- * Value of currently (visually) active option.
27
- */
28
- activeOption: Ref<TValue | undefined>;
29
- /**
30
- * Wether the listbox is controlled from the outside, e.g. by a combobox.
31
- * This disables keyboard events and makes the listbox not focusable.
32
- */
33
- controlled?: boolean;
34
- /**
35
- * Whether the listbox is multiselect.
36
- */
37
- multiple?: MaybeRef<TMultiple | undefined>;
38
- /**
39
- * Hook when an option is selected.
40
- */
41
- onSelect?: (value: TValue) => void;
42
- /**
43
- * Hook when the first option should be activated.
44
- */
45
- onActivateFirst?: () => void;
46
- /**
47
- * Hook when the last option should be activated.
48
- */
49
- onActivateLast?: () => void;
50
- /**
51
- * Hook when the next option should be activated.
52
- */
53
- onActivateNext?: (currentValue: TValue) => void;
54
- /**
55
- * Hook when the previous option should be activated.
56
- */
57
- onActivatePrevious?: (currentValue: TValue) => void;
58
- /**
59
- * Hook when the first option starting with the given label should be activated.
60
- */
61
- onTypeAhead?: (label: string) => void;
62
- };
63
-
64
- /**
65
- * Composable for creating a accessibility-conform listbox.
66
- * For supported keyboard shortcuts, see: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable/
67
- */
68
- export const createListbox = createBuilder(
69
- <TValue extends ListboxValue = ListboxValue, TMultiple extends boolean = false>(
70
- options: CreateListboxOptions<TValue, TMultiple>,
71
- ) => {
72
- const isMultiselect = computed(() => unref(options.multiple) ?? false);
73
-
74
- /**
75
- * Map for option IDs. key = option value, key = ID for the HTML element
76
- */
77
- const descendantKeyIdMap = new Map<TValue, string>();
78
-
79
- const getOptionId = (value: TValue) => {
80
- if (!descendantKeyIdMap.has(value)) {
81
- descendantKeyIdMap.set(value, createId("listbox-option"));
82
- }
83
- return descendantKeyIdMap.get(value)!;
84
- };
85
-
86
- /**
87
- * Whether the listbox element is focused.
88
- */
89
- const isFocused = ref(false);
90
-
91
- // scroll currently active option into view if needed
92
- watchEffect(() => {
93
- if (options.activeOption.value == undefined || (!isFocused.value && !options.controlled))
94
- return;
95
- const id = getOptionId(options.activeOption.value);
96
- document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
97
- });
98
-
99
- const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
100
-
101
- const handleKeydown = (event: KeyboardEvent) => {
102
- switch (event.key) {
103
- case " ":
104
- event.preventDefault();
105
- if (options.activeOption.value != undefined) {
106
- // TODO: don't call onSelect if the option is disabled
107
- options.onSelect?.(options.activeOption.value);
108
- }
109
- break;
110
-
111
- case "ArrowUp":
112
- event.preventDefault();
113
- // if no option is active yet, activate the last option
114
- if (options.activeOption.value == undefined) {
115
- options.onActivateLast?.();
116
- return;
117
- }
118
-
119
- options.onActivatePrevious?.(options.activeOption.value);
120
- break;
121
-
122
- case "ArrowDown":
123
- event.preventDefault();
124
- // if no option is active yet, activate the first option
125
- if (options.activeOption.value == undefined) {
126
- options.onActivateFirst?.();
127
- return;
128
- }
129
-
130
- options.onActivateNext?.(options.activeOption.value);
131
- break;
132
-
133
- case "Home":
134
- event.preventDefault();
135
- options.onActivateFirst?.();
136
- break;
137
-
138
- case "End":
139
- event.preventDefault();
140
- options.onActivateLast?.();
141
- break;
142
-
143
- default:
144
- // if printable characters are pressed, the first option/text starting with the typed characters should be active
145
- typeAhead(event);
146
- }
147
- };
148
-
149
- const listbox = computed<HeadlessElementAttributes>(() =>
150
- options.controlled
151
- ? {
152
- role: "listbox",
153
- "aria-multiselectable": isMultiselect.value,
154
- "aria-label": unref(options.label),
155
- tabindex: "-1",
156
- }
157
- : {
158
- role: "listbox",
159
- "aria-multiselectable": isMultiselect.value,
160
- "aria-label": unref(options.label),
161
- tabindex: "0",
162
- "aria-activedescendant":
163
- options.activeOption.value != undefined
164
- ? getOptionId(options.activeOption.value)
165
- : undefined,
166
- onFocus: () => (isFocused.value = true),
167
- onBlur: () => (isFocused.value = false),
168
- onKeydown: handleKeydown,
169
- },
170
- );
171
-
172
- return {
173
- elements: {
174
- listbox,
175
- group: computed(() => {
176
- return (options: { label: string }) => ({
177
- role: "group",
178
- "aria-label": options.label,
179
- });
180
- }),
181
- option: computed(() => {
182
- return (data: {
183
- label: string;
184
- value: TValue;
185
- selected?: boolean;
186
- disabled?: boolean;
187
- }) => {
188
- const isSelected = data.selected ?? false;
189
- return {
190
- id: getOptionId(data.value),
191
- role: "option",
192
- "aria-label": data.label,
193
- "aria-checked": isMultiselect.value ? isSelected : undefined,
194
- "aria-selected": !isMultiselect.value ? isSelected : undefined,
195
- "aria-disabled": data.disabled,
196
- onClick: () => !data.disabled && options.onSelect?.(data.value),
197
- } as const;
198
- };
199
- }),
200
- },
201
- state: {
202
- isFocused,
203
- },
204
- internals: {
205
- getOptionId,
206
- },
207
- };
208
- },
209
- );
@@ -1,150 +0,0 @@
1
- import {
2
- computed,
3
- onBeforeMount,
4
- onBeforeUnmount,
5
- ref,
6
- unref,
7
- watchEffect,
8
- type MaybeRef,
9
- } from "vue";
10
- import { createId } from "../..";
11
- import { createBuilder } from "../../utils/builder";
12
-
13
- export type CreateTooltipOptions = {
14
- open: MaybeRef<TooltipOpen>;
15
- };
16
-
17
- export type TooltipOpen =
18
- | TooltipTrigger
19
- | boolean
20
- | {
21
- type: "hover";
22
- /**
23
- * Number of milliseconds to use as debounce when showing/hiding the tooltip
24
- */
25
- debounce: number;
26
- };
27
-
28
- export const TOOLTIP_TRIGGERS = ["hover", "click"] as const;
29
- export type TooltipTrigger = (typeof TOOLTIP_TRIGGERS)[number];
30
-
31
- export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
32
- const tooltipId = createId("tooltip");
33
- const _isVisible = ref(false);
34
- let timeout: ReturnType<typeof setTimeout> | undefined;
35
-
36
- const debounce = computed(() => {
37
- const open = unref(options.open);
38
- if (typeof open !== "object") return 200;
39
- return open.debounce;
40
- });
41
-
42
- const openType = computed(() => {
43
- const open = unref(options.open);
44
- if (typeof open !== "object") return open;
45
- return open.type;
46
- });
47
-
48
- /**
49
- * Debounced visible state that will only be toggled after a given timeout.
50
- */
51
- const debouncedVisible = computed({
52
- get: () => _isVisible.value,
53
- set: (newValue) => {
54
- clearTimeout(timeout);
55
- timeout = setTimeout(() => {
56
- _isVisible.value = newValue;
57
- }, debounce.value);
58
- },
59
- });
60
-
61
- /**
62
- * Whether the tooltip should be currently visible.
63
- * If openMode is set as boolean it will prefer it over the hover/click state.
64
- */
65
- const isVisible = computed(() => {
66
- if (typeof openType.value === "boolean") return openType.value;
67
- return debouncedVisible.value;
68
- });
69
-
70
- /**
71
- * Toggles the tooltip if element is clicked.
72
- */
73
- const handleClick = () => {
74
- _isVisible.value = !_isVisible.value;
75
- };
76
-
77
- const hoverEvents = computed(() => {
78
- if (openType.value !== "hover") return;
79
- return {
80
- onMouseover: () => (debouncedVisible.value = true),
81
- onMouseout: () => (debouncedVisible.value = false),
82
- onFocusin: () => (_isVisible.value = true),
83
- onFocusout: () => (_isVisible.value = false),
84
- };
85
- });
86
-
87
- /**
88
- * Closes the tooltip if Escape is pressed.
89
- */
90
- const handleDocumentKeydown = (event: KeyboardEvent) => {
91
- if (event.key !== "Escape") return;
92
- _isVisible.value = false;
93
- };
94
-
95
- /**
96
- * Document click handle that closes then tooltip when clicked outside.
97
- * Should only be called when trigger is "click".
98
- */
99
- const handleDocumentClick = (event: MouseEvent) => {
100
- const tooltipParent = document.getElementById(tooltipId)?.parentElement;
101
- if (!tooltipParent || !(event.target instanceof Node)) return;
102
-
103
- const isOutsideClick = !tooltipParent.contains(event.target);
104
- if (isOutsideClick) _isVisible.value = false;
105
- };
106
-
107
- // add global document event listeners only on/before mounted to also work in server side rendering
108
- onBeforeMount(() => {
109
- document.addEventListener("keydown", handleDocumentKeydown);
110
-
111
- /**
112
- * Registers keydown and click handlers when trigger is "click" to close
113
- * the tooltip.
114
- */
115
- watchEffect(() => {
116
- if (openType.value === "click") {
117
- document.addEventListener("click", handleDocumentClick);
118
- } else {
119
- document.removeEventListener("click", handleDocumentClick);
120
- }
121
- });
122
- });
123
-
124
- /**
125
- * Clean up global event listeners to prevent dangling events.
126
- */
127
- onBeforeUnmount(() => {
128
- document.addEventListener("keydown", handleDocumentKeydown);
129
- document.addEventListener("click", handleDocumentClick);
130
- });
131
-
132
- return {
133
- elements: {
134
- trigger: computed(() => ({
135
- "aria-describedby": tooltipId,
136
- onClick: openType.value === "click" ? handleClick : undefined,
137
- ...hoverEvents.value,
138
- })),
139
- tooltip: computed(() => ({
140
- role: "tooltip",
141
- id: tooltipId,
142
- tabindex: "-1",
143
- ...hoverEvents.value,
144
- })),
145
- },
146
- state: {
147
- isVisible,
148
- },
149
- };
150
- });
@@ -1,29 +0,0 @@
1
- import { beforeAll, expect, test, vi } from "vitest";
2
- import { useTypeAhead } from "./typeAhead";
3
-
4
- beforeAll(() => {
5
- vi.useFakeTimers();
6
- });
7
-
8
- test("useTypeAhead", () => {
9
- const spy = vi.fn();
10
- const typeAhead = useTypeAhead(spy);
11
-
12
- typeAhead({ key: "a" });
13
- expect(spy).toHaveBeenCalledOnce();
14
- expect(spy).toHaveBeenLastCalledWith("a");
15
-
16
- typeAhead({ key: "Alt" });
17
- expect(spy).toHaveBeenCalledOnce();
18
- expect(spy).toHaveBeenLastCalledWith("a");
19
-
20
- typeAhead({ key: "b" });
21
- expect(spy).toHaveBeenCalledTimes(2);
22
- expect(spy).toHaveBeenLastCalledWith("ab");
23
-
24
- vi.runAllTimers();
25
-
26
- typeAhead({ key: "c" });
27
- expect(spy).toBeCalledTimes(3);
28
- expect(spy).toHaveBeenLastCalledWith("c");
29
- });
@@ -1,26 +0,0 @@
1
- import { isPrintableCharacter } from "../utils/keyboard";
2
- import { debounce } from "../utils/timer";
3
-
4
- /**
5
- * Enhances typeAhead to combine multiple inputs in quick succession and filter out non-printable characters.
6
- *
7
- * @example
8
- * ```ts
9
- * const typeAhead = useTypeAhead((inputString) => console.log("Typed string:", inputString));
10
- * // ...
11
- * addEventListener("keydown", typeAhead);
12
- * ```
13
- */
14
- export const useTypeAhead = (callback: (input: string) => void, timeout = 500) => {
15
- let inputString = "";
16
- const debouncedReset = debounce(() => (inputString = ""), timeout);
17
-
18
- return (event: Pick<KeyboardEvent, "key">) => {
19
- if (!isPrintableCharacter(event.key)) {
20
- return;
21
- }
22
- debouncedReset();
23
- inputString = `${inputString}${event.key}`;
24
- callback(inputString);
25
- };
26
- };
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./composables/comboBox/createComboBox";
2
- export * from "./composables/listbox/createListbox";
3
- export * from "./composables/tooltip/createTooltip";
4
- export { createId } from "./utils/id";
package/src/playwright.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from "./composables/comboBox/createComboBox.ct";
2
- export * from "./composables/listbox/createListbox.ct";
@@ -1,37 +0,0 @@
1
- import type { ComputedRef, HtmlHTMLAttributes, Ref } from "vue";
2
- import type { IfDefined } from "./types";
3
-
4
- export type IteratedHeadlessElementFunc<T extends Record<string, unknown>> = (
5
- opts: T,
6
- ) => HtmlHTMLAttributes;
7
-
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- export type HeadlessElementAttributes = HtmlHTMLAttributes | IteratedHeadlessElementFunc<any>;
10
-
11
- export type HeadlessElements = Record<
12
- string,
13
- HeadlessElementAttributes | ComputedRef<HeadlessElementAttributes>
14
- >;
15
-
16
- export type HeadlessState = Record<string, Ref>;
17
-
18
- export type HeadlessComposable<
19
- Elements extends HeadlessElements,
20
- State extends HeadlessState | undefined = undefined,
21
- Internals extends object | undefined = undefined,
22
- > = {
23
- elements: Elements;
24
- } & IfDefined<"internals", Internals> &
25
- IfDefined<"state", State>;
26
-
27
- /**
28
- * We use this identity function to ensure the correct typings of the headless composables
29
- */
30
- export const createBuilder = <
31
- P,
32
- Elements extends HeadlessElements,
33
- State extends HeadlessState | undefined = undefined,
34
- Internals extends object | undefined = undefined,
35
- >(
36
- builder: (props: P) => HeadlessComposable<Elements, State, Internals>,
37
- ) => builder;
package/src/utils/id.ts DELETED
@@ -1,14 +0,0 @@
1
- /**
2
- * Returns a unique global id string
3
- */
4
- // ⚠️ we make use of an IIFE to encapsulate the globalCounter so it can never accidentally be used somewhere else.
5
- const nextId = (() => {
6
- let globalCounter = 1;
7
- return () => globalCounter++;
8
- })();
9
-
10
- /**
11
- * Creates a globally unique string using a counter.
12
- * The given name is the prefix.
13
- */
14
- export const createId = (name: string) => `${name}-${nextId()}`;
@@ -1,18 +0,0 @@
1
- import { expect, test } from "vitest";
2
- import { isPrintableCharacter } from "./keyboard";
3
-
4
- test.each([
5
- // ARRANGE
6
- { key: "a", expected: true },
7
- { key: "🎉", expected: true },
8
- { key: "あ", expected: true },
9
- { key: " ", expected: true },
10
- { key: "Meta", expected: false },
11
- { key: "Fn", expected: false },
12
- ])("should return $expected for key $key", ({ key, expected }) => {
13
- // ACT
14
- const result = isPrintableCharacter(key);
15
-
16
- // ASSERT
17
- expect(result).toBe(expected);
18
- });