@sit-onyx/headless 1.0.0-beta.4 → 1.0.0-beta.6
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/createComboBox.ts +18 -6
- package/src/composables/helpers/useDismissible.ts +19 -0
- package/src/composables/helpers/useOutsideClick.spec.ts +83 -0
- package/src/composables/helpers/useOutsideClick.ts +13 -7
- package/src/composables/listbox/TestListbox.vue +2 -0
- package/src/composables/listbox/createListbox.ts +16 -1
- package/src/composables/tooltip/createToggletip.ts +61 -0
- package/src/composables/tooltip/createTooltip.ts +37 -95
- package/src/index.ts +1 -0
- package/src/utils/builder.ts +2 -1
- package/src/utils/types.ts +7 -0
- package/src/utils/vitest.ts +2 -2
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ListboxValue,
|
|
11
11
|
} from "../listbox/createListbox";
|
|
12
12
|
|
|
13
|
+
/** See https://w3c.github.io/aria/#aria-autocomplete */
|
|
13
14
|
export type ComboboxAutoComplete = "none" | "list" | "both";
|
|
14
15
|
|
|
15
16
|
export const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
|
|
@@ -19,11 +20,15 @@ export const CLOSING_KEYS: PressedKey[] = [
|
|
|
19
20
|
"Enter",
|
|
20
21
|
"Tab",
|
|
21
22
|
];
|
|
22
|
-
const SELECTING_KEYS_SINGLE: PressedKey[] = ["Enter", " "];
|
|
23
|
-
const SELECTING_KEYS_MULTIPLE: PressedKey[] = ["Enter"];
|
|
24
23
|
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const SELECTING_KEYS: PressedKey[] = ["Enter"];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* if the a search input is included, space should not be used to select
|
|
28
|
+
* TODO: idea for the future: move this distinction to the listbox?
|
|
29
|
+
*/
|
|
30
|
+
const isSelectingKey = (event: KeyboardEvent, withSpace?: boolean) => {
|
|
31
|
+
const selectingKeys = withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS;
|
|
27
32
|
return isKeyOfGroup(event, selectingKeys);
|
|
28
33
|
};
|
|
29
34
|
|
|
@@ -41,6 +46,10 @@ export type CreateComboboxOptions<
|
|
|
41
46
|
* Labels the listbox which displays the available options. E.g. the list label could be "Countries" for a combobox which is labelled "Country".
|
|
42
47
|
*/
|
|
43
48
|
listLabel: MaybeRef<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Provides additional description for the listbox which displays the available options.
|
|
51
|
+
*/
|
|
52
|
+
listDescription?: MaybeRef<string | undefined>;
|
|
44
53
|
/**
|
|
45
54
|
* Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
|
|
46
55
|
*/
|
|
@@ -107,6 +116,7 @@ export const createComboBox = createBuilder(
|
|
|
107
116
|
multiple: multipleRef,
|
|
108
117
|
label,
|
|
109
118
|
listLabel,
|
|
119
|
+
listDescription,
|
|
110
120
|
isExpanded: isExpandedRef,
|
|
111
121
|
activeOption,
|
|
112
122
|
onToggle,
|
|
@@ -183,7 +193,7 @@ export const createComboBox = createBuilder(
|
|
|
183
193
|
}
|
|
184
194
|
return onActivateFirst?.();
|
|
185
195
|
}
|
|
186
|
-
if (isSelectingKey(event,
|
|
196
|
+
if (isSelectingKey(event, autocomplete.value === "none")) {
|
|
187
197
|
return handleSelect(activeOption.value!);
|
|
188
198
|
}
|
|
189
199
|
if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) {
|
|
@@ -213,14 +223,16 @@ export const createComboBox = createBuilder(
|
|
|
213
223
|
internals: { getOptionId },
|
|
214
224
|
} = createListbox({
|
|
215
225
|
label: listLabel,
|
|
226
|
+
description: listDescription,
|
|
216
227
|
multiple,
|
|
217
228
|
controlled: true,
|
|
218
229
|
activeOption,
|
|
230
|
+
isExpanded,
|
|
219
231
|
onSelect: handleSelect,
|
|
220
232
|
});
|
|
221
233
|
|
|
222
234
|
useOutsideClick({
|
|
223
|
-
|
|
235
|
+
inside: templateRef,
|
|
224
236
|
onOutsideClick() {
|
|
225
237
|
if (!isExpanded.value) return;
|
|
226
238
|
onToggle?.(true);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { computed, type Ref } from "vue";
|
|
2
|
+
import { useGlobalEventListener } from "./useGlobalListener";
|
|
3
|
+
|
|
4
|
+
type UseDismissibleOptions = { isExpanded: Ref<boolean> };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable that sets `isExpanded` to false, when the `Escape` key is pressed.
|
|
8
|
+
* Addresses the "dismissible" aspect of https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html
|
|
9
|
+
*/
|
|
10
|
+
export const useDismissible = ({ isExpanded }: UseDismissibleOptions) =>
|
|
11
|
+
useGlobalEventListener({
|
|
12
|
+
type: "keydown",
|
|
13
|
+
listener: (e) => {
|
|
14
|
+
if (e.key === "Escape") {
|
|
15
|
+
isExpanded.value = false;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
disabled: computed(() => !isExpanded.value),
|
|
19
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { mockVueLifecycle } from "../../utils/vitest";
|
|
4
|
+
import { useOutsideClick } from "./useOutsideClick";
|
|
5
|
+
|
|
6
|
+
describe("useOutsideClick", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
mockVueLifecycle();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should be defined", () => {
|
|
13
|
+
expect(useOutsideClick).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should detect outside clicks", () => {
|
|
17
|
+
// ARRANGE
|
|
18
|
+
const inside = ref(document.createElement("button"));
|
|
19
|
+
document.body.appendChild(inside.value);
|
|
20
|
+
const outside = ref(document.createElement("button"));
|
|
21
|
+
document.body.appendChild(outside.value);
|
|
22
|
+
|
|
23
|
+
const onOutsideClick = vi.fn();
|
|
24
|
+
useOutsideClick({ inside, onOutsideClick });
|
|
25
|
+
// ACT
|
|
26
|
+
const event = new MouseEvent("click", { bubbles: true });
|
|
27
|
+
outside.value.dispatchEvent(event);
|
|
28
|
+
// ASSERT
|
|
29
|
+
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(onOutsideClick).toBeCalledWith(event);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should detect outside clicks correctly for multiple inside elements", () => {
|
|
34
|
+
// ARRANGE
|
|
35
|
+
const inside = [document.createElement("button"), document.createElement("button")];
|
|
36
|
+
inside.forEach((e) => document.body.appendChild(e));
|
|
37
|
+
const outside = ref(document.createElement("button"));
|
|
38
|
+
document.body.appendChild(outside.value);
|
|
39
|
+
|
|
40
|
+
const onOutsideClick = vi.fn();
|
|
41
|
+
useOutsideClick({ inside, onOutsideClick });
|
|
42
|
+
// ACT
|
|
43
|
+
const event = new MouseEvent("click", { bubbles: true });
|
|
44
|
+
inside[0].dispatchEvent(event);
|
|
45
|
+
inside[1].dispatchEvent(event);
|
|
46
|
+
// ASSERT
|
|
47
|
+
expect(onOutsideClick).not.toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
// ACT
|
|
50
|
+
outside.value.dispatchEvent(event);
|
|
51
|
+
// ASSERT
|
|
52
|
+
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(onOutsideClick).toBeCalledWith(event);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should ignore outside clicks when disabled", async () => {
|
|
57
|
+
// ARRANGE
|
|
58
|
+
vi.useFakeTimers();
|
|
59
|
+
const inside = ref(document.createElement("button"));
|
|
60
|
+
document.body.appendChild(inside.value);
|
|
61
|
+
const outside = ref(document.createElement("button"));
|
|
62
|
+
document.body.appendChild(outside.value);
|
|
63
|
+
|
|
64
|
+
const disabled = ref(false);
|
|
65
|
+
const onOutsideClick = vi.fn();
|
|
66
|
+
useOutsideClick({ inside, disabled, onOutsideClick });
|
|
67
|
+
|
|
68
|
+
// ACT
|
|
69
|
+
const event = new MouseEvent("click", { bubbles: true });
|
|
70
|
+
outside.value.dispatchEvent(event);
|
|
71
|
+
// ASSERT
|
|
72
|
+
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(onOutsideClick).toBeCalledWith(event);
|
|
74
|
+
|
|
75
|
+
// ACT
|
|
76
|
+
disabled.value = true;
|
|
77
|
+
await vi.runAllTimersAsync();
|
|
78
|
+
const event2 = new MouseEvent("click", { bubbles: true });
|
|
79
|
+
outside.value.dispatchEvent(event2);
|
|
80
|
+
// ASSERT
|
|
81
|
+
expect(onOutsideClick).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Arrayable } from "vitest";
|
|
2
|
+
import { toValue, type Ref } from "vue";
|
|
3
|
+
import type { MaybeReactiveSource } from "../../utils/types";
|
|
2
4
|
import { useGlobalEventListener } from "./useGlobalListener";
|
|
3
5
|
|
|
4
6
|
export type UseOutsideClickOptions = {
|
|
5
7
|
/**
|
|
6
8
|
* HTML element of the component where clicks should be ignored
|
|
7
9
|
*/
|
|
8
|
-
|
|
10
|
+
inside: MaybeReactiveSource<Arrayable<HTMLElement | undefined>>;
|
|
9
11
|
/**
|
|
10
12
|
* Callback when an outside click occurred.
|
|
11
13
|
*/
|
|
12
|
-
onOutsideClick: () => void;
|
|
14
|
+
onOutsideClick: (event: MouseEvent) => void;
|
|
13
15
|
/**
|
|
14
16
|
* If `true`, event listeners will be removed and no outside clicks will be captured.
|
|
15
17
|
*/
|
|
@@ -20,14 +22,18 @@ export type UseOutsideClickOptions = {
|
|
|
20
22
|
* Composable for listening to click events that occur outside of a component.
|
|
21
23
|
* Useful to e.g. close flyouts or tooltips.
|
|
22
24
|
*/
|
|
23
|
-
export const useOutsideClick = ({
|
|
25
|
+
export const useOutsideClick = ({ inside, onOutsideClick, disabled }: UseOutsideClickOptions) => {
|
|
24
26
|
/**
|
|
25
27
|
* Document click handle that closes then tooltip when clicked outside.
|
|
26
28
|
* Should only be called when trigger is "click".
|
|
27
29
|
*/
|
|
28
|
-
const listener = (
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const listener = (event: MouseEvent) => {
|
|
31
|
+
const raw = toValue(inside);
|
|
32
|
+
const elements = Array.isArray(raw) ? raw : [raw];
|
|
33
|
+
const isOutsideClick = !elements.some((element) =>
|
|
34
|
+
element?.contains(event.target as HTMLElement),
|
|
35
|
+
);
|
|
36
|
+
if (isOutsideClick) onOutsideClick(event);
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
useGlobalEventListener({ type: "click", listener, disabled });
|
|
@@ -30,7 +30,9 @@ const {
|
|
|
30
30
|
elements: { listbox, option: headlessOption },
|
|
31
31
|
} = createListbox({
|
|
32
32
|
label: "Test listbox",
|
|
33
|
+
description: "Test description",
|
|
33
34
|
activeOption,
|
|
35
|
+
isExpanded: true,
|
|
34
36
|
onSelect: (id) => {
|
|
35
37
|
selectedOption.value = selectedOption.value === id ? undefined : id;
|
|
36
38
|
},
|
|
@@ -10,6 +10,10 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
|
|
|
10
10
|
* Aria label for the listbox.
|
|
11
11
|
*/
|
|
12
12
|
label: MaybeRef<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Aria description for the listbox.
|
|
15
|
+
*/
|
|
16
|
+
description?: MaybeRef<string | undefined>;
|
|
13
17
|
/**
|
|
14
18
|
* Value of currently (visually) active option.
|
|
15
19
|
*/
|
|
@@ -19,6 +23,10 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
|
|
|
19
23
|
* This disables keyboard events and makes the listbox not focusable.
|
|
20
24
|
*/
|
|
21
25
|
controlled?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Controls the opened/visible state of the listbox. When expanded the activeOption can be controlled via the keyboard.
|
|
28
|
+
*/
|
|
29
|
+
isExpanded?: MaybeRef<boolean>;
|
|
22
30
|
/**
|
|
23
31
|
* Whether the listbox is multiselect.
|
|
24
32
|
*/
|
|
@@ -77,6 +85,7 @@ export const createListbox = createBuilder(
|
|
|
77
85
|
options: CreateListboxOptions<TValue, TMultiple>,
|
|
78
86
|
) => {
|
|
79
87
|
const isMultiselect = computed(() => unref(options.multiple) ?? false);
|
|
88
|
+
const isExpanded = computed(() => unref(options.isExpanded) ?? false);
|
|
80
89
|
|
|
81
90
|
/**
|
|
82
91
|
* Map for option IDs. key = option value, key = ID for the HTML element
|
|
@@ -97,7 +106,11 @@ export const createListbox = createBuilder(
|
|
|
97
106
|
|
|
98
107
|
// scroll currently active option into view if needed
|
|
99
108
|
watchEffect(() => {
|
|
100
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
!isExpanded.value ||
|
|
111
|
+
options.activeOption.value == undefined ||
|
|
112
|
+
(!isFocused.value && !options.controlled)
|
|
113
|
+
)
|
|
101
114
|
return;
|
|
102
115
|
const id = getOptionId(options.activeOption.value);
|
|
103
116
|
document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
@@ -158,12 +171,14 @@ export const createListbox = createBuilder(
|
|
|
158
171
|
role: "listbox",
|
|
159
172
|
"aria-multiselectable": isMultiselect.value,
|
|
160
173
|
"aria-label": unref(options.label),
|
|
174
|
+
"aria-description": options.description,
|
|
161
175
|
tabindex: "-1",
|
|
162
176
|
}
|
|
163
177
|
: {
|
|
164
178
|
role: "listbox",
|
|
165
179
|
"aria-multiselectable": isMultiselect.value,
|
|
166
180
|
"aria-label": unref(options.label),
|
|
181
|
+
"aria-description": options.description,
|
|
167
182
|
tabindex: "0",
|
|
168
183
|
"aria-activedescendant":
|
|
169
184
|
options.activeOption.value != undefined
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { computed, toRef, toValue, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
+
import { createBuilder, createElRef } from "../../utils/builder";
|
|
3
|
+
import { useDismissible } from "../helpers/useDismissible";
|
|
4
|
+
import { useOutsideClick } from "../helpers/useOutsideClick";
|
|
5
|
+
|
|
6
|
+
export type CreateToggletipOptions = {
|
|
7
|
+
toggleLabel: MaybeRefOrGetter<string>;
|
|
8
|
+
isVisible?: Ref<boolean>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a toggletip as described in https://inclusive-components.design/tooltips-toggletips/
|
|
13
|
+
* Its visibility is toggled on click.
|
|
14
|
+
* Therefore a toggletip MUST NOT be used to describe the associated trigger element.
|
|
15
|
+
* Commonly this pattern uses a button with the ⓘ as the trigger element.
|
|
16
|
+
* To describe the associated element use `createTooltip`.
|
|
17
|
+
*/
|
|
18
|
+
export const createToggletip = createBuilder(
|
|
19
|
+
({ toggleLabel, isVisible }: CreateToggletipOptions) => {
|
|
20
|
+
const triggerRef = createElRef<HTMLButtonElement>();
|
|
21
|
+
const tooltipRef = createElRef<HTMLElement>();
|
|
22
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
23
|
+
|
|
24
|
+
// close tooltip on outside click
|
|
25
|
+
useOutsideClick({
|
|
26
|
+
inside: computed(() => [triggerRef.value, tooltipRef.value]),
|
|
27
|
+
onOutsideClick: () => (_isVisible.value = false),
|
|
28
|
+
disabled: computed(() => !_isVisible.value),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
useDismissible({ isExpanded: _isVisible });
|
|
32
|
+
|
|
33
|
+
const toggle = () => (_isVisible.value = !_isVisible.value);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
elements: {
|
|
37
|
+
/**
|
|
38
|
+
* The element which controls the toggletip visibility:
|
|
39
|
+
* Preferably a `button` element.
|
|
40
|
+
*/
|
|
41
|
+
trigger: computed(() => ({
|
|
42
|
+
ref: triggerRef,
|
|
43
|
+
onClick: toggle,
|
|
44
|
+
"aria-label": toValue(toggleLabel),
|
|
45
|
+
})),
|
|
46
|
+
/**
|
|
47
|
+
* The element with the relevant toggletip content.
|
|
48
|
+
* Only simple, textual content is allowed.
|
|
49
|
+
*/
|
|
50
|
+
tooltip: {
|
|
51
|
+
ref: tooltipRef,
|
|
52
|
+
role: "status",
|
|
53
|
+
tabindex: "-1",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
state: {
|
|
57
|
+
isVisible: _isVisible,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
);
|
|
@@ -1,44 +1,27 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, toRef, toValue, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
2
|
import { createId } from "../..";
|
|
3
|
-
import { createBuilder
|
|
4
|
-
import {
|
|
3
|
+
import { createBuilder } from "../../utils/builder";
|
|
4
|
+
import { useDismissible } from "../helpers/useDismissible";
|
|
5
5
|
|
|
6
6
|
export type CreateTooltipOptions = {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Number of milliseconds to use as debounce when showing/hiding the tooltip.
|
|
9
|
+
*/
|
|
10
|
+
debounce: MaybeRefOrGetter<number>;
|
|
11
|
+
isVisible?: Ref<boolean>;
|
|
8
12
|
};
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
debounce: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const TOOLTIP_TRIGGERS = ["hover", "click"] as const;
|
|
22
|
-
export type TooltipTrigger = (typeof TOOLTIP_TRIGGERS)[number];
|
|
23
|
-
|
|
24
|
-
export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
|
|
25
|
-
const rootRef = createElRef<HTMLElement>();
|
|
14
|
+
/**
|
|
15
|
+
* Create a tooltip as described in https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
|
|
16
|
+
* Its visibility is toggled on hover or focus.
|
|
17
|
+
* A tooltip MUST be used to describe the associated trigger element. E.g. The usage with the ⓘ would be incorrect.
|
|
18
|
+
* To provide contextual information use the `createToggletip`.
|
|
19
|
+
*/
|
|
20
|
+
export const createTooltip = createBuilder(({ debounce, isVisible }: CreateTooltipOptions) => {
|
|
26
21
|
const tooltipId = createId("tooltip");
|
|
27
|
-
const _isVisible =
|
|
22
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
28
23
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
29
24
|
|
|
30
|
-
const debounce = computed(() => {
|
|
31
|
-
const open = unref(options.open);
|
|
32
|
-
if (typeof open !== "object") return 200;
|
|
33
|
-
return open.debounce;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const openType = computed(() => {
|
|
37
|
-
const open = unref(options.open);
|
|
38
|
-
if (typeof open !== "object") return open;
|
|
39
|
-
return open.type;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
25
|
/**
|
|
43
26
|
* Debounced visible state that will only be toggled after a given timeout.
|
|
44
27
|
*/
|
|
@@ -48,82 +31,41 @@ export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
|
|
|
48
31
|
clearTimeout(timeout);
|
|
49
32
|
timeout = setTimeout(() => {
|
|
50
33
|
_isVisible.value = newValue;
|
|
51
|
-
}, debounce
|
|
34
|
+
}, toValue(debounce));
|
|
52
35
|
},
|
|
53
36
|
});
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (typeof openType.value === "boolean") return openType.value;
|
|
61
|
-
return debouncedVisible.value;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Toggles the tooltip if element is clicked.
|
|
66
|
-
*/
|
|
67
|
-
const handleClick = () => {
|
|
68
|
-
_isVisible.value = !_isVisible.value;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const hoverEvents = computed(() => {
|
|
72
|
-
if (openType.value !== "hover") return;
|
|
73
|
-
return {
|
|
74
|
-
onMouseover: () => (debouncedVisible.value = true),
|
|
75
|
-
onMouseout: () => (debouncedVisible.value = false),
|
|
76
|
-
onFocusin: () => (_isVisible.value = true),
|
|
77
|
-
onFocusout: () => (_isVisible.value = false),
|
|
78
|
-
};
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Closes the tooltip if Escape is pressed.
|
|
83
|
-
*/
|
|
84
|
-
const handleDocumentKeydown = (event: KeyboardEvent) => {
|
|
85
|
-
if (event.key !== "Escape") return;
|
|
86
|
-
_isVisible.value = false;
|
|
38
|
+
const hoverEvents = {
|
|
39
|
+
onMouseover: () => (debouncedVisible.value = true),
|
|
40
|
+
onMouseout: () => (debouncedVisible.value = false),
|
|
41
|
+
onFocusin: () => (_isVisible.value = true),
|
|
42
|
+
onFocusout: () => (_isVisible.value = false),
|
|
87
43
|
};
|
|
88
44
|
|
|
89
|
-
|
|
90
|
-
useOutsideClick({
|
|
91
|
-
element: rootRef,
|
|
92
|
-
onOutsideClick: () => (_isVisible.value = false),
|
|
93
|
-
disabled: computed(() => openType.value !== "click"),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// add global document event listeners only on/before mounted to also work in server side rendering
|
|
97
|
-
onBeforeMount(() => {
|
|
98
|
-
document.addEventListener("keydown", handleDocumentKeydown);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Clean up global event listeners to prevent dangling events.
|
|
103
|
-
*/
|
|
104
|
-
onBeforeUnmount(() => {
|
|
105
|
-
document.removeEventListener("keydown", handleDocumentKeydown);
|
|
106
|
-
});
|
|
45
|
+
useDismissible({ isExpanded: _isVisible });
|
|
107
46
|
|
|
108
47
|
return {
|
|
109
48
|
elements: {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
trigger:
|
|
49
|
+
/**
|
|
50
|
+
* The element which controls the tooltip visibility on hover.
|
|
51
|
+
*/
|
|
52
|
+
trigger: {
|
|
114
53
|
"aria-describedby": tooltipId,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
54
|
+
...hoverEvents,
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* The element describing the tooltip.
|
|
58
|
+
* Only simple, textual and non-focusable content is allowed.
|
|
59
|
+
*/
|
|
60
|
+
tooltip: {
|
|
119
61
|
role: "tooltip",
|
|
120
62
|
id: tooltipId,
|
|
121
63
|
tabindex: "-1",
|
|
122
|
-
...hoverEvents
|
|
123
|
-
}
|
|
64
|
+
...hoverEvents,
|
|
65
|
+
},
|
|
124
66
|
},
|
|
125
67
|
state: {
|
|
126
|
-
isVisible,
|
|
68
|
+
isVisible: _isVisible,
|
|
127
69
|
},
|
|
128
70
|
};
|
|
129
71
|
});
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./composables/comboBox/createComboBox";
|
|
|
2
2
|
export * from "./composables/listbox/createListbox";
|
|
3
3
|
export * from "./composables/menuButton/createMenuButton";
|
|
4
4
|
export * from "./composables/navigationMenu/createMenu";
|
|
5
|
+
export * from "./composables/tooltip/createToggletip";
|
|
5
6
|
export * from "./composables/tooltip/createTooltip";
|
|
6
7
|
export * from "./utils/builder";
|
|
7
8
|
export { createId } from "./utils/id";
|
package/src/utils/builder.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import type { IfDefined } from "./types";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Properties as they can be used by `v-bind` on an HTML element.
|
|
15
|
+
* This includes generic html attributes and the vue reserved `ref` property.
|
|
15
16
|
* `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
|
|
16
17
|
*/
|
|
17
18
|
export type VBindAttributes<
|
package/src/utils/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Adds the entry with the key `Key` and the value of type `TValue` to a record when it is defined.
|
|
3
5
|
* Then the entry is either undefined or exists without being optional.
|
|
@@ -21,3 +23,8 @@ export type IfDefined<Key extends string, TValue> =
|
|
|
21
23
|
export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple extends true
|
|
22
24
|
? TValue[]
|
|
23
25
|
: TValue;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type for any kind of ref source. Preferably used in combination with vue's `toValue` method
|
|
29
|
+
*/
|
|
30
|
+
export type MaybeReactiveSource<T> = MaybeRefOrGetter<T> | ComputedRef<T>;
|
package/src/utils/vitest.ts
CHANGED