@sit-onyx/headless 1.0.0-beta.2 → 1.0.0-beta.20
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/README.md +1 -5
- package/package.json +8 -3
- package/src/composables/comboBox/SelectOnlyCombobox.vue +14 -7
- package/src/composables/comboBox/TestCombobox.vue +12 -9
- package/src/composables/comboBox/createComboBox.ts +29 -23
- package/src/composables/helpers/useDismissible.ts +19 -0
- package/src/composables/helpers/useGlobalListener.ts +1 -1
- package/src/composables/helpers/useOutsideClick.spec.ts +117 -0
- package/src/composables/helpers/useOutsideClick.ts +44 -9
- package/src/composables/listbox/TestListbox.vue +2 -0
- package/src/composables/listbox/createListbox.ts +27 -9
- package/src/composables/menuButton/TestMenuButton.vue +3 -2
- package/src/composables/menuButton/createMenuButton.testing.ts +0 -19
- package/src/composables/menuButton/createMenuButton.ts +172 -117
- package/src/composables/navigationMenu/createMenu.testing.ts +2 -13
- package/src/composables/navigationMenu/createMenu.ts +4 -5
- package/src/composables/tabs/TestTabs.ct.tsx +12 -0
- package/src/composables/tabs/TestTabs.vue +28 -0
- package/src/composables/tabs/createTabs.testing.ts +151 -0
- package/src/composables/tabs/createTabs.ts +129 -0
- package/src/composables/tooltip/createToggletip.ts +58 -0
- package/src/composables/tooltip/createTooltip.ts +38 -96
- package/src/index.ts +4 -1
- package/src/playwright.ts +2 -0
- package/src/utils/builder.ts +107 -11
- package/src/utils/timer.ts +10 -3
- package/src/utils/types.ts +10 -0
- package/src/utils/vitest.ts +2 -2
- package/src/utils/id.ts +0 -14
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { computed, unref, useId, type MaybeRef, type Ref } from "vue";
|
|
2
|
+
import { createBuilder } from "../../utils/builder";
|
|
3
|
+
|
|
4
|
+
type CreateTabsOptions<TKey extends PropertyKey = PropertyKey> = {
|
|
5
|
+
/**
|
|
6
|
+
* Label of the tablist.
|
|
7
|
+
*/
|
|
8
|
+
label: MaybeRef<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Currently selected tab.
|
|
11
|
+
*/
|
|
12
|
+
selectedTab: Ref<TKey>;
|
|
13
|
+
/**
|
|
14
|
+
* Called when the user selects a tab.
|
|
15
|
+
*/
|
|
16
|
+
onSelect?: (selectedTabValue: TKey) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Composable for implementing accessible tabs.
|
|
21
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
22
|
+
*/
|
|
23
|
+
export const createTabs = createBuilder(<T extends PropertyKey>(options: CreateTabsOptions<T>) => {
|
|
24
|
+
/**
|
|
25
|
+
* Map for looking up tab and panel IDs for given tab keys/values defined by the user.
|
|
26
|
+
* Key = custom value from the user, value = random generated tab and panel ID
|
|
27
|
+
*/
|
|
28
|
+
const idMap = new Map<T, { tabId: string; panelId: string }>();
|
|
29
|
+
|
|
30
|
+
const getId = (value: T) => {
|
|
31
|
+
if (!idMap.has(value)) {
|
|
32
|
+
idMap.set(value, { tabId: useId(), panelId: useId() });
|
|
33
|
+
}
|
|
34
|
+
return idMap.get(value)!;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
38
|
+
const tab = event.target as Element;
|
|
39
|
+
|
|
40
|
+
const enabledTabs = Array.from(
|
|
41
|
+
tab.parentElement?.querySelectorAll('[role="tab"]') ?? [],
|
|
42
|
+
).filter((tab) => tab.ariaDisabled !== "true");
|
|
43
|
+
|
|
44
|
+
const currentTabIndex = enabledTabs.indexOf(tab);
|
|
45
|
+
|
|
46
|
+
const focusElement = (element?: Element | null) => {
|
|
47
|
+
if (element instanceof HTMLElement) element.focus();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const focusFirstTab = () => focusElement(enabledTabs.at(0));
|
|
51
|
+
const focusLastTab = () => focusElement(enabledTabs.at(-1));
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Focuses the next/previous tab. Will ignore/skip disabled ones.
|
|
55
|
+
*/
|
|
56
|
+
const focusTab = (direction: "next" | "previous") => {
|
|
57
|
+
if (currentTabIndex === -1) return;
|
|
58
|
+
const newIndex = direction === "next" ? currentTabIndex + 1 : currentTabIndex - 1;
|
|
59
|
+
|
|
60
|
+
if (newIndex < 0) {
|
|
61
|
+
return focusLastTab();
|
|
62
|
+
} else if (newIndex >= enabledTabs.length) {
|
|
63
|
+
return focusFirstTab();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return focusElement(enabledTabs.at(newIndex));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
switch (event.key) {
|
|
70
|
+
case "ArrowRight":
|
|
71
|
+
focusTab("next");
|
|
72
|
+
break;
|
|
73
|
+
case "ArrowLeft":
|
|
74
|
+
focusTab("previous");
|
|
75
|
+
break;
|
|
76
|
+
case "Home":
|
|
77
|
+
focusFirstTab();
|
|
78
|
+
break;
|
|
79
|
+
case "End":
|
|
80
|
+
focusLastTab();
|
|
81
|
+
break;
|
|
82
|
+
case "Enter":
|
|
83
|
+
case " ":
|
|
84
|
+
{
|
|
85
|
+
const tabEntry = Array.from(idMap.entries()).find(([, { tabId }]) => tabId === tab.id);
|
|
86
|
+
if (tabEntry) options.onSelect?.(tabEntry[0]);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
elements: {
|
|
94
|
+
tablist: computed(() => ({
|
|
95
|
+
role: "tablist",
|
|
96
|
+
"aria-label": unref(options.label),
|
|
97
|
+
onKeydown: handleKeydown,
|
|
98
|
+
})),
|
|
99
|
+
tab: computed(() => {
|
|
100
|
+
return (data: { value: T; disabled?: boolean }) => {
|
|
101
|
+
const { tabId: selectedTabId } = getId(unref(options.selectedTab));
|
|
102
|
+
const { tabId, panelId } = getId(data.value);
|
|
103
|
+
const isSelected = tabId === selectedTabId;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: tabId,
|
|
107
|
+
role: "tab",
|
|
108
|
+
"aria-selected": isSelected,
|
|
109
|
+
"aria-controls": panelId,
|
|
110
|
+
"aria-disabled": data.disabled ? true : undefined,
|
|
111
|
+
onClick: () => options.onSelect?.(data.value),
|
|
112
|
+
tabindex: isSelected && !data.disabled ? 0 : -1,
|
|
113
|
+
} as const;
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
tabpanel: computed(() => {
|
|
117
|
+
return (data: { value: T }) => {
|
|
118
|
+
const { tabId, panelId } = getId(data.value);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
id: panelId,
|
|
122
|
+
role: "tabpanel",
|
|
123
|
+
"aria-labelledby": tabId,
|
|
124
|
+
} as const;
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { computed, toRef, toValue, useId, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
+
import { createBuilder } from "../../utils/builder";
|
|
3
|
+
import { useDismissible } from "../helpers/useDismissible";
|
|
4
|
+
|
|
5
|
+
export type CreateToggletipOptions = {
|
|
6
|
+
toggleLabel: MaybeRefOrGetter<string>;
|
|
7
|
+
isVisible?: Ref<boolean>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a toggletip as described in https://inclusive-components.design/tooltips-toggletips/
|
|
12
|
+
* Its visibility is toggled on click.
|
|
13
|
+
* Therefore a toggletip MUST NOT be used to describe the associated trigger element.
|
|
14
|
+
* Commonly this pattern uses a button with the ⓘ as the trigger element.
|
|
15
|
+
* To describe the associated element use `createTooltip`.
|
|
16
|
+
*/
|
|
17
|
+
export const createToggletip = createBuilder(
|
|
18
|
+
({ toggleLabel, isVisible }: CreateToggletipOptions) => {
|
|
19
|
+
const triggerId = useId();
|
|
20
|
+
|
|
21
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
22
|
+
|
|
23
|
+
useDismissible({ isExpanded: _isVisible });
|
|
24
|
+
|
|
25
|
+
const toggle = () => (_isVisible.value = !_isVisible.value);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
elements: {
|
|
29
|
+
/**
|
|
30
|
+
* The element which controls the toggletip visibility:
|
|
31
|
+
* Preferably a `button` element.
|
|
32
|
+
*/
|
|
33
|
+
trigger: computed(() => ({
|
|
34
|
+
id: triggerId,
|
|
35
|
+
onClick: toggle,
|
|
36
|
+
"aria-label": toValue(toggleLabel),
|
|
37
|
+
})),
|
|
38
|
+
/**
|
|
39
|
+
* The element with the relevant toggletip content.
|
|
40
|
+
* Only simple, textual content is allowed.
|
|
41
|
+
*/
|
|
42
|
+
tooltip: {
|
|
43
|
+
onToggle: (e: Event) => {
|
|
44
|
+
const tooltip = e.target as HTMLDialogElement;
|
|
45
|
+
_isVisible.value = tooltip.matches(":popover-open");
|
|
46
|
+
},
|
|
47
|
+
anchor: triggerId,
|
|
48
|
+
popover: "auto",
|
|
49
|
+
role: "status",
|
|
50
|
+
tabindex: "-1",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
state: {
|
|
54
|
+
isVisible: _isVisible,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
);
|
|
@@ -1,44 +1,26 @@
|
|
|
1
|
-
import { computed,
|
|
2
|
-
import { createId } from "../..";
|
|
1
|
+
import { computed, toRef, toValue, useId, type MaybeRefOrGetter, type Ref } from "vue";
|
|
3
2
|
import { createBuilder } from "../../utils/builder";
|
|
4
|
-
import {
|
|
3
|
+
import { useDismissible } from "../helpers/useDismissible";
|
|
5
4
|
|
|
6
5
|
export type CreateTooltipOptions = {
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Number of milliseconds to use as debounce when showing/hiding the tooltip.
|
|
8
|
+
*/
|
|
9
|
+
debounce: MaybeRefOrGetter<number>;
|
|
10
|
+
isVisible?: Ref<boolean>;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 = ref<HTMLElement>();
|
|
26
|
-
const tooltipId = createId("tooltip");
|
|
27
|
-
const _isVisible = ref(false);
|
|
13
|
+
/**
|
|
14
|
+
* Create a tooltip as described in https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
|
|
15
|
+
* Its visibility is toggled on hover or focus.
|
|
16
|
+
* A tooltip MUST be used to describe the associated trigger element. E.g. The usage with the ⓘ would be incorrect.
|
|
17
|
+
* To provide contextual information use the `createToggletip`.
|
|
18
|
+
*/
|
|
19
|
+
export const createTooltip = createBuilder(({ debounce, isVisible }: CreateTooltipOptions) => {
|
|
20
|
+
const tooltipId = useId();
|
|
21
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
28
22
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
29
23
|
|
|
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
24
|
/**
|
|
43
25
|
* Debounced visible state that will only be toggled after a given timeout.
|
|
44
26
|
*/
|
|
@@ -48,82 +30,42 @@ export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
|
|
|
48
30
|
clearTimeout(timeout);
|
|
49
31
|
timeout = setTimeout(() => {
|
|
50
32
|
_isVisible.value = newValue;
|
|
51
|
-
}, debounce
|
|
33
|
+
}, toValue(debounce));
|
|
52
34
|
},
|
|
53
35
|
});
|
|
54
36
|
|
|
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;
|
|
37
|
+
const hoverEvents = {
|
|
38
|
+
onMouseover: () => (debouncedVisible.value = true),
|
|
39
|
+
onMouseout: () => (debouncedVisible.value = false),
|
|
40
|
+
onFocusin: () => (_isVisible.value = true),
|
|
41
|
+
onFocusout: () => (_isVisible.value = false),
|
|
87
42
|
};
|
|
88
43
|
|
|
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
|
-
});
|
|
44
|
+
useDismissible({ isExpanded: _isVisible });
|
|
107
45
|
|
|
108
46
|
return {
|
|
109
47
|
elements: {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
trigger:
|
|
48
|
+
/**
|
|
49
|
+
* The element which controls the tooltip visibility on hover.
|
|
50
|
+
*/
|
|
51
|
+
trigger: {
|
|
114
52
|
"aria-describedby": tooltipId,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
53
|
+
...hoverEvents,
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* The element describing the tooltip.
|
|
57
|
+
* Only simple, textual and non-focusable content is allowed.
|
|
58
|
+
*/
|
|
59
|
+
tooltip: {
|
|
60
|
+
popover: "manual",
|
|
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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * from "./composables/comboBox/createComboBox";
|
|
2
|
+
export * from "./composables/helpers/useGlobalListener";
|
|
2
3
|
export * from "./composables/listbox/createListbox";
|
|
3
4
|
export * from "./composables/menuButton/createMenuButton";
|
|
4
5
|
export * from "./composables/navigationMenu/createMenu";
|
|
6
|
+
export * from "./composables/tabs/createTabs";
|
|
7
|
+
export * from "./composables/tooltip/createToggletip";
|
|
5
8
|
export * from "./composables/tooltip/createTooltip";
|
|
6
|
-
export
|
|
9
|
+
export * from "./utils/builder";
|
|
7
10
|
export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard";
|
|
8
11
|
export { debounce } from "./utils/timer";
|
package/src/playwright.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export * from "./composables/comboBox/createComboBox.testing";
|
|
2
2
|
export * from "./composables/listbox/createListbox.testing";
|
|
3
3
|
export * from "./composables/menuButton/createMenuButton.testing";
|
|
4
|
+
export * from "./composables/navigationMenu/createMenu.testing";
|
|
5
|
+
export * from "./composables/tabs/createTabs.testing";
|
package/src/utils/builder.ts
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
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
|
+
* 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.
|
|
16
|
+
* `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
|
|
17
|
+
*/
|
|
18
|
+
export type VBindAttributes<
|
|
19
|
+
A extends HTMLAttributes = HTMLAttributes,
|
|
20
|
+
E extends Element = Element,
|
|
21
|
+
> = A & {
|
|
22
|
+
ref?: VueTemplateRef<E>;
|
|
23
|
+
};
|
|
5
24
|
|
|
6
|
-
export type IteratedHeadlessElementFunc<
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
export type IteratedHeadlessElementFunc<
|
|
26
|
+
A extends HTMLAttributes,
|
|
27
|
+
T extends Record<string, unknown>,
|
|
28
|
+
> = (opts: T) => VBindAttributes<A>;
|
|
9
29
|
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
export type HeadlessElementAttributes<A extends HTMLAttributes> =
|
|
31
|
+
| VBindAttributes<A>
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the specific type doesn't matter here
|
|
33
|
+
| IteratedHeadlessElementFunc<A, any>;
|
|
12
34
|
|
|
13
|
-
export type HeadlessElements = Record<
|
|
14
|
-
string,
|
|
15
|
-
HeadlessElementAttributes | ComputedRef<HeadlessElementAttributes>
|
|
16
|
-
>;
|
|
35
|
+
export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
|
|
17
36
|
|
|
18
37
|
export type HeadlessState = Record<string, Ref>;
|
|
19
38
|
|
|
@@ -28,6 +47,39 @@ export type HeadlessComposable<
|
|
|
28
47
|
|
|
29
48
|
/**
|
|
30
49
|
* We use this identity function to ensure the correct typings of the headless composables
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
|
|
53
|
+
* const tooltipId = useId();
|
|
54
|
+
* const isVisible = ref(initialVisible);
|
|
55
|
+
*
|
|
56
|
+
* const hoverEvents = {
|
|
57
|
+
* onMouseover: () => (isVisible.value = true),
|
|
58
|
+
* onMouseout: () => (isVisible.value = false),
|
|
59
|
+
* onFocusin: () => (isVisible.value = true),
|
|
60
|
+
* onFocusout: () => (isVisible.value = false),
|
|
61
|
+
* };
|
|
62
|
+
*
|
|
63
|
+
* return {
|
|
64
|
+
* elements: {
|
|
65
|
+
* trigger: {
|
|
66
|
+
* "aria-describedby": tooltipId,
|
|
67
|
+
* ...hoverEvents,
|
|
68
|
+
* },
|
|
69
|
+
* tooltip: {
|
|
70
|
+
* role: "tooltip",
|
|
71
|
+
* id: tooltipId,
|
|
72
|
+
* tabindex: "-1",
|
|
73
|
+
* ...hoverEvents,
|
|
74
|
+
* },
|
|
75
|
+
* },
|
|
76
|
+
* state: {
|
|
77
|
+
* isVisible,
|
|
78
|
+
* },
|
|
79
|
+
* };
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* ```
|
|
31
83
|
*/
|
|
32
84
|
export const createBuilder = <
|
|
33
85
|
Args extends unknown[] = unknown[],
|
|
@@ -37,3 +89,47 @@ export const createBuilder = <
|
|
|
37
89
|
>(
|
|
38
90
|
builder: (...args: Args) => HeadlessComposable<Elements, State, Internals>,
|
|
39
91
|
) => builder;
|
|
92
|
+
|
|
93
|
+
type VueTemplateRefElement<E extends Element> = E | (ComponentPublicInstance & { $el: E }) | null;
|
|
94
|
+
type VueTemplateRef<E extends Element> = Ref<VueTemplateRefElement<E>>;
|
|
95
|
+
|
|
96
|
+
declare const HeadlessElRefSymbol: unique symbol;
|
|
97
|
+
type HeadlessElRef<E extends Element> = WritableComputedRef<E> & {
|
|
98
|
+
/**
|
|
99
|
+
* type differentiator
|
|
100
|
+
* ensures that only `createElRef` can be used for headless element ref bindings
|
|
101
|
+
*/
|
|
102
|
+
[HeadlessElRefSymbol]: true;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a special writeable computed that references a DOM Element.
|
|
107
|
+
* Vue Component references will be unwrapped.
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* createBuilder() => {
|
|
111
|
+
* const buttonRef = createElRef<HtmlButtonElement>();
|
|
112
|
+
* return {
|
|
113
|
+
* elements: {
|
|
114
|
+
* button: {
|
|
115
|
+
* ref: buttonRef,
|
|
116
|
+
* },
|
|
117
|
+
* }
|
|
118
|
+
* };
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function createElRef<E extends Element>(): HeadlessElRef<E>;
|
|
123
|
+
export function createElRef<
|
|
124
|
+
E extends Element,
|
|
125
|
+
V extends VueTemplateRefElement<E> = VueTemplateRefElement<E>,
|
|
126
|
+
>() {
|
|
127
|
+
const elementRef = shallowRef<E>();
|
|
128
|
+
|
|
129
|
+
return computed({
|
|
130
|
+
set: (element: V) => {
|
|
131
|
+
elementRef.value = element != null && "$el" in element ? element.$el : (element as E);
|
|
132
|
+
},
|
|
133
|
+
get: () => elementRef.value,
|
|
134
|
+
} as WritableComputedOptions<E>);
|
|
135
|
+
}
|
package/src/utils/timer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toValue, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Debounces a given callback which will only be called when not called for the given timeout.
|
|
3
5
|
*
|
|
@@ -5,11 +7,16 @@
|
|
|
5
7
|
*/
|
|
6
8
|
export const debounce = <TArgs extends unknown[]>(
|
|
7
9
|
handler: (...args: TArgs) => void,
|
|
8
|
-
timeout: number
|
|
10
|
+
timeout: MaybeRefOrGetter<number>,
|
|
9
11
|
) => {
|
|
10
12
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
const func = (...lastArgs: TArgs) => {
|
|
12
15
|
clearTimeout(timer);
|
|
13
|
-
timer = setTimeout(() => handler(...lastArgs), timeout);
|
|
16
|
+
timer = setTimeout(() => handler(...lastArgs), toValue(timeout));
|
|
14
17
|
};
|
|
18
|
+
/** Abort the currently debounced action, if any. */
|
|
19
|
+
func.abort = () => clearTimeout(timer);
|
|
20
|
+
|
|
21
|
+
return func;
|
|
15
22
|
};
|
package/src/utils/types.ts
CHANGED
|
@@ -21,3 +21,13 @@ export type IfDefined<Key extends string, TValue> =
|
|
|
21
21
|
export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple extends true
|
|
22
22
|
? TValue[]
|
|
23
23
|
: TValue;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A type that can be wrapped in an array.
|
|
27
|
+
*/
|
|
28
|
+
export type Arrayable<T> = T | Array<T>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Either the actual value or a nullish one.
|
|
32
|
+
*/
|
|
33
|
+
export type Nullable<T = never> = T | null | undefined;
|
package/src/utils/vitest.ts
CHANGED
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()}`;
|