@sit-onyx/headless 1.0.0-beta.2 → 1.0.0-beta.4
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/package.json +1 -1
- package/src/composables/comboBox/SelectOnlyCombobox.vue +6 -2
- package/src/composables/comboBox/TestCombobox.vue +1 -2
- package/src/composables/comboBox/createComboBox.ts +0 -6
- package/src/composables/listbox/createListbox.ts +2 -2
- package/src/composables/menuButton/createMenuButton.ts +3 -3
- package/src/composables/navigationMenu/createMenu.testing.ts +2 -13
- package/src/composables/tooltip/createTooltip.ts +2 -2
- package/src/index.ts +1 -0
- package/src/playwright.ts +1 -0
- package/src/utils/builder.ts +106 -11
package/package.json
CHANGED
|
@@ -26,7 +26,6 @@ const onToggle = () => (isExpanded.value = !isExpanded.value);
|
|
|
26
26
|
const onTypeAhead = () => {};
|
|
27
27
|
|
|
28
28
|
const comboBox = createComboBox({
|
|
29
|
-
inputValue: selectedOption,
|
|
30
29
|
autocomplete: "none",
|
|
31
30
|
label: "some label",
|
|
32
31
|
listLabel: "List",
|
|
@@ -51,7 +50,12 @@ defineExpose({ comboBox });
|
|
|
51
50
|
|
|
52
51
|
<template>
|
|
53
52
|
<div ref="comboboxRef">
|
|
54
|
-
<input
|
|
53
|
+
<input
|
|
54
|
+
v-bind="input"
|
|
55
|
+
v-model="selectedOption"
|
|
56
|
+
readonly
|
|
57
|
+
@keydown.arrow-down="isExpanded = true"
|
|
58
|
+
/>
|
|
55
59
|
|
|
56
60
|
<button v-bind="button">
|
|
57
61
|
<template v-if="isExpanded">⬆️</template>
|
|
@@ -30,7 +30,6 @@ const onAutocomplete = (input: string) => (searchTerm.value = input);
|
|
|
30
30
|
const onToggle = () => (isExpanded.value = !isExpanded.value);
|
|
31
31
|
|
|
32
32
|
const comboBox = createComboBox({
|
|
33
|
-
inputValue: searchTerm,
|
|
34
33
|
autocomplete: "list",
|
|
35
34
|
label: "some label",
|
|
36
35
|
listLabel: "List",
|
|
@@ -55,7 +54,7 @@ defineExpose({ comboBox });
|
|
|
55
54
|
|
|
56
55
|
<template>
|
|
57
56
|
<div ref="comboboxRef">
|
|
58
|
-
<input v-bind="input" @keydown.arrow-down="isExpanded = true" />
|
|
57
|
+
<input v-bind="input" v-model="searchTerm" @keydown.arrow-down="isExpanded = true" />
|
|
59
58
|
|
|
60
59
|
<button v-bind="button">
|
|
61
60
|
<template v-if="isExpanded">⬆️</template>
|
|
@@ -41,10 +41,6 @@ export type CreateComboboxOptions<
|
|
|
41
41
|
* Labels the listbox which displays the available options. E.g. the list label could be "Countries" for a combobox which is labelled "Country".
|
|
42
42
|
*/
|
|
43
43
|
listLabel: MaybeRef<string>;
|
|
44
|
-
/**
|
|
45
|
-
* The current value of the combobox. Is updated when an option from the controlled listbox is selected or by typing into it.
|
|
46
|
-
*/
|
|
47
|
-
inputValue: Ref<string | undefined>;
|
|
48
44
|
/**
|
|
49
45
|
* Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
|
|
50
46
|
*/
|
|
@@ -105,7 +101,6 @@ export const createComboBox = createBuilder(
|
|
|
105
101
|
TAutoComplete extends ComboboxAutoComplete,
|
|
106
102
|
TMultiple extends boolean = false,
|
|
107
103
|
>({
|
|
108
|
-
inputValue,
|
|
109
104
|
autocomplete: autocompleteRef,
|
|
110
105
|
onAutocomplete,
|
|
111
106
|
onTypeAhead,
|
|
@@ -250,7 +245,6 @@ export const createComboBox = createBuilder(
|
|
|
250
245
|
* 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.
|
|
251
246
|
*/
|
|
252
247
|
input: computed(() => ({
|
|
253
|
-
value: inputValue.value,
|
|
254
248
|
role: "combobox",
|
|
255
249
|
"aria-expanded": isExpanded.value,
|
|
256
250
|
"aria-controls": controlsId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, ref, unref, watchEffect, type MaybeRef, type Ref } from "vue";
|
|
2
2
|
import { createId } from "../..";
|
|
3
|
-
import { createBuilder, type
|
|
3
|
+
import { createBuilder, type VBindAttributes } from "../../utils/builder";
|
|
4
4
|
import { useTypeAhead } from "../helpers/useTypeAhead";
|
|
5
5
|
|
|
6
6
|
export type ListboxValue = string | number | boolean;
|
|
@@ -152,7 +152,7 @@ export const createListbox = createBuilder(
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
|
|
155
|
-
const listbox = computed<
|
|
155
|
+
const listbox = computed<VBindAttributes>(() =>
|
|
156
156
|
options.controlled
|
|
157
157
|
? {
|
|
158
158
|
role: "listbox",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { computed,
|
|
2
|
-
import { createBuilder } from "../../utils/builder";
|
|
1
|
+
import { computed, type Ref } from "vue";
|
|
2
|
+
import { createBuilder, createElRef } from "../../utils/builder";
|
|
3
3
|
import { createId } from "../../utils/id";
|
|
4
4
|
import { debounce } from "../../utils/timer";
|
|
5
5
|
import { useGlobalEventListener } from "../helpers/useGlobalListener";
|
|
@@ -16,7 +16,7 @@ export const createMenuButton = createBuilder(
|
|
|
16
16
|
({ isExpanded, onToggle }: CreateMenuButtonOptions) => {
|
|
17
17
|
const rootId = createId("menu-button-root");
|
|
18
18
|
const menuId = createId("menu-button-list");
|
|
19
|
-
const menuRef =
|
|
19
|
+
const menuRef = createElRef<HTMLElement>();
|
|
20
20
|
const buttonId = createId("menu-button-button");
|
|
21
21
|
|
|
22
22
|
useGlobalEventListener({
|
|
@@ -22,22 +22,11 @@ export const navigationTesting = async ({ nav, buttons }: NavigationMenuTestingO
|
|
|
22
22
|
*/
|
|
23
23
|
await expect(nav).toHaveRole("navigation");
|
|
24
24
|
await expect(nav).toHaveAttribute("aria-label");
|
|
25
|
-
|
|
26
|
-
* Disclosure buttons should have aria attributes
|
|
27
|
-
*/
|
|
28
|
-
for (const button of await buttons.all()) {
|
|
29
|
-
await expect(button, "button must have arial-controls attribute").toHaveAttribute(
|
|
30
|
-
"aria-controls",
|
|
31
|
-
);
|
|
32
|
-
await expect(button, "button must have aria-expanded attribute").toHaveAttribute(
|
|
33
|
-
"aria-expanded",
|
|
34
|
-
);
|
|
35
|
-
}
|
|
25
|
+
|
|
36
26
|
/**
|
|
37
27
|
* Focus first button
|
|
38
28
|
*/
|
|
39
|
-
await
|
|
40
|
-
await expect(buttons.nth(0)).toBeFocused();
|
|
29
|
+
await buttons.first().focus();
|
|
41
30
|
/**
|
|
42
31
|
* Move keyboard focus among top-level buttons using arrow keys
|
|
43
32
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, onBeforeMount, onBeforeUnmount, ref, unref, type MaybeRef } from "vue";
|
|
2
2
|
import { createId } from "../..";
|
|
3
|
-
import { createBuilder } from "../../utils/builder";
|
|
3
|
+
import { createBuilder, createElRef } from "../../utils/builder";
|
|
4
4
|
import { useOutsideClick } from "../helpers/useOutsideClick";
|
|
5
5
|
|
|
6
6
|
export type CreateTooltipOptions = {
|
|
@@ -22,7 +22,7 @@ export const TOOLTIP_TRIGGERS = ["hover", "click"] as const;
|
|
|
22
22
|
export type TooltipTrigger = (typeof TOOLTIP_TRIGGERS)[number];
|
|
23
23
|
|
|
24
24
|
export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
|
|
25
|
-
const rootRef =
|
|
25
|
+
const rootRef = createElRef<HTMLElement>();
|
|
26
26
|
const tooltipId = createId("tooltip");
|
|
27
27
|
const _isVisible = ref(false);
|
|
28
28
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./composables/listbox/createListbox";
|
|
|
3
3
|
export * from "./composables/menuButton/createMenuButton";
|
|
4
4
|
export * from "./composables/navigationMenu/createMenu";
|
|
5
5
|
export * from "./composables/tooltip/createTooltip";
|
|
6
|
+
export * from "./utils/builder";
|
|
6
7
|
export { createId } from "./utils/id";
|
|
7
8
|
export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard";
|
|
8
9
|
export { debounce } from "./utils/timer";
|
package/src/playwright.ts
CHANGED
package/src/utils/builder.ts
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
shallowRef,
|
|
4
|
+
type ComponentPublicInstance,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
type MaybeRef,
|
|
7
|
+
type Ref,
|
|
8
|
+
type WritableComputedOptions,
|
|
9
|
+
type WritableComputedRef,
|
|
10
|
+
} from "vue";
|
|
2
11
|
import type { IfDefined } from "./types";
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `v-bind`able attributes as they are provided by the headless composables.
|
|
15
|
+
* `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
|
|
16
|
+
*/
|
|
17
|
+
export type VBindAttributes<
|
|
18
|
+
A extends HTMLAttributes = HTMLAttributes,
|
|
19
|
+
E extends Element = Element,
|
|
20
|
+
> = A & {
|
|
21
|
+
ref?: VueTemplateRef<E>;
|
|
22
|
+
};
|
|
5
23
|
|
|
6
|
-
export type IteratedHeadlessElementFunc<
|
|
7
|
-
|
|
8
|
-
|
|
24
|
+
export type IteratedHeadlessElementFunc<
|
|
25
|
+
A extends HTMLAttributes,
|
|
26
|
+
T extends Record<string, unknown>,
|
|
27
|
+
> = (opts: T) => VBindAttributes<A>;
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
export type HeadlessElementAttributes<A extends HTMLAttributes> =
|
|
30
|
+
| VBindAttributes<A>
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
| IteratedHeadlessElementFunc<A, any>;
|
|
12
33
|
|
|
13
|
-
export type HeadlessElements = Record<
|
|
14
|
-
string,
|
|
15
|
-
HeadlessElementAttributes | ComputedRef<HeadlessElementAttributes>
|
|
16
|
-
>;
|
|
34
|
+
export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
|
|
17
35
|
|
|
18
36
|
export type HeadlessState = Record<string, Ref>;
|
|
19
37
|
|
|
@@ -28,6 +46,39 @@ export type HeadlessComposable<
|
|
|
28
46
|
|
|
29
47
|
/**
|
|
30
48
|
* We use this identity function to ensure the correct typings of the headless composables
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
|
|
52
|
+
* const tooltipId = createId("tooltip");
|
|
53
|
+
* const isVisible = ref(initialVisible);
|
|
54
|
+
*
|
|
55
|
+
* const hoverEvents = {
|
|
56
|
+
* onMouseover: () => (isVisible.value = true),
|
|
57
|
+
* onMouseout: () => (isVisible.value = false),
|
|
58
|
+
* onFocusin: () => (isVisible.value = true),
|
|
59
|
+
* onFocusout: () => (isVisible.value = false),
|
|
60
|
+
* };
|
|
61
|
+
*
|
|
62
|
+
* return {
|
|
63
|
+
* elements: {
|
|
64
|
+
* trigger: {
|
|
65
|
+
* "aria-describedby": tooltipId,
|
|
66
|
+
* ...hoverEvents,
|
|
67
|
+
* },
|
|
68
|
+
* tooltip: {
|
|
69
|
+
* role: "tooltip",
|
|
70
|
+
* id: tooltipId,
|
|
71
|
+
* tabindex: "-1",
|
|
72
|
+
* ...hoverEvents,
|
|
73
|
+
* },
|
|
74
|
+
* },
|
|
75
|
+
* state: {
|
|
76
|
+
* isVisible,
|
|
77
|
+
* },
|
|
78
|
+
* };
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* ```
|
|
31
82
|
*/
|
|
32
83
|
export const createBuilder = <
|
|
33
84
|
Args extends unknown[] = unknown[],
|
|
@@ -37,3 +88,47 @@ export const createBuilder = <
|
|
|
37
88
|
>(
|
|
38
89
|
builder: (...args: Args) => HeadlessComposable<Elements, State, Internals>,
|
|
39
90
|
) => builder;
|
|
91
|
+
|
|
92
|
+
type VueTemplateRefElement<E extends Element> = E | (ComponentPublicInstance & { $el: E }) | null;
|
|
93
|
+
type VueTemplateRef<E extends Element> = Ref<VueTemplateRefElement<E>>;
|
|
94
|
+
|
|
95
|
+
declare const HeadlessElRefSymbol: unique symbol;
|
|
96
|
+
type HeadlessElRef<E extends Element> = WritableComputedRef<E> & {
|
|
97
|
+
/**
|
|
98
|
+
* type differentiator
|
|
99
|
+
* ensures that only `createElRef` can be used for headless element ref bindings
|
|
100
|
+
*/
|
|
101
|
+
[HeadlessElRefSymbol]: true;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates a special writeable computed that references a DOM Element.
|
|
106
|
+
* Vue Component references will be unwrapped.
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* createBuilder() => {
|
|
110
|
+
* const buttonRef = createElRef<HtmlButtonElement>();
|
|
111
|
+
* return {
|
|
112
|
+
* elements: {
|
|
113
|
+
* button: {
|
|
114
|
+
* ref: buttonRef,
|
|
115
|
+
* },
|
|
116
|
+
* }
|
|
117
|
+
* };
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function createElRef<E extends Element>(): HeadlessElRef<E>;
|
|
122
|
+
export function createElRef<
|
|
123
|
+
E extends Element,
|
|
124
|
+
V extends VueTemplateRefElement<E> = VueTemplateRefElement<E>,
|
|
125
|
+
>() {
|
|
126
|
+
const elementRef = shallowRef<E>();
|
|
127
|
+
|
|
128
|
+
return computed({
|
|
129
|
+
set: (element: V) => {
|
|
130
|
+
elementRef.value = element != null && "$el" in element ? element.$el : (element as E);
|
|
131
|
+
},
|
|
132
|
+
get: () => elementRef.value,
|
|
133
|
+
} as WritableComputedOptions<E>);
|
|
134
|
+
}
|