@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.
- package/README.md +2 -8
- package/dist/composables/comboBox/SelectOnlyCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/TestCombobox.ct.d.ts +1 -0
- package/dist/composables/comboBox/TestCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/createComboBox.d.ts +370 -0
- package/dist/composables/comboBox/createComboBox.testing.d.ts +10 -0
- package/dist/composables/helpers/useDismissible.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.spec.d.ts +1 -0
- package/dist/composables/helpers/useOutsideClick.d.ts +26 -0
- package/dist/composables/helpers/useOutsideClick.spec.d.ts +1 -0
- package/dist/composables/helpers/useTypeAhead.d.ts +11 -0
- package/dist/composables/helpers/useTypeAhead.spec.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.ct.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.d.vue.ts +2 -0
- package/dist/composables/listbox/createListbox.d.ts +102 -0
- package/dist/composables/listbox/createListbox.testing.d.ts +24 -0
- package/dist/composables/menuButton/TestMenuButton.ct.d.ts +1 -0
- package/dist/composables/menuButton/TestMenuButton.d.vue.ts +2 -0
- package/dist/composables/menuButton/createMenuButton.d.ts +78 -0
- package/dist/composables/menuButton/createMenuButton.testing.d.ts +24 -0
- package/dist/composables/navigationMenu/TestMenu.ct.d.ts +1 -0
- package/dist/composables/navigationMenu/TestMenu.d.vue.ts +2 -0
- package/dist/composables/navigationMenu/createMenu.d.ts +21 -0
- package/dist/composables/navigationMenu/createMenu.testing.d.ts +16 -0
- package/dist/composables/tabs/TestTabs.ct.d.ts +1 -0
- package/dist/composables/tabs/TestTabs.d.vue.ts +2 -0
- package/dist/composables/tabs/createTabs.d.ts +48 -0
- package/dist/composables/tabs/createTabs.testing.d.ts +13 -0
- package/dist/composables/tooltip/createToggletip.d.ts +36 -0
- package/dist/composables/tooltip/createTooltip.d.ts +42 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +780 -0
- package/dist/playwright.d.ts +5 -0
- package/dist/playwright.js +369 -0
- package/dist/utils/builder.d.ts +85 -0
- package/dist/utils/keyboard.d.ts +17 -0
- package/dist/utils/keyboard.spec.d.ts +1 -0
- package/dist/utils/math.d.ts +6 -0
- package/dist/utils/math.spec.d.ts +1 -0
- package/dist/utils/object.d.ts +5 -0
- package/dist/utils/object.spec.d.ts +1 -0
- package/dist/utils/timer.d.ts +10 -0
- package/dist/utils/types.d.ts +25 -0
- package/dist/utils/vitest.d.ts +12 -0
- package/package.json +24 -8
- package/src/composables/comboBox/TestCombobox.ct.tsx +0 -13
- package/src/composables/comboBox/TestCombobox.vue +0 -76
- package/src/composables/comboBox/createComboBox.ct.ts +0 -72
- package/src/composables/comboBox/createComboBox.ts +0 -163
- package/src/composables/listbox/TestListbox.ct.tsx +0 -17
- package/src/composables/listbox/TestListbox.vue +0 -91
- package/src/composables/listbox/createListbox.ct.ts +0 -141
- package/src/composables/listbox/createListbox.ts +0 -209
- package/src/composables/tooltip/createTooltip.ts +0 -150
- package/src/composables/typeAhead.spec.ts +0 -29
- package/src/composables/typeAhead.ts +0 -26
- package/src/index.ts +0 -4
- package/src/playwright.ts +0 -2
- package/src/utils/builder.ts +0 -37
- package/src/utils/id.ts +0 -14
- package/src/utils/keyboard.spec.ts +0 -18
- package/src/utils/keyboard.ts +0 -328
- package/src/utils/timer.ts +0 -15
- package/src/utils/types.ts +0 -8
package/dist/index.js
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
import { shallowRef, computed, reactive, onBeforeMount, watchEffect, onBeforeUnmount, toValue, unref, ref, nextTick, useId, watch, toRef } from "vue";
|
|
2
|
+
const createBuilder = (builder) => builder;
|
|
3
|
+
function createElRef() {
|
|
4
|
+
const elementRef = shallowRef();
|
|
5
|
+
return computed({
|
|
6
|
+
set: (element) => {
|
|
7
|
+
elementRef.value = element != null && "$el" in element ? element.$el : element;
|
|
8
|
+
},
|
|
9
|
+
get: () => elementRef.value
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
const isSubsetMatching = (subset, target) => Object.entries(subset).every(
|
|
13
|
+
([key, value]) => target[key] === value
|
|
14
|
+
);
|
|
15
|
+
const wasKeyPressed = (event, key) => {
|
|
16
|
+
if (typeof key === "string") {
|
|
17
|
+
return event.key === key;
|
|
18
|
+
}
|
|
19
|
+
return isSubsetMatching(
|
|
20
|
+
{ altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...key },
|
|
21
|
+
event
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter("en-US");
|
|
25
|
+
const isPrintableCharacter = (key) => [...GRAPHEME_SEGMENTER.segment(key)].length === 1;
|
|
26
|
+
const GLOBAL_LISTENERS = reactive(/* @__PURE__ */ new Map());
|
|
27
|
+
const updateRemainingListeners = (type, remaining) => {
|
|
28
|
+
if (remaining?.size) {
|
|
29
|
+
GLOBAL_LISTENERS.set(type, remaining);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
GLOBAL_LISTENERS.delete(type);
|
|
33
|
+
document.removeEventListener(type, GLOBAL_HANDLER);
|
|
34
|
+
};
|
|
35
|
+
const removeGlobalListener = (type, listener) => {
|
|
36
|
+
const globalListener = GLOBAL_LISTENERS.get(type);
|
|
37
|
+
globalListener?.delete(listener);
|
|
38
|
+
updateRemainingListeners(type, globalListener);
|
|
39
|
+
};
|
|
40
|
+
const addGlobalListener = (type, listener) => {
|
|
41
|
+
const globalListener = GLOBAL_LISTENERS.get(type) ?? /* @__PURE__ */ new Set();
|
|
42
|
+
globalListener.add(listener);
|
|
43
|
+
GLOBAL_LISTENERS.set(type, globalListener);
|
|
44
|
+
document.addEventListener(type, GLOBAL_HANDLER);
|
|
45
|
+
};
|
|
46
|
+
const GLOBAL_HANDLER = (event) => {
|
|
47
|
+
const type = event.type;
|
|
48
|
+
GLOBAL_LISTENERS.get(type)?.forEach((cb) => cb(event));
|
|
49
|
+
};
|
|
50
|
+
const useGlobalEventListener = ({
|
|
51
|
+
type,
|
|
52
|
+
listener,
|
|
53
|
+
disabled
|
|
54
|
+
}) => {
|
|
55
|
+
onBeforeMount(
|
|
56
|
+
() => watchEffect(
|
|
57
|
+
() => disabled?.value ? removeGlobalListener(type, listener) : addGlobalListener(type, listener)
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
onBeforeUnmount(() => removeGlobalListener(type, listener));
|
|
61
|
+
};
|
|
62
|
+
const useOutsideClick = ({
|
|
63
|
+
inside,
|
|
64
|
+
onOutsideClick,
|
|
65
|
+
disabled,
|
|
66
|
+
checkOnTab
|
|
67
|
+
}) => {
|
|
68
|
+
const isOutsideClick = (target) => {
|
|
69
|
+
if (!target) return true;
|
|
70
|
+
const raw = toValue(inside);
|
|
71
|
+
const elements = Array.isArray(raw) ? raw : [raw];
|
|
72
|
+
return !elements.some((element) => element?.contains(target));
|
|
73
|
+
};
|
|
74
|
+
const clickListener = (event) => {
|
|
75
|
+
if (isOutsideClick(event.target)) onOutsideClick(event);
|
|
76
|
+
};
|
|
77
|
+
useGlobalEventListener({ type: "mousedown", listener: clickListener, disabled });
|
|
78
|
+
if (checkOnTab) {
|
|
79
|
+
const keydownListener = async (event) => {
|
|
80
|
+
if (event.key !== "Tab") return;
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
82
|
+
if (isOutsideClick(document.activeElement)) {
|
|
83
|
+
onOutsideClick(event);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
useGlobalEventListener({ type: "keydown", listener: keydownListener, disabled });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const debounce = (handler, timeout) => {
|
|
90
|
+
let timer;
|
|
91
|
+
const func = (...lastArgs) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
timer = setTimeout(() => handler(...lastArgs), toValue(timeout));
|
|
94
|
+
};
|
|
95
|
+
func.abort = () => clearTimeout(timer);
|
|
96
|
+
return func;
|
|
97
|
+
};
|
|
98
|
+
const useTypeAhead = (callback, timeout = 500) => {
|
|
99
|
+
let inputString = "";
|
|
100
|
+
const debouncedReset = debounce(() => inputString = "", timeout);
|
|
101
|
+
return (event) => {
|
|
102
|
+
if (!isPrintableCharacter(event.key)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
debouncedReset();
|
|
106
|
+
inputString = `${inputString}${event.key}`;
|
|
107
|
+
callback(inputString);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const createListbox = createBuilder(
|
|
111
|
+
(options) => {
|
|
112
|
+
const isMultiselect = computed(() => unref(options.multiple) ?? false);
|
|
113
|
+
const isExpanded = computed(() => unref(options.isExpanded) ?? false);
|
|
114
|
+
const descendantKeyIdMap = /* @__PURE__ */ new Map();
|
|
115
|
+
const getOptionId = (value) => {
|
|
116
|
+
if (!descendantKeyIdMap.has(value)) {
|
|
117
|
+
descendantKeyIdMap.set(value, useId());
|
|
118
|
+
}
|
|
119
|
+
return descendantKeyIdMap.get(value);
|
|
120
|
+
};
|
|
121
|
+
const isFocused = ref(false);
|
|
122
|
+
watchEffect(async () => {
|
|
123
|
+
if (!isExpanded.value || options.activeOption.value == void 0 || !isFocused.value && !options.controlled) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const id = getOptionId(options.activeOption.value);
|
|
127
|
+
await nextTick();
|
|
128
|
+
document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
129
|
+
});
|
|
130
|
+
const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
|
|
131
|
+
const handleKeydown = (event) => {
|
|
132
|
+
switch (event.key) {
|
|
133
|
+
case " ":
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
if (options.activeOption.value != void 0) {
|
|
136
|
+
options.onSelect?.(options.activeOption.value);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case "ArrowUp":
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
if (options.activeOption.value == void 0) {
|
|
142
|
+
options.onActivateLast?.();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
options.onActivatePrevious?.(options.activeOption.value);
|
|
146
|
+
break;
|
|
147
|
+
case "ArrowDown":
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
if (options.activeOption.value == void 0) {
|
|
150
|
+
options.onActivateFirst?.();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
options.onActivateNext?.(options.activeOption.value);
|
|
154
|
+
break;
|
|
155
|
+
case "Home":
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
options.onActivateFirst?.();
|
|
158
|
+
break;
|
|
159
|
+
case "End":
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
options.onActivateLast?.();
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
typeAhead(event);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const listbox = computed(
|
|
168
|
+
() => options.controlled ? {
|
|
169
|
+
role: "listbox",
|
|
170
|
+
"aria-multiselectable": isMultiselect.value,
|
|
171
|
+
"aria-label": unref(options.label),
|
|
172
|
+
"aria-description": options.description,
|
|
173
|
+
tabindex: "-1"
|
|
174
|
+
} : {
|
|
175
|
+
role: "listbox",
|
|
176
|
+
"aria-multiselectable": isMultiselect.value,
|
|
177
|
+
"aria-label": unref(options.label),
|
|
178
|
+
"aria-description": options.description,
|
|
179
|
+
tabindex: "0",
|
|
180
|
+
"aria-activedescendant": options.activeOption.value != void 0 ? getOptionId(options.activeOption.value) : void 0,
|
|
181
|
+
onFocus: () => isFocused.value = true,
|
|
182
|
+
onBlur: () => isFocused.value = false,
|
|
183
|
+
onKeydown: handleKeydown
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
elements: {
|
|
188
|
+
listbox,
|
|
189
|
+
group: computed(() => {
|
|
190
|
+
return (options2) => ({
|
|
191
|
+
role: "group",
|
|
192
|
+
"aria-label": options2.label
|
|
193
|
+
});
|
|
194
|
+
}),
|
|
195
|
+
option: computed(() => {
|
|
196
|
+
return (data) => {
|
|
197
|
+
const selected = data.selected ?? false;
|
|
198
|
+
return {
|
|
199
|
+
id: getOptionId(data.value),
|
|
200
|
+
role: "option",
|
|
201
|
+
"aria-label": data.label,
|
|
202
|
+
"aria-disabled": data.disabled,
|
|
203
|
+
"aria-checked": isMultiselect.value ? selected : void 0,
|
|
204
|
+
"aria-selected": !isMultiselect.value ? selected : void 0,
|
|
205
|
+
onClick: () => !data.disabled && options.onSelect?.(data.value)
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
state: {
|
|
211
|
+
isFocused
|
|
212
|
+
},
|
|
213
|
+
internals: {
|
|
214
|
+
getOptionId
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
const OPENING_KEYS = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
|
|
220
|
+
const CLOSING_KEYS = [
|
|
221
|
+
"Escape",
|
|
222
|
+
{ key: "ArrowUp", altKey: true },
|
|
223
|
+
"Enter",
|
|
224
|
+
"Tab"
|
|
225
|
+
];
|
|
226
|
+
const SELECTING_KEYS = ["Enter"];
|
|
227
|
+
const isSelectingKey = (event, withSpace) => {
|
|
228
|
+
const selectingKeys = withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS;
|
|
229
|
+
return isKeyOfGroup(event, selectingKeys);
|
|
230
|
+
};
|
|
231
|
+
const isKeyOfGroup = (event, group) => group.some((key) => wasKeyPressed(event, key));
|
|
232
|
+
const createComboBox = createBuilder(
|
|
233
|
+
({
|
|
234
|
+
autocomplete: autocompleteRef,
|
|
235
|
+
onAutocomplete,
|
|
236
|
+
onTypeAhead,
|
|
237
|
+
multiple: multipleRef,
|
|
238
|
+
label,
|
|
239
|
+
listLabel,
|
|
240
|
+
listDescription,
|
|
241
|
+
isExpanded: isExpandedRef,
|
|
242
|
+
activeOption,
|
|
243
|
+
onToggle,
|
|
244
|
+
onSelect,
|
|
245
|
+
onActivateFirst,
|
|
246
|
+
onActivateLast,
|
|
247
|
+
onActivateNext,
|
|
248
|
+
onActivatePrevious,
|
|
249
|
+
templateRef
|
|
250
|
+
}) => {
|
|
251
|
+
const controlsId = useId();
|
|
252
|
+
const autocomplete = computed(() => unref(autocompleteRef));
|
|
253
|
+
const isExpanded = computed(() => unref(isExpandedRef));
|
|
254
|
+
const multiple = computed(() => unref(multipleRef));
|
|
255
|
+
const handleInput = (event) => {
|
|
256
|
+
const inputElement = event.target;
|
|
257
|
+
if (autocomplete.value !== "none") {
|
|
258
|
+
onAutocomplete?.(inputElement.value);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const typeAhead = useTypeAhead((inputString) => onTypeAhead?.(inputString));
|
|
262
|
+
const handleSelect = (value) => {
|
|
263
|
+
onSelect?.(value);
|
|
264
|
+
if (!unref(multiple)) {
|
|
265
|
+
onToggle?.();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const handleNavigation = (event) => {
|
|
269
|
+
switch (event.key) {
|
|
270
|
+
case "ArrowUp":
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
if (activeOption.value == void 0) {
|
|
273
|
+
return onActivateLast?.();
|
|
274
|
+
}
|
|
275
|
+
onActivatePrevious?.(activeOption.value);
|
|
276
|
+
break;
|
|
277
|
+
case "ArrowDown":
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
if (activeOption.value == void 0) {
|
|
280
|
+
return onActivateFirst?.();
|
|
281
|
+
}
|
|
282
|
+
onActivateNext?.(activeOption.value);
|
|
283
|
+
break;
|
|
284
|
+
case "Home":
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
onActivateFirst?.();
|
|
287
|
+
break;
|
|
288
|
+
case "End":
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
onActivateLast?.();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const handleKeydown = (event) => {
|
|
295
|
+
if (event.key === "Enter") {
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
}
|
|
298
|
+
if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
|
|
299
|
+
onToggle?.();
|
|
300
|
+
if (event.key === " ") {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
}
|
|
303
|
+
if (event.key === "End") {
|
|
304
|
+
return onActivateLast?.();
|
|
305
|
+
}
|
|
306
|
+
return onActivateFirst?.();
|
|
307
|
+
}
|
|
308
|
+
if (isSelectingKey(event, autocomplete.value === "none")) {
|
|
309
|
+
return handleSelect(activeOption.value);
|
|
310
|
+
}
|
|
311
|
+
if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) {
|
|
312
|
+
return onToggle?.();
|
|
313
|
+
}
|
|
314
|
+
if (autocomplete.value === "none" && isPrintableCharacter(event.key)) {
|
|
315
|
+
!isExpanded.value && onToggle?.();
|
|
316
|
+
return typeAhead(event);
|
|
317
|
+
}
|
|
318
|
+
if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
|
|
319
|
+
!isExpanded.value && onToggle?.();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
return handleNavigation(event);
|
|
323
|
+
};
|
|
324
|
+
const autocompleteInput = computed(() => {
|
|
325
|
+
if (autocomplete.value === "none") return null;
|
|
326
|
+
return {
|
|
327
|
+
"aria-autocomplete": autocomplete.value,
|
|
328
|
+
type: "text"
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
const {
|
|
332
|
+
elements: { option, group, listbox },
|
|
333
|
+
internals: { getOptionId }
|
|
334
|
+
} = createListbox({
|
|
335
|
+
label: listLabel,
|
|
336
|
+
description: listDescription,
|
|
337
|
+
multiple,
|
|
338
|
+
controlled: true,
|
|
339
|
+
activeOption,
|
|
340
|
+
isExpanded,
|
|
341
|
+
onSelect: handleSelect
|
|
342
|
+
});
|
|
343
|
+
useOutsideClick({
|
|
344
|
+
inside: templateRef,
|
|
345
|
+
onOutsideClick() {
|
|
346
|
+
if (!isExpanded.value) return;
|
|
347
|
+
onToggle?.(true);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
elements: {
|
|
352
|
+
option,
|
|
353
|
+
group,
|
|
354
|
+
/**
|
|
355
|
+
* The listbox associated with the combobox.
|
|
356
|
+
*/
|
|
357
|
+
listbox: computed(() => ({
|
|
358
|
+
...listbox.value,
|
|
359
|
+
id: controlsId,
|
|
360
|
+
// preventDefault to not lose focus of the combobox
|
|
361
|
+
onMousedown: (e) => e.preventDefault()
|
|
362
|
+
})),
|
|
363
|
+
/**
|
|
364
|
+
* An input that controls another element, that can dynamically pop-up to help the user set the value of the input.
|
|
365
|
+
* 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.
|
|
366
|
+
*/
|
|
367
|
+
input: computed(() => ({
|
|
368
|
+
role: "combobox",
|
|
369
|
+
"aria-expanded": isExpanded.value,
|
|
370
|
+
"aria-controls": controlsId,
|
|
371
|
+
"aria-label": unref(label),
|
|
372
|
+
"aria-activedescendant": activeOption.value != void 0 ? getOptionId(activeOption.value) : void 0,
|
|
373
|
+
onInput: handleInput,
|
|
374
|
+
onKeydown: handleKeydown,
|
|
375
|
+
...autocompleteInput.value
|
|
376
|
+
})),
|
|
377
|
+
/**
|
|
378
|
+
* An optional button to control the visibility of the popup.
|
|
379
|
+
*/
|
|
380
|
+
button: computed(() => ({
|
|
381
|
+
tabindex: "-1",
|
|
382
|
+
onClick: () => onToggle?.()
|
|
383
|
+
}))
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
const createMenuButton = createBuilder((options) => {
|
|
389
|
+
const rootId = useId();
|
|
390
|
+
const menuId = useId();
|
|
391
|
+
const rootRef = createElRef();
|
|
392
|
+
const menuRef = createElRef();
|
|
393
|
+
const buttonId = useId();
|
|
394
|
+
const position = computed(() => toValue(options.position) ?? "bottom");
|
|
395
|
+
useGlobalEventListener({
|
|
396
|
+
type: "keydown",
|
|
397
|
+
listener: (e) => e.key === "Escape" && setExpanded(false),
|
|
398
|
+
disabled: computed(() => !options.isExpanded.value)
|
|
399
|
+
});
|
|
400
|
+
const updateDebouncedExpanded = debounce(() => options.onToggle(), 200);
|
|
401
|
+
watch(options.isExpanded, () => updateDebouncedExpanded.abort());
|
|
402
|
+
const setExpanded = (expanded, debounced = false) => {
|
|
403
|
+
if (options.disabled?.value) return;
|
|
404
|
+
if (expanded === options.isExpanded.value) {
|
|
405
|
+
updateDebouncedExpanded.abort();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (debounced) {
|
|
409
|
+
updateDebouncedExpanded();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
options.onToggle();
|
|
413
|
+
};
|
|
414
|
+
const focusRelativeItem = (next) => {
|
|
415
|
+
const currentMenuItem = document.activeElement;
|
|
416
|
+
const currentMenu = currentMenuItem?.closest('[role="menu"]') || menuRef.value;
|
|
417
|
+
if (!currentMenu) return;
|
|
418
|
+
const menuItems = Array.from(currentMenu.querySelectorAll('[role="menuitem"]')).filter((item) => item.closest('[role="menu"]') === currentMenu);
|
|
419
|
+
if (position.value === "top") menuItems.reverse();
|
|
420
|
+
let nextIndex = 0;
|
|
421
|
+
if (currentMenuItem) {
|
|
422
|
+
const currentIndex = menuItems.indexOf(currentMenuItem);
|
|
423
|
+
switch (next) {
|
|
424
|
+
case "next":
|
|
425
|
+
nextIndex = currentIndex + 1;
|
|
426
|
+
break;
|
|
427
|
+
case "prev":
|
|
428
|
+
nextIndex = currentIndex - 1;
|
|
429
|
+
break;
|
|
430
|
+
case "first":
|
|
431
|
+
nextIndex = 0;
|
|
432
|
+
break;
|
|
433
|
+
case "last":
|
|
434
|
+
nextIndex = menuItems.length - 1;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const nextMenuItem = menuItems[nextIndex];
|
|
439
|
+
nextMenuItem?.focus();
|
|
440
|
+
};
|
|
441
|
+
const handleKeydown = (event) => {
|
|
442
|
+
switch (event.key) {
|
|
443
|
+
case "ArrowDown":
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
focusRelativeItem(position.value === "bottom" ? "next" : "prev");
|
|
446
|
+
break;
|
|
447
|
+
case "ArrowUp":
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
focusRelativeItem(position.value === "bottom" ? "prev" : "next");
|
|
450
|
+
break;
|
|
451
|
+
case "Home":
|
|
452
|
+
event.preventDefault();
|
|
453
|
+
focusRelativeItem("first");
|
|
454
|
+
break;
|
|
455
|
+
case "End":
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
focusRelativeItem("last");
|
|
458
|
+
break;
|
|
459
|
+
case " ":
|
|
460
|
+
case "Enter":
|
|
461
|
+
if (event.target instanceof HTMLInputElement) break;
|
|
462
|
+
event.preventDefault();
|
|
463
|
+
event.target.click();
|
|
464
|
+
break;
|
|
465
|
+
case "Escape":
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
setExpanded(false);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
const triggerEvents = computed(() => {
|
|
472
|
+
if (toValue(options.trigger) !== "hover") return;
|
|
473
|
+
return {
|
|
474
|
+
onMouseenter: () => setExpanded(true),
|
|
475
|
+
onMouseleave: () => setExpanded(false, true)
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
useOutsideClick({
|
|
479
|
+
inside: rootRef,
|
|
480
|
+
onOutsideClick: () => setExpanded(false),
|
|
481
|
+
disabled: computed(() => !options.isExpanded.value),
|
|
482
|
+
checkOnTab: true
|
|
483
|
+
});
|
|
484
|
+
return {
|
|
485
|
+
elements: {
|
|
486
|
+
root: computed(() => ({
|
|
487
|
+
id: rootId,
|
|
488
|
+
onKeydown: handleKeydown,
|
|
489
|
+
ref: rootRef,
|
|
490
|
+
...triggerEvents.value
|
|
491
|
+
})),
|
|
492
|
+
button: computed(
|
|
493
|
+
() => ({
|
|
494
|
+
"aria-controls": menuId,
|
|
495
|
+
"aria-expanded": options.isExpanded.value,
|
|
496
|
+
"aria-haspopup": true,
|
|
497
|
+
onFocus: () => setExpanded(true, true),
|
|
498
|
+
onClick: () => toValue(options.trigger) == "click" ? setExpanded(!options.isExpanded.value) : void 0,
|
|
499
|
+
id: buttonId,
|
|
500
|
+
disabled: options.disabled?.value
|
|
501
|
+
})
|
|
502
|
+
),
|
|
503
|
+
menu: {
|
|
504
|
+
id: menuId,
|
|
505
|
+
ref: menuRef,
|
|
506
|
+
role: "menu",
|
|
507
|
+
"aria-labelledby": buttonId,
|
|
508
|
+
onClick: () => setExpanded(false)
|
|
509
|
+
},
|
|
510
|
+
...createMenuItems().elements
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
const createMenuItems = createBuilder((options) => {
|
|
515
|
+
const onKeydown = (event) => {
|
|
516
|
+
switch (event.key) {
|
|
517
|
+
case "ArrowRight":
|
|
518
|
+
case " ":
|
|
519
|
+
case "Enter":
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
options?.onOpen?.();
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
elements: {
|
|
527
|
+
listItem: {
|
|
528
|
+
role: "none"
|
|
529
|
+
},
|
|
530
|
+
menuItem: (data) => ({
|
|
531
|
+
"aria-current": data.active ? "page" : void 0,
|
|
532
|
+
"aria-disabled": data.disabled,
|
|
533
|
+
role: "menuitem",
|
|
534
|
+
onKeydown
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
const MathUtils = {
|
|
540
|
+
/**
|
|
541
|
+
* Ensures that a given `number` is or is between a given `min` and `max`.
|
|
542
|
+
*/
|
|
543
|
+
clamp: (number, min, max) => Math.max(Math.min(number, max), min)
|
|
544
|
+
};
|
|
545
|
+
const createNavigationMenu = createBuilder(({ navigationName }) => {
|
|
546
|
+
const navId = useId();
|
|
547
|
+
const getMenuButtons = () => {
|
|
548
|
+
const nav = navId ? document.getElementById(navId) : void 0;
|
|
549
|
+
if (!nav) return [];
|
|
550
|
+
return Array.from(nav.querySelectorAll("button[aria-expanded][aria-controls]"));
|
|
551
|
+
};
|
|
552
|
+
const focusRelative = (trigger, next) => {
|
|
553
|
+
const menuButtons = getMenuButtons();
|
|
554
|
+
const index = menuButtons.indexOf(trigger);
|
|
555
|
+
if (index === -1) return;
|
|
556
|
+
const nextIndex = MathUtils.clamp(
|
|
557
|
+
index + (next === "next" ? 1 : -1),
|
|
558
|
+
0,
|
|
559
|
+
menuButtons.length - 1
|
|
560
|
+
);
|
|
561
|
+
menuButtons[nextIndex]?.focus();
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
elements: {
|
|
565
|
+
nav: {
|
|
566
|
+
"aria-label": unref(navigationName),
|
|
567
|
+
id: navId,
|
|
568
|
+
onKeydown: (event) => {
|
|
569
|
+
switch (event.key) {
|
|
570
|
+
case "ArrowRight":
|
|
571
|
+
focusRelative(event.target, "next");
|
|
572
|
+
break;
|
|
573
|
+
case "ArrowLeft":
|
|
574
|
+
focusRelative(event.target, "previous");
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
const createTabs = createBuilder((options) => {
|
|
583
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
584
|
+
const getId = (value) => {
|
|
585
|
+
if (!idMap.has(value)) {
|
|
586
|
+
idMap.set(value, { tabId: useId(), panelId: useId() });
|
|
587
|
+
}
|
|
588
|
+
return idMap.get(value);
|
|
589
|
+
};
|
|
590
|
+
const handleKeydown = (event) => {
|
|
591
|
+
const tab = event.target;
|
|
592
|
+
const enabledTabs = Array.from(
|
|
593
|
+
tab.parentElement?.querySelectorAll('[role="tab"]') ?? []
|
|
594
|
+
).filter((tab2) => tab2.ariaDisabled !== "true");
|
|
595
|
+
const currentTabIndex = enabledTabs.indexOf(tab);
|
|
596
|
+
const focusElement = (element) => {
|
|
597
|
+
if (element instanceof HTMLElement) element.focus();
|
|
598
|
+
};
|
|
599
|
+
const focusFirstTab = () => focusElement(enabledTabs.at(0));
|
|
600
|
+
const focusLastTab = () => focusElement(enabledTabs.at(-1));
|
|
601
|
+
const focusTab = (direction) => {
|
|
602
|
+
if (currentTabIndex === -1) return;
|
|
603
|
+
const newIndex = direction === "next" ? currentTabIndex + 1 : currentTabIndex - 1;
|
|
604
|
+
if (newIndex < 0) {
|
|
605
|
+
return focusLastTab();
|
|
606
|
+
} else if (newIndex >= enabledTabs.length) {
|
|
607
|
+
return focusFirstTab();
|
|
608
|
+
}
|
|
609
|
+
return focusElement(enabledTabs.at(newIndex));
|
|
610
|
+
};
|
|
611
|
+
switch (event.key) {
|
|
612
|
+
case "ArrowRight":
|
|
613
|
+
focusTab("next");
|
|
614
|
+
break;
|
|
615
|
+
case "ArrowLeft":
|
|
616
|
+
focusTab("previous");
|
|
617
|
+
break;
|
|
618
|
+
case "Home":
|
|
619
|
+
focusFirstTab();
|
|
620
|
+
break;
|
|
621
|
+
case "End":
|
|
622
|
+
focusLastTab();
|
|
623
|
+
break;
|
|
624
|
+
case "Enter":
|
|
625
|
+
case " ":
|
|
626
|
+
{
|
|
627
|
+
const tabEntry = Array.from(idMap.entries()).find(([, { tabId }]) => tabId === tab.id);
|
|
628
|
+
if (tabEntry) options.onSelect?.(tabEntry[0]);
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
elements: {
|
|
635
|
+
tablist: computed(() => ({
|
|
636
|
+
role: "tablist",
|
|
637
|
+
"aria-label": unref(options.label),
|
|
638
|
+
onKeydown: handleKeydown
|
|
639
|
+
})),
|
|
640
|
+
tab: computed(() => {
|
|
641
|
+
return (data) => {
|
|
642
|
+
const { tabId: selectedTabId } = getId(unref(options.selectedTab));
|
|
643
|
+
const { tabId, panelId } = getId(data.value);
|
|
644
|
+
const isSelected = tabId === selectedTabId;
|
|
645
|
+
return {
|
|
646
|
+
id: tabId,
|
|
647
|
+
role: "tab",
|
|
648
|
+
"aria-selected": isSelected,
|
|
649
|
+
"aria-controls": panelId,
|
|
650
|
+
"aria-disabled": data.disabled ? true : void 0,
|
|
651
|
+
onClick: () => options.onSelect?.(data.value),
|
|
652
|
+
tabindex: isSelected && !data.disabled ? 0 : -1
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
}),
|
|
656
|
+
tabpanel: computed(() => {
|
|
657
|
+
return (data) => {
|
|
658
|
+
const { tabId, panelId } = getId(data.value);
|
|
659
|
+
return {
|
|
660
|
+
id: panelId,
|
|
661
|
+
role: "tabpanel",
|
|
662
|
+
"aria-labelledby": tabId
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
const useDismissible = ({ isExpanded }) => useGlobalEventListener({
|
|
670
|
+
type: "keydown",
|
|
671
|
+
listener: (e) => {
|
|
672
|
+
if (e.key === "Escape") {
|
|
673
|
+
isExpanded.value = false;
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
disabled: computed(() => !isExpanded.value)
|
|
677
|
+
});
|
|
678
|
+
const createToggletip = createBuilder(
|
|
679
|
+
({ toggleLabel, isVisible }) => {
|
|
680
|
+
const triggerId = useId();
|
|
681
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
682
|
+
useDismissible({ isExpanded: _isVisible });
|
|
683
|
+
const toggle = () => _isVisible.value = !_isVisible.value;
|
|
684
|
+
return {
|
|
685
|
+
elements: {
|
|
686
|
+
/**
|
|
687
|
+
* The element which controls the toggletip visibility:
|
|
688
|
+
* Preferably a `button` element.
|
|
689
|
+
*/
|
|
690
|
+
trigger: computed(() => ({
|
|
691
|
+
id: triggerId,
|
|
692
|
+
onClick: toggle,
|
|
693
|
+
"aria-label": toValue(toggleLabel)
|
|
694
|
+
})),
|
|
695
|
+
/**
|
|
696
|
+
* The element with the relevant toggletip content.
|
|
697
|
+
* Only simple, textual content is allowed.
|
|
698
|
+
*/
|
|
699
|
+
tooltip: {
|
|
700
|
+
onToggle: (e) => {
|
|
701
|
+
const tooltip = e.target;
|
|
702
|
+
_isVisible.value = tooltip.matches(":popover-open");
|
|
703
|
+
},
|
|
704
|
+
anchor: triggerId,
|
|
705
|
+
popover: "auto",
|
|
706
|
+
role: "status",
|
|
707
|
+
tabindex: "-1"
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
state: {
|
|
711
|
+
isVisible: _isVisible
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
);
|
|
716
|
+
const createTooltip = createBuilder(({ debounce: debounce2, isVisible }) => {
|
|
717
|
+
const tooltipId = useId();
|
|
718
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
719
|
+
let timeout;
|
|
720
|
+
const debouncedVisible = computed({
|
|
721
|
+
get: () => _isVisible.value,
|
|
722
|
+
set: (newValue) => {
|
|
723
|
+
clearTimeout(timeout);
|
|
724
|
+
timeout = setTimeout(() => {
|
|
725
|
+
_isVisible.value = newValue;
|
|
726
|
+
}, toValue(debounce2));
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
const hoverEvents = {
|
|
730
|
+
onMouseover: () => debouncedVisible.value = true,
|
|
731
|
+
onMouseout: () => debouncedVisible.value = false,
|
|
732
|
+
onFocusin: () => _isVisible.value = true,
|
|
733
|
+
onFocusout: () => _isVisible.value = false
|
|
734
|
+
};
|
|
735
|
+
useDismissible({ isExpanded: _isVisible });
|
|
736
|
+
return {
|
|
737
|
+
elements: {
|
|
738
|
+
/**
|
|
739
|
+
* The element which controls the tooltip visibility on hover.
|
|
740
|
+
*/
|
|
741
|
+
trigger: {
|
|
742
|
+
"aria-describedby": tooltipId,
|
|
743
|
+
...hoverEvents
|
|
744
|
+
},
|
|
745
|
+
/**
|
|
746
|
+
* The element describing the tooltip.
|
|
747
|
+
* Only simple, textual and non-focusable content is allowed.
|
|
748
|
+
*/
|
|
749
|
+
tooltip: {
|
|
750
|
+
popover: "manual",
|
|
751
|
+
role: "tooltip",
|
|
752
|
+
id: tooltipId,
|
|
753
|
+
tabindex: "-1",
|
|
754
|
+
...hoverEvents
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
state: {
|
|
758
|
+
isVisible: _isVisible
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
export {
|
|
763
|
+
CLOSING_KEYS,
|
|
764
|
+
OPENING_KEYS,
|
|
765
|
+
createBuilder,
|
|
766
|
+
createComboBox,
|
|
767
|
+
createElRef,
|
|
768
|
+
createListbox,
|
|
769
|
+
createMenuButton,
|
|
770
|
+
createMenuItems,
|
|
771
|
+
createNavigationMenu,
|
|
772
|
+
createTabs,
|
|
773
|
+
createToggletip,
|
|
774
|
+
createTooltip,
|
|
775
|
+
debounce,
|
|
776
|
+
isPrintableCharacter,
|
|
777
|
+
useGlobalEventListener,
|
|
778
|
+
useOutsideClick,
|
|
779
|
+
wasKeyPressed
|
|
780
|
+
};
|