@sit-onyx/headless 0.8.0 → 0.8.1-dev-20260331053800
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/dist/composables/slider/createSlider.d.ts +1 -1
- package/dist/index.js +1605 -1605
- package/dist/playwright.js +439 -522
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,1642 +1,1642 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref, shallowRef, toRef, toValue, unref, useId, watch, watchEffect } from "vue";
|
|
2
|
+
//#region src/utils/builder.ts
|
|
3
|
+
/**
|
|
4
|
+
* We use this identity function to ensure the correct typings of the headless composables
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
|
|
8
|
+
* const tooltipId = useId();
|
|
9
|
+
* const isVisible = ref(initialVisible);
|
|
10
|
+
*
|
|
11
|
+
* const hoverEvents = {
|
|
12
|
+
* onMouseover: () => (isVisible.value = true),
|
|
13
|
+
* onMouseout: () => (isVisible.value = false),
|
|
14
|
+
* onFocusin: () => (isVisible.value = true),
|
|
15
|
+
* onFocusout: () => (isVisible.value = false),
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* return {
|
|
19
|
+
* elements: {
|
|
20
|
+
* trigger: {
|
|
21
|
+
* "aria-describedby": tooltipId,
|
|
22
|
+
* ...hoverEvents,
|
|
23
|
+
* },
|
|
24
|
+
* tooltip: {
|
|
25
|
+
* role: "tooltip",
|
|
26
|
+
* id: tooltipId,
|
|
27
|
+
* tabindex: "-1",
|
|
28
|
+
* ...hoverEvents,
|
|
29
|
+
* },
|
|
30
|
+
* },
|
|
31
|
+
* state: {
|
|
32
|
+
* isVisible,
|
|
33
|
+
* },
|
|
34
|
+
* };
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
var createBuilder = (builder) => builder;
|
|
40
|
+
/**
|
|
41
|
+
* Creates a special writeable computed that references a DOM Element.
|
|
42
|
+
* Vue Component references will be unwrapped.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* createBuilder() => {
|
|
46
|
+
* const buttonRef = createElRef<HtmlButtonElement>();
|
|
47
|
+
* return {
|
|
48
|
+
* elements: {
|
|
49
|
+
* button: {
|
|
50
|
+
* ref: buttonRef,
|
|
51
|
+
* },
|
|
52
|
+
* }
|
|
53
|
+
* };
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
3
57
|
function createElRef() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
});
|
|
58
|
+
const elementRef = shallowRef(null);
|
|
59
|
+
return computed({
|
|
60
|
+
set: (ref) => {
|
|
61
|
+
elementRef.value = getNativeElement(Array.isArray(ref) ? ref[0] : ref);
|
|
62
|
+
},
|
|
63
|
+
get: () => elementRef.value
|
|
64
|
+
});
|
|
12
65
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return element ?? null;
|
|
66
|
+
var getNativeElement = (element) => {
|
|
67
|
+
if (element && typeof element === "object" && "$el" in element) return element.$el;
|
|
68
|
+
return element ?? null;
|
|
18
69
|
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/utils/dates.ts
|
|
19
72
|
function getISOWeekNumber(date) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return weekNo;
|
|
73
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
74
|
+
const dayNum = d.getUTCDay() || 7;
|
|
75
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
76
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
77
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
26
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Checks whether the given date is in between the given start and end date.
|
|
81
|
+
*/
|
|
27
82
|
function isInDateRange(date, start, end) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
start = new Date(start);
|
|
84
|
+
start.setHours(0, 0, 0, 0);
|
|
85
|
+
end = new Date(end);
|
|
86
|
+
end.setHours(23, 59, 59, 999);
|
|
87
|
+
const time = date.getTime();
|
|
88
|
+
return time >= start.getTime() && time <= end.getTime();
|
|
34
89
|
}
|
|
35
90
|
function getNormalizedDayIndex(date, weekStartDay) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (normalizedDay - start + 7) % 7;
|
|
91
|
+
const day = date.getDay();
|
|
92
|
+
const start = WEEKDAYS.indexOf(weekStartDay);
|
|
93
|
+
return ((day === 0 ? 6 : day - 1) - start + 7) % 7;
|
|
40
94
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
95
|
+
var WEEKDAYS = [
|
|
96
|
+
"Monday",
|
|
97
|
+
"Tuesday",
|
|
98
|
+
"Wednesday",
|
|
99
|
+
"Thursday",
|
|
100
|
+
"Friday",
|
|
101
|
+
"Saturday",
|
|
102
|
+
"Sunday"
|
|
49
103
|
];
|
|
50
104
|
function sortDateRange(range) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
105
|
+
const start = new Date(range.start);
|
|
106
|
+
const end = range.end ? new Date(range.end) : void 0;
|
|
107
|
+
if (end && end.getTime() < start.getTime()) return {
|
|
108
|
+
start: end,
|
|
109
|
+
end: start
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
start,
|
|
113
|
+
end
|
|
114
|
+
};
|
|
57
115
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"aria-selected": isSelected.value(cell.date),
|
|
307
|
-
"aria-disabled": isDisabled.value(cell.date)
|
|
308
|
-
})),
|
|
309
|
-
button: computed(() => (button) => {
|
|
310
|
-
const formatter = new Intl.DateTimeFormat(toValue(options.locale), { dateStyle: "full" });
|
|
311
|
-
const attributes = {
|
|
312
|
-
"aria-label": formatter.format(button.date),
|
|
313
|
-
"data-date": button.date.toDateString()
|
|
314
|
-
};
|
|
315
|
-
const selection = toValue(options.selectionMode);
|
|
316
|
-
if (!selection) return attributes;
|
|
317
|
-
const disabled = isDisabled.value(button.date);
|
|
318
|
-
return {
|
|
319
|
-
...attributes,
|
|
320
|
-
tabindex: isFocused.value(button.date) && !disabled ? "0" : "-1",
|
|
321
|
-
disabled,
|
|
322
|
-
onClick: disabled ? void 0 : () => goToDate(button.date)
|
|
323
|
-
};
|
|
324
|
-
})
|
|
325
|
-
},
|
|
326
|
-
internals: {
|
|
327
|
-
goToMonthByOffset,
|
|
328
|
-
goToToday,
|
|
329
|
-
isSelected,
|
|
330
|
-
isToday,
|
|
331
|
-
getRangeType,
|
|
332
|
-
isDisabled,
|
|
333
|
-
goToDate,
|
|
334
|
-
isFocused
|
|
335
|
-
}
|
|
336
|
-
};
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/composables/calendar/createCalendar.ts
|
|
118
|
+
/**
|
|
119
|
+
* @experimental
|
|
120
|
+
* @deprecated This component is still under active development and its API might change in patch releases.
|
|
121
|
+
*/
|
|
122
|
+
var _unstableCreateCalendar = createBuilder((options) => {
|
|
123
|
+
const viewMonth = computed({
|
|
124
|
+
get: () => {
|
|
125
|
+
const date = toValue(options.viewMonth);
|
|
126
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
127
|
+
},
|
|
128
|
+
set: (newValue) => options.onUpdateViewMonth?.(new Date(newValue))
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* Human readable weekday names depending on the given locale.
|
|
132
|
+
*/
|
|
133
|
+
const weekdayNames = computed(() => {
|
|
134
|
+
const formatter = new Intl.DateTimeFormat(toValue(options.locale), { weekday: toValue(options.calendarSize) === "big" ? "long" : "short" });
|
|
135
|
+
const names = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, 1 + i)).map((day) => formatter.format(day));
|
|
136
|
+
const weekStartDay = toValue(options.weekStartDay);
|
|
137
|
+
const index = WEEKDAYS.indexOf(weekStartDay);
|
|
138
|
+
return names.slice(index).concat(names.slice(0, index));
|
|
139
|
+
});
|
|
140
|
+
const initialFocusedDate = () => {
|
|
141
|
+
const today = /* @__PURE__ */ new Date();
|
|
142
|
+
const view = viewMonth.value;
|
|
143
|
+
if (today.getFullYear() === view.getFullYear() && today.getMonth() === view.getMonth()) return today;
|
|
144
|
+
else return new Date(view.getFullYear(), view.getMonth(), 1);
|
|
145
|
+
};
|
|
146
|
+
const focusedDate = ref(initialFocusedDate());
|
|
147
|
+
watch(() => toValue(options.modelValue), (newValue) => {
|
|
148
|
+
if (!newValue) return;
|
|
149
|
+
let newFocusDate;
|
|
150
|
+
if (Array.isArray(newValue)) newFocusDate = newValue.length ? new Date(newValue[0]) : void 0;
|
|
151
|
+
else if (typeof newValue === "object" && !(newValue instanceof Date)) newFocusDate = new Date(newValue.start);
|
|
152
|
+
else newFocusDate = new Date(newValue);
|
|
153
|
+
if (newFocusDate) {
|
|
154
|
+
focusedDate.value = newFocusDate;
|
|
155
|
+
viewMonth.value = newFocusDate;
|
|
156
|
+
}
|
|
157
|
+
}, { immediate: true });
|
|
158
|
+
const isToday = (date) => {
|
|
159
|
+
return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
|
|
160
|
+
};
|
|
161
|
+
const isSelected = computed(() => {
|
|
162
|
+
return (date) => {
|
|
163
|
+
const value = toValue(options.modelValue);
|
|
164
|
+
if (!value) return false;
|
|
165
|
+
if (Array.isArray(value)) return value.map((i) => new Date(i)).some((d) => d.toDateString() === date.toDateString());
|
|
166
|
+
if (typeof value === "object" && !(value instanceof Date)) {
|
|
167
|
+
const start = new Date(value.start);
|
|
168
|
+
const end = value.end ? new Date(value.end) : void 0;
|
|
169
|
+
return start.toDateString() === date.toDateString() || end?.toDateString() === date.toDateString();
|
|
170
|
+
}
|
|
171
|
+
return new Date(value).toDateString() === date.toDateString();
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
const isFocused = computed(() => {
|
|
175
|
+
return (date) => {
|
|
176
|
+
return focusedDate.value?.toDateString() === date.toDateString();
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
const isDisabled = computed(() => {
|
|
180
|
+
return (date) => {
|
|
181
|
+
const disabledPropValue = toValue(options.disabled);
|
|
182
|
+
if (typeof disabledPropValue === "boolean") {
|
|
183
|
+
if (disabledPropValue) return true;
|
|
184
|
+
}
|
|
185
|
+
if (typeof disabledPropValue === "function") {
|
|
186
|
+
if (disabledPropValue(date)) return true;
|
|
187
|
+
}
|
|
188
|
+
const min = toValue(options.min);
|
|
189
|
+
const minDate = min ? new Date(min) : void 0;
|
|
190
|
+
minDate?.setHours(0, 0, 0, 0);
|
|
191
|
+
const max = toValue(options.max);
|
|
192
|
+
const maxDate = max ? new Date(max) : void 0;
|
|
193
|
+
maxDate?.setHours(23, 59, 59, 999);
|
|
194
|
+
if (minDate && maxDate) return !isInDateRange(date, minDate, maxDate);
|
|
195
|
+
if (minDate) return date.getTime() < minDate.getTime();
|
|
196
|
+
if (maxDate) return date.getTime() > maxDate.getTime();
|
|
197
|
+
return false;
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
const weeksToRender = computed(() => {
|
|
201
|
+
const weekStartDay = toValue(options.weekStartDay);
|
|
202
|
+
const firstDayInViewMonth = new Date(viewMonth.value.getFullYear(), viewMonth.value.getMonth(), 1);
|
|
203
|
+
const daysBeforeStart = ((firstDayInViewMonth.getDay() + 6) % 7 - WEEKDAYS.indexOf(weekStartDay) + 7) % 7;
|
|
204
|
+
const weeksToRender = 6;
|
|
205
|
+
const weeks = [];
|
|
206
|
+
for (let weekIndex = 0; weekIndex < weeksToRender; weekIndex++) {
|
|
207
|
+
const weekStartDate = new Date(firstDayInViewMonth);
|
|
208
|
+
weekStartDate.setDate(weekStartDate.getDate() - daysBeforeStart + weekIndex * 7);
|
|
209
|
+
const days = Array.from({ length: 7 }, (_, dayIndex) => {
|
|
210
|
+
const date = new Date(weekStartDate);
|
|
211
|
+
date.setDate(date.getDate() + dayIndex);
|
|
212
|
+
return {
|
|
213
|
+
date,
|
|
214
|
+
isCurrentMonth: date.getMonth() === viewMonth.value.getMonth()
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
weeks.push({
|
|
218
|
+
weekNumber: getISOWeekNumber(weekStartDate),
|
|
219
|
+
days
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return weeks;
|
|
223
|
+
});
|
|
224
|
+
const goToDate = (date, preventSelectionUpdate) => {
|
|
225
|
+
focusedDate.value = new Date(date);
|
|
226
|
+
if (focusedDate.value.getFullYear() !== viewMonth.value.getFullYear() || focusedDate.value.getMonth() !== viewMonth.value.getMonth()) viewMonth.value = new Date(focusedDate.value);
|
|
227
|
+
const selectionMode = toValue(options.selectionMode);
|
|
228
|
+
if (!selectionMode || preventSelectionUpdate) return;
|
|
229
|
+
const selection = toValue(options.modelValue);
|
|
230
|
+
switch (selectionMode) {
|
|
231
|
+
case "single":
|
|
232
|
+
options.onUpdateModelValue?.(new Date(date));
|
|
233
|
+
break;
|
|
234
|
+
case "multiple": {
|
|
235
|
+
let values = Array.isArray(selection) ? selection.map((d) => new Date(d)) : [];
|
|
236
|
+
if (isSelected.value(date)) values = values.filter((d) => d.toDateString() !== date.toDateString());
|
|
237
|
+
else values.push(new Date(date));
|
|
238
|
+
options.onUpdateModelValue?.(values);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "range": {
|
|
242
|
+
const currentRange = typeof selection === "object" && !(selection instanceof Date) && !Array.isArray(selection) ? selection : void 0;
|
|
243
|
+
let newRange;
|
|
244
|
+
if (currentRange?.start && currentRange.end) newRange = { start: new Date(date) };
|
|
245
|
+
else newRange = {
|
|
246
|
+
start: currentRange?.start ? new Date(currentRange.start) : new Date(date),
|
|
247
|
+
end: currentRange?.start ? new Date(date) : void 0
|
|
248
|
+
};
|
|
249
|
+
newRange = sortDateRange(newRange);
|
|
250
|
+
options.onUpdateModelValue?.(newRange);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const handleKeyNavigation = async (event) => {
|
|
256
|
+
let newDate;
|
|
257
|
+
const getNewFocusDateByDiff = (diff, type = "days") => {
|
|
258
|
+
const date = new Date(focusedDate.value);
|
|
259
|
+
if (type === "month") date.setMonth(date.getMonth() + diff);
|
|
260
|
+
else date.setDate(date.getDate() + diff);
|
|
261
|
+
return date;
|
|
262
|
+
};
|
|
263
|
+
switch (event.key) {
|
|
264
|
+
case "ArrowUp":
|
|
265
|
+
newDate = getNewFocusDateByDiff(-7);
|
|
266
|
+
break;
|
|
267
|
+
case "ArrowDown":
|
|
268
|
+
newDate = getNewFocusDateByDiff(7);
|
|
269
|
+
break;
|
|
270
|
+
case "ArrowLeft":
|
|
271
|
+
newDate = getNewFocusDateByDiff(-1);
|
|
272
|
+
break;
|
|
273
|
+
case "ArrowRight":
|
|
274
|
+
newDate = getNewFocusDateByDiff(1);
|
|
275
|
+
break;
|
|
276
|
+
case "Home":
|
|
277
|
+
newDate = getNewFocusDateByDiff(-getNormalizedDayIndex(focusedDate.value, toValue(options.weekStartDay)));
|
|
278
|
+
break;
|
|
279
|
+
case "End":
|
|
280
|
+
newDate = getNewFocusDateByDiff(6 - getNormalizedDayIndex(focusedDate.value, toValue(options.weekStartDay)));
|
|
281
|
+
break;
|
|
282
|
+
case "PageUp":
|
|
283
|
+
newDate = getNewFocusDateByDiff(-(event.shiftKey ? 12 : 1), "month");
|
|
284
|
+
break;
|
|
285
|
+
case "PageDown":
|
|
286
|
+
newDate = getNewFocusDateByDiff(event.shiftKey ? 12 : 1, "month");
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
if (!newDate || isDisabled.value(newDate)) return;
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
goToDate(newDate, true);
|
|
292
|
+
await nextTick();
|
|
293
|
+
tableRef.value?.querySelector(`[data-date="${focusedDate.value.toDateString()}"]`)?.focus();
|
|
294
|
+
};
|
|
295
|
+
const getRangeType = computed(() => {
|
|
296
|
+
return (date, range) => {
|
|
297
|
+
const selection = range ?? toValue(options.modelValue);
|
|
298
|
+
if (!selection || typeof selection !== "object" || Array.isArray(selection) || selection instanceof Date) return;
|
|
299
|
+
const sortedRange = sortDateRange(selection);
|
|
300
|
+
const start = new Date(sortedRange.start);
|
|
301
|
+
start.setHours(0, 0, 0, 0);
|
|
302
|
+
const end = sortedRange.end ? new Date(sortedRange.end) : void 0;
|
|
303
|
+
end?.setHours(23, 59, 59, 999);
|
|
304
|
+
if (date.toDateString() === start.toDateString() && date.toDateString() === end?.toDateString()) return;
|
|
305
|
+
if (date.toDateString() === start.toDateString()) return "start";
|
|
306
|
+
if (date.toDateString() === end?.toDateString()) return "end";
|
|
307
|
+
if (end && isInDateRange(date, start, end)) return "middle";
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
const goToMonthByOffset = (offset) => {
|
|
311
|
+
const date = new Date(viewMonth.value);
|
|
312
|
+
date.setMonth(date.getMonth() + offset, 1);
|
|
313
|
+
viewMonth.value = date;
|
|
314
|
+
};
|
|
315
|
+
const goToToday = () => {
|
|
316
|
+
viewMonth.value = /* @__PURE__ */ new Date();
|
|
317
|
+
};
|
|
318
|
+
const tableRef = createElRef();
|
|
319
|
+
return {
|
|
320
|
+
state: {
|
|
321
|
+
weekdayNames,
|
|
322
|
+
weeksToRender,
|
|
323
|
+
focusedDate,
|
|
324
|
+
viewMonth
|
|
325
|
+
},
|
|
326
|
+
elements: {
|
|
327
|
+
table: {
|
|
328
|
+
role: "grid",
|
|
329
|
+
onKeydown: handleKeyNavigation,
|
|
330
|
+
ref: tableRef
|
|
331
|
+
},
|
|
332
|
+
cell: computed(() => (cell) => ({
|
|
333
|
+
role: "gridcell",
|
|
334
|
+
"aria-selected": isSelected.value(cell.date),
|
|
335
|
+
"aria-disabled": isDisabled.value(cell.date)
|
|
336
|
+
})),
|
|
337
|
+
button: computed(() => (button) => {
|
|
338
|
+
const formatter = new Intl.DateTimeFormat(toValue(options.locale), { dateStyle: "full" });
|
|
339
|
+
const attributes = {
|
|
340
|
+
"aria-label": formatter.format(button.date),
|
|
341
|
+
"data-date": button.date.toDateString()
|
|
342
|
+
};
|
|
343
|
+
if (!toValue(options.selectionMode)) return attributes;
|
|
344
|
+
const disabled = isDisabled.value(button.date);
|
|
345
|
+
return {
|
|
346
|
+
...attributes,
|
|
347
|
+
tabindex: isFocused.value(button.date) && !disabled ? "0" : "-1",
|
|
348
|
+
disabled,
|
|
349
|
+
onClick: disabled ? void 0 : () => goToDate(button.date)
|
|
350
|
+
};
|
|
351
|
+
})
|
|
352
|
+
},
|
|
353
|
+
internals: {
|
|
354
|
+
goToMonthByOffset,
|
|
355
|
+
goToToday,
|
|
356
|
+
isSelected,
|
|
357
|
+
isToday,
|
|
358
|
+
getRangeType,
|
|
359
|
+
isDisabled,
|
|
360
|
+
goToDate,
|
|
361
|
+
isFocused
|
|
362
|
+
}
|
|
363
|
+
};
|
|
337
364
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/utils/object.ts
|
|
367
|
+
/**
|
|
368
|
+
* Check if every entry of a subset exists and matches the entry of a target object.
|
|
369
|
+
* @returns `true`, if target contains the subset
|
|
370
|
+
*/
|
|
371
|
+
var isSubsetMatching = (subset, target) => Object.entries(subset).every(([key, value]) => target[key] === value);
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/utils/keyboard.ts
|
|
374
|
+
/**
|
|
375
|
+
* Check if a specified key was pressed.
|
|
376
|
+
* @param event The KeyboardEvent
|
|
377
|
+
* @param key The key, either the [key property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) as a string (e.g. "m")
|
|
378
|
+
* or an object with the relevant key parameters, e.g. `{ key: "m", altKey: true }`
|
|
379
|
+
* @returns true, if the key was pressed with the specified parameters
|
|
380
|
+
*/
|
|
381
|
+
var wasKeyPressed = (event, key) => {
|
|
382
|
+
if (typeof key === "string") return event.key === key;
|
|
383
|
+
return isSubsetMatching({
|
|
384
|
+
altKey: false,
|
|
385
|
+
ctrlKey: false,
|
|
386
|
+
metaKey: false,
|
|
387
|
+
shiftKey: false,
|
|
388
|
+
...key
|
|
389
|
+
}, event);
|
|
349
390
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
391
|
+
var GRAPHEME_SEGMENTER = new Intl.Segmenter("en-US");
|
|
392
|
+
/**
|
|
393
|
+
* Check if the `key` property of a KeyboardEvent is a printable character.
|
|
394
|
+
*
|
|
395
|
+
* There is no standardized or specified algorithm to check for [named keys](https://www.w3.org/TR/uievents-key/#named-key-attribute-values) vs printable characters.
|
|
396
|
+
* For this check we assume that any `key` value that is a single Grapheme is a printable characters.
|
|
397
|
+
* This way we can ensure that combining characters and emojis are correctly recognized without need to keep a list of all named key.
|
|
398
|
+
*/
|
|
399
|
+
var isPrintableCharacter = (key) => [...GRAPHEME_SEGMENTER.segment(key)].length === 1;
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/composables/helpers/useGlobalListener.ts
|
|
402
|
+
var GLOBAL_LISTENERS = reactive(/* @__PURE__ */ new Map());
|
|
403
|
+
var updateRemainingListeners = (type, remaining) => {
|
|
404
|
+
if (remaining?.size) {
|
|
405
|
+
GLOBAL_LISTENERS.set(type, remaining);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
GLOBAL_LISTENERS.delete(type);
|
|
409
|
+
document.removeEventListener(type, GLOBAL_HANDLER);
|
|
360
410
|
};
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
411
|
+
var removeGlobalListener = (type, listener) => {
|
|
412
|
+
const globalListener = GLOBAL_LISTENERS.get(type);
|
|
413
|
+
globalListener?.delete(listener);
|
|
414
|
+
updateRemainingListeners(type, globalListener);
|
|
365
415
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
416
|
+
var addGlobalListener = (type, listener) => {
|
|
417
|
+
const globalListener = GLOBAL_LISTENERS.get(type) ?? /* @__PURE__ */ new Set();
|
|
418
|
+
globalListener.add(listener);
|
|
419
|
+
GLOBAL_LISTENERS.set(type, globalListener);
|
|
420
|
+
document.addEventListener(type, GLOBAL_HANDLER);
|
|
371
421
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
422
|
+
/**
|
|
423
|
+
* A single and unique function for all event types.
|
|
424
|
+
* We use the fact that `addEventListener` and `removeEventListener` are idempotent when called with the same function reference.
|
|
425
|
+
*/
|
|
426
|
+
var GLOBAL_HANDLER = (event) => {
|
|
427
|
+
const type = event.type;
|
|
428
|
+
GLOBAL_LISTENERS.get(type)?.forEach((cb) => cb(event));
|
|
375
429
|
};
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
disabled
|
|
380
|
-
}) => {
|
|
381
|
-
onBeforeMount(
|
|
382
|
-
() => watchEffect(
|
|
383
|
-
() => disabled?.value ? removeGlobalListener(type, listener) : addGlobalListener(type, listener)
|
|
384
|
-
)
|
|
385
|
-
);
|
|
386
|
-
onBeforeUnmount(() => removeGlobalListener(type, listener));
|
|
430
|
+
var useGlobalEventListener = ({ type, listener, disabled }) => {
|
|
431
|
+
onBeforeMount(() => watchEffect(() => disabled?.value ? removeGlobalListener(type, listener) : addGlobalListener(type, listener)));
|
|
432
|
+
onBeforeUnmount(() => removeGlobalListener(type, listener));
|
|
387
433
|
};
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/composables/helpers/useOutsideClick.ts
|
|
436
|
+
/**
|
|
437
|
+
* Composable for listening to click events that occur outside of a component.
|
|
438
|
+
* Useful to e.g. close flyouts or tooltips.
|
|
439
|
+
*/
|
|
440
|
+
var useOutsideClick = ({ inside, onOutsideClick, disabled, checkOnTab }) => {
|
|
441
|
+
const isOutsideClick = (target) => {
|
|
442
|
+
if (!target) return true;
|
|
443
|
+
const raw = toValue(inside);
|
|
444
|
+
return !(Array.isArray(raw) ? raw : [raw]).some((element) => {
|
|
445
|
+
return getNativeElement(element)?.contains(target);
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* Document click handle that closes then tooltip when clicked outside.
|
|
450
|
+
* Should only be called when trigger is "click".
|
|
451
|
+
*/
|
|
452
|
+
const clickListener = (event) => {
|
|
453
|
+
if (isOutsideClick(event.target)) onOutsideClick(event);
|
|
454
|
+
};
|
|
455
|
+
useGlobalEventListener({
|
|
456
|
+
type: "mousedown",
|
|
457
|
+
listener: clickListener,
|
|
458
|
+
disabled
|
|
459
|
+
});
|
|
460
|
+
if (checkOnTab) {
|
|
461
|
+
const keydownListener = (event) => {
|
|
462
|
+
if (event.key !== "Tab") return;
|
|
463
|
+
if (isOutsideClick(document.activeElement)) onOutsideClick(event);
|
|
464
|
+
const controller = new AbortController();
|
|
465
|
+
const { signal } = controller;
|
|
466
|
+
/**
|
|
467
|
+
* Handles when focus enters a new element after Tab navigation.
|
|
468
|
+
* Triggers outside click if focus moves outside the component.
|
|
469
|
+
*/
|
|
470
|
+
const onFocusIn = (event) => {
|
|
471
|
+
const target = event.target;
|
|
472
|
+
if (isOutsideClick(target)) onOutsideClick(event);
|
|
473
|
+
controller.abort();
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Handles when the entire window loses focus during Tab navigation.
|
|
477
|
+
* This covers cases like switching to dev tools or another application.
|
|
478
|
+
*/
|
|
479
|
+
const onWindowBlur = (event) => {
|
|
480
|
+
if (isOutsideClick(document.activeElement)) onOutsideClick(event);
|
|
481
|
+
controller.abort();
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Handles the Tab key release event.
|
|
485
|
+
* Cleans up temporary listeners if Tab navigation is completed.
|
|
486
|
+
*/
|
|
487
|
+
const onKeyUp = (e) => {
|
|
488
|
+
if (e.key === "Tab") controller.abort();
|
|
489
|
+
};
|
|
490
|
+
document.addEventListener("focusin", onFocusIn, {
|
|
491
|
+
once: true,
|
|
492
|
+
capture: true,
|
|
493
|
+
signal
|
|
494
|
+
});
|
|
495
|
+
window.addEventListener("blur", onWindowBlur, {
|
|
496
|
+
once: true,
|
|
497
|
+
signal
|
|
498
|
+
});
|
|
499
|
+
document.addEventListener("keyup", onKeyUp, {
|
|
500
|
+
once: true,
|
|
501
|
+
capture: true,
|
|
502
|
+
signal
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
useGlobalEventListener({
|
|
506
|
+
type: "keydown",
|
|
507
|
+
listener: keydownListener,
|
|
508
|
+
disabled
|
|
509
|
+
});
|
|
510
|
+
}
|
|
447
511
|
};
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/utils/timer.ts
|
|
514
|
+
/**
|
|
515
|
+
* Debounces a given callback which will only be called when not called for the given timeout.
|
|
516
|
+
*
|
|
517
|
+
* @returns Callback to reset the debounce timer.
|
|
518
|
+
*/
|
|
519
|
+
var debounce = (handler, timeout) => {
|
|
520
|
+
let timer;
|
|
521
|
+
const func = (...lastArgs) => {
|
|
522
|
+
clearTimeout(timer);
|
|
523
|
+
timer = setTimeout(() => handler(...lastArgs), toValue(timeout));
|
|
524
|
+
};
|
|
525
|
+
/** Abort the currently debounced action, if any. */
|
|
526
|
+
func.abort = () => clearTimeout(timer);
|
|
527
|
+
return func;
|
|
456
528
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/composables/helpers/useTypeAhead.ts
|
|
531
|
+
/**
|
|
532
|
+
* Enhances typeAhead to combine multiple inputs in quick succession and filter out non-printable characters.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* const typeAhead = useTypeAhead((inputString) => console.log("Typed string:", inputString));
|
|
537
|
+
* // ...
|
|
538
|
+
* addEventListener("keydown", typeAhead);
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
var useTypeAhead = (callback, timeout = 500) => {
|
|
542
|
+
let inputString = "";
|
|
543
|
+
const debouncedReset = debounce(() => inputString = "", timeout);
|
|
544
|
+
return (event) => {
|
|
545
|
+
if (!isPrintableCharacter(event.key)) return;
|
|
546
|
+
debouncedReset();
|
|
547
|
+
inputString = `${inputString}${event.key}`;
|
|
548
|
+
callback(inputString);
|
|
549
|
+
};
|
|
468
550
|
};
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/composables/listbox/createListbox.ts
|
|
553
|
+
/**
|
|
554
|
+
* Composable for creating a accessibility-conform listbox.
|
|
555
|
+
* For supported keyboard shortcuts, see: https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-scrollable/
|
|
556
|
+
*/
|
|
557
|
+
var createListbox = createBuilder((options) => {
|
|
558
|
+
const isMultiselect = computed(() => unref(options.multiple) ?? false);
|
|
559
|
+
const isExpanded = computed(() => unref(options.isExpanded) ?? false);
|
|
560
|
+
/**
|
|
561
|
+
* Map for option IDs. key = option value, key = ID for the HTML element
|
|
562
|
+
*/
|
|
563
|
+
const descendantKeyIdMap = /* @__PURE__ */ new Map();
|
|
564
|
+
const getOptionId = (value) => {
|
|
565
|
+
if (!descendantKeyIdMap.has(value)) descendantKeyIdMap.set(value, useId());
|
|
566
|
+
return descendantKeyIdMap.get(value);
|
|
567
|
+
};
|
|
568
|
+
const getOptionValueById = (id) => {
|
|
569
|
+
return Array.from(descendantKeyIdMap.entries()).find(([_value, key]) => key === id)?.[0];
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* Whether the listbox element is focused.
|
|
573
|
+
*/
|
|
574
|
+
const isFocused = ref(false);
|
|
575
|
+
watchEffect(async () => {
|
|
576
|
+
if (!isExpanded.value || options.activeOption.value == void 0 || !isFocused.value && !options.controlled) return;
|
|
577
|
+
const id = getOptionId(options.activeOption.value);
|
|
578
|
+
await nextTick();
|
|
579
|
+
document.getElementById(id)?.scrollIntoView({
|
|
580
|
+
block: "nearest",
|
|
581
|
+
inline: "nearest"
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
|
|
585
|
+
const handleKeydown = (event) => {
|
|
586
|
+
switch (event.key) {
|
|
587
|
+
case " ":
|
|
588
|
+
event.preventDefault();
|
|
589
|
+
if (options.activeOption.value != void 0) options.onSelect?.(options.activeOption.value);
|
|
590
|
+
break;
|
|
591
|
+
case "ArrowUp":
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
if (options.activeOption.value == void 0) {
|
|
594
|
+
options.onActivateLast?.();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
options.onActivatePrevious?.(options.activeOption.value);
|
|
598
|
+
break;
|
|
599
|
+
case "ArrowDown":
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
if (options.activeOption.value == void 0) {
|
|
602
|
+
options.onActivateFirst?.();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
options.onActivateNext?.(options.activeOption.value);
|
|
606
|
+
break;
|
|
607
|
+
case "Home":
|
|
608
|
+
event.preventDefault();
|
|
609
|
+
options.onActivateFirst?.();
|
|
610
|
+
break;
|
|
611
|
+
case "End":
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
options.onActivateLast?.();
|
|
614
|
+
break;
|
|
615
|
+
default: typeAhead(event);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
return {
|
|
619
|
+
elements: {
|
|
620
|
+
listbox: computed(() => options.controlled ? {
|
|
621
|
+
role: "listbox",
|
|
622
|
+
"aria-multiselectable": isMultiselect.value,
|
|
623
|
+
"aria-label": unref(options.label),
|
|
624
|
+
"aria-description": options.description,
|
|
625
|
+
tabindex: "-1"
|
|
626
|
+
} : {
|
|
627
|
+
role: "listbox",
|
|
628
|
+
"aria-multiselectable": isMultiselect.value,
|
|
629
|
+
"aria-label": unref(options.label),
|
|
630
|
+
"aria-description": options.description,
|
|
631
|
+
tabindex: "0",
|
|
632
|
+
"aria-activedescendant": options.activeOption.value != void 0 ? getOptionId(options.activeOption.value) : void 0,
|
|
633
|
+
onFocus: () => isFocused.value = true,
|
|
634
|
+
onBlur: () => isFocused.value = false,
|
|
635
|
+
onKeydown: handleKeydown
|
|
636
|
+
}),
|
|
637
|
+
group: computed(() => {
|
|
638
|
+
return (options) => ({
|
|
639
|
+
role: "group",
|
|
640
|
+
"aria-label": options.label
|
|
641
|
+
});
|
|
642
|
+
}),
|
|
643
|
+
option: computed(() => {
|
|
644
|
+
return (data) => {
|
|
645
|
+
const selected = data.selected ?? false;
|
|
646
|
+
return {
|
|
647
|
+
id: getOptionId(data.value),
|
|
648
|
+
role: "option",
|
|
649
|
+
"aria-label": data.label,
|
|
650
|
+
"aria-disabled": data.disabled,
|
|
651
|
+
"aria-checked": isMultiselect.value ? selected : void 0,
|
|
652
|
+
"aria-selected": !isMultiselect.value ? selected : void 0,
|
|
653
|
+
onClick: () => !data.disabled && options.onSelect?.(data.value)
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
})
|
|
657
|
+
},
|
|
658
|
+
state: { isFocused },
|
|
659
|
+
internals: {
|
|
660
|
+
getOptionId,
|
|
661
|
+
getOptionValueById
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/composables/comboBox/createComboBox.ts
|
|
667
|
+
var OPENING_KEYS = [
|
|
668
|
+
"ArrowDown",
|
|
669
|
+
"ArrowUp",
|
|
670
|
+
" ",
|
|
671
|
+
"Enter",
|
|
672
|
+
"Home",
|
|
673
|
+
"End"
|
|
674
|
+
];
|
|
675
|
+
var CLOSING_KEYS = [
|
|
676
|
+
"Escape",
|
|
677
|
+
{
|
|
678
|
+
key: "ArrowUp",
|
|
679
|
+
altKey: true
|
|
680
|
+
},
|
|
681
|
+
"Enter",
|
|
682
|
+
"Tab"
|
|
589
683
|
];
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
684
|
+
var SELECTING_KEYS = ["Enter"];
|
|
685
|
+
/**
|
|
686
|
+
* if the a search input is included, space should not be used to select
|
|
687
|
+
* TODO: idea for the future: move this distinction to the listbox?
|
|
688
|
+
*/
|
|
689
|
+
var isSelectingKey = (event, withSpace) => {
|
|
690
|
+
return isKeyOfGroup(event, withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS);
|
|
594
691
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
"aria-controls": controlsId,
|
|
735
|
-
"aria-label": unref(label),
|
|
736
|
-
"aria-activedescendant": activeOption.value != void 0 ? getOptionId(activeOption.value) : void 0,
|
|
737
|
-
onInput: handleInput,
|
|
738
|
-
onKeydown: handleKeydown,
|
|
739
|
-
...autocompleteInput.value
|
|
740
|
-
})),
|
|
741
|
-
/**
|
|
742
|
-
* An optional button to control the visibility of the popup.
|
|
743
|
-
*/
|
|
744
|
-
button: computed(() => ({
|
|
745
|
-
tabindex: "-1",
|
|
746
|
-
onClick: () => onToggle?.()
|
|
747
|
-
}))
|
|
748
|
-
},
|
|
749
|
-
internals: { getOptionId, getOptionValueById }
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
);
|
|
753
|
-
const useAllSettled = (cb) => {
|
|
754
|
-
const active = ref(false);
|
|
755
|
-
const allPromises = [];
|
|
756
|
-
let latestPromise = Promise.resolve();
|
|
757
|
-
const add = (promise) => {
|
|
758
|
-
active.value = true;
|
|
759
|
-
allPromises.push(promise);
|
|
760
|
-
const newAllSettled = Promise.allSettled(allPromises).then(() => {
|
|
761
|
-
if (newAllSettled === latestPromise) {
|
|
762
|
-
active.value = false;
|
|
763
|
-
allPromises.splice(0, allPromises.length);
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
latestPromise = newAllSettled;
|
|
767
|
-
};
|
|
768
|
-
return { add, active };
|
|
692
|
+
var isKeyOfGroup = (event, group) => group.some((key) => wasKeyPressed(event, key));
|
|
693
|
+
var createComboBox = createBuilder(({ autocomplete: autocompleteRef, onAutocomplete, onTypeAhead, multiple: multipleRef, label, listLabel, listDescription, isExpanded: isExpandedRef, activeOption, onToggle, onSelect, onActivateFirst, onActivateLast, onActivateNext, onActivatePrevious, templateRef }) => {
|
|
694
|
+
const controlsId = useId();
|
|
695
|
+
const autocomplete = computed(() => unref(autocompleteRef));
|
|
696
|
+
const isExpanded = computed(() => unref(isExpandedRef));
|
|
697
|
+
const multiple = computed(() => unref(multipleRef));
|
|
698
|
+
const handleInput = (event) => {
|
|
699
|
+
const inputElement = event.target;
|
|
700
|
+
if (autocomplete.value !== "none") onAutocomplete?.(inputElement.value);
|
|
701
|
+
};
|
|
702
|
+
const typeAhead = useTypeAhead((inputString) => onTypeAhead?.(inputString));
|
|
703
|
+
const handleSelect = (value) => {
|
|
704
|
+
onSelect?.(value);
|
|
705
|
+
if (!unref(multiple)) onToggle?.();
|
|
706
|
+
};
|
|
707
|
+
const handleNavigation = (event) => {
|
|
708
|
+
switch (event.key) {
|
|
709
|
+
case "ArrowUp":
|
|
710
|
+
event.preventDefault();
|
|
711
|
+
if (activeOption.value == void 0) return onActivateLast?.();
|
|
712
|
+
onActivatePrevious?.(activeOption.value);
|
|
713
|
+
break;
|
|
714
|
+
case "ArrowDown":
|
|
715
|
+
event.preventDefault();
|
|
716
|
+
if (activeOption.value == void 0) return onActivateFirst?.();
|
|
717
|
+
onActivateNext?.(activeOption.value);
|
|
718
|
+
break;
|
|
719
|
+
case "Home":
|
|
720
|
+
event.preventDefault();
|
|
721
|
+
onActivateFirst?.();
|
|
722
|
+
break;
|
|
723
|
+
case "End":
|
|
724
|
+
event.preventDefault();
|
|
725
|
+
onActivateLast?.();
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
const handleKeydown = (event) => {
|
|
730
|
+
if (event.key === "Enter") event.preventDefault();
|
|
731
|
+
if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
|
|
732
|
+
onToggle?.();
|
|
733
|
+
if (event.key === " ") event.preventDefault();
|
|
734
|
+
if (event.key === "End") return onActivateLast?.();
|
|
735
|
+
return onActivateFirst?.();
|
|
736
|
+
}
|
|
737
|
+
if (isSelectingKey(event, autocomplete.value === "none")) return handleSelect(activeOption.value);
|
|
738
|
+
if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) return onToggle?.();
|
|
739
|
+
if (autocomplete.value === "none" && isPrintableCharacter(event.key)) {
|
|
740
|
+
!isExpanded.value && onToggle?.();
|
|
741
|
+
return typeAhead(event);
|
|
742
|
+
}
|
|
743
|
+
if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
|
|
744
|
+
!isExpanded.value && onToggle?.();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
return handleNavigation(event);
|
|
748
|
+
};
|
|
749
|
+
const autocompleteInput = computed(() => {
|
|
750
|
+
if (autocomplete.value === "none") return null;
|
|
751
|
+
return {
|
|
752
|
+
"aria-autocomplete": autocomplete.value,
|
|
753
|
+
type: "text"
|
|
754
|
+
};
|
|
755
|
+
});
|
|
756
|
+
const { elements: { option, group, listbox }, internals: { getOptionId, getOptionValueById } } = createListbox({
|
|
757
|
+
label: listLabel,
|
|
758
|
+
description: listDescription,
|
|
759
|
+
multiple,
|
|
760
|
+
controlled: true,
|
|
761
|
+
activeOption,
|
|
762
|
+
isExpanded,
|
|
763
|
+
onSelect: handleSelect
|
|
764
|
+
});
|
|
765
|
+
useOutsideClick({
|
|
766
|
+
inside: templateRef,
|
|
767
|
+
onOutsideClick() {
|
|
768
|
+
if (!isExpanded.value) return;
|
|
769
|
+
onToggle?.(true);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
elements: {
|
|
774
|
+
option,
|
|
775
|
+
group,
|
|
776
|
+
listbox: computed(() => ({
|
|
777
|
+
...listbox.value,
|
|
778
|
+
id: controlsId,
|
|
779
|
+
onMousedown: (e) => e.preventDefault()
|
|
780
|
+
})),
|
|
781
|
+
input: computed(() => ({
|
|
782
|
+
role: "combobox",
|
|
783
|
+
"aria-expanded": isExpanded.value,
|
|
784
|
+
"aria-controls": controlsId,
|
|
785
|
+
"aria-label": unref(label),
|
|
786
|
+
"aria-activedescendant": activeOption.value != void 0 ? getOptionId(activeOption.value) : void 0,
|
|
787
|
+
onInput: handleInput,
|
|
788
|
+
onKeydown: handleKeydown,
|
|
789
|
+
...autocompleteInput.value
|
|
790
|
+
})),
|
|
791
|
+
button: computed(() => ({
|
|
792
|
+
tabindex: "-1",
|
|
793
|
+
onClick: () => onToggle?.()
|
|
794
|
+
}))
|
|
795
|
+
},
|
|
796
|
+
internals: {
|
|
797
|
+
getOptionId,
|
|
798
|
+
getOptionValueById
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
});
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/composables/helpers/useAllSettled.ts
|
|
804
|
+
/**
|
|
805
|
+
* Execute a callback, when all added promise are settled (either resolved or rejected).
|
|
806
|
+
* It allows for more promises to be added while waiting.
|
|
807
|
+
*
|
|
808
|
+
* @param cb callback to execute when all added promise are settled.
|
|
809
|
+
* @returns an object with an add function and the active state which is true as long as any promise is running.
|
|
810
|
+
*/
|
|
811
|
+
var useAllSettled = (cb) => {
|
|
812
|
+
const active = ref(false);
|
|
813
|
+
const allPromises = [];
|
|
814
|
+
let latestPromise = Promise.resolve();
|
|
815
|
+
const add = (promise) => {
|
|
816
|
+
active.value = true;
|
|
817
|
+
allPromises.push(promise);
|
|
818
|
+
const newAllSettled = Promise.allSettled(allPromises).then(() => {
|
|
819
|
+
if (newAllSettled === latestPromise) {
|
|
820
|
+
active.value = false;
|
|
821
|
+
allPromises.splice(0, allPromises.length);
|
|
822
|
+
cb?.();
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
latestPromise = newAllSettled;
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
add,
|
|
829
|
+
active
|
|
830
|
+
};
|
|
769
831
|
};
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/composables/helpers/useLastSettled.ts
|
|
834
|
+
/**
|
|
835
|
+
* Execute a callback, when the latest promise is settled (either resolved or rejected).
|
|
836
|
+
* This ensures that out-of-order settling promises are ignored and only the latest promise is considered.
|
|
837
|
+
*
|
|
838
|
+
* @param cb callback to execute when the last promise, that was added to the queue, is settled.
|
|
839
|
+
* @returns the active state of the last settled promise and a queue function to add new promises to the queue.
|
|
840
|
+
*/
|
|
841
|
+
var useLastSettled = (cb) => {
|
|
842
|
+
const active = ref(false);
|
|
843
|
+
let lastId = -1;
|
|
844
|
+
const add = (promise) => {
|
|
845
|
+
const promiseId = ++lastId;
|
|
846
|
+
active.value = true;
|
|
847
|
+
const onFinally = (success) => (resolved) => {
|
|
848
|
+
if (promiseId === lastId) {
|
|
849
|
+
active.value = false;
|
|
850
|
+
if (success) cb(success, resolved);
|
|
851
|
+
else cb(success);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
promise.then(onFinally(true)).catch(onFinally(false));
|
|
855
|
+
};
|
|
856
|
+
const cancel = () => {
|
|
857
|
+
active.value = false;
|
|
858
|
+
lastId++;
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
active,
|
|
862
|
+
add,
|
|
863
|
+
cancel
|
|
864
|
+
};
|
|
793
865
|
};
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/composables/dataGrid/createDataGrid.ts
|
|
868
|
+
var COL_KEY_DATA_ATTR = "data-onyx-col-key";
|
|
869
|
+
var COL_INDEX_ARIA_ATTR = "aria-colindex";
|
|
870
|
+
var ROW_ID_DATA_ATTR = "data-onyx-row-id";
|
|
871
|
+
var ROW_INDEX_ARIA_ATTR = "aria-rowindex";
|
|
872
|
+
var StaticResolver = {
|
|
873
|
+
mapCellToIndex: (cell) => Array.from(cell.closest("tr")?.cells ?? []).indexOf(cell),
|
|
874
|
+
mapRowToIndex: (row) => Array.from(row.closest("table")?.rows ?? []).indexOf(row),
|
|
875
|
+
resolveCell: async (cellIndex, rowIndex, table) => table.rows.item(rowIndex)?.cells.item(cellIndex),
|
|
876
|
+
getTotalRows: (table) => table.rows.length,
|
|
877
|
+
getTotalCols: (table) => table.rows.item(0)?.cells.length ?? 0
|
|
804
878
|
};
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
await requestLazyLoad(cellIndex, rowIndex);
|
|
821
|
-
cell = queryCell();
|
|
822
|
-
if (cell) {
|
|
823
|
-
return cell;
|
|
824
|
-
}
|
|
825
|
-
throw new Error(
|
|
826
|
-
`Table cell with row index "${rowIndex}" and column index "${cellIndex}" was not found after requested lazy loading and is unable to be focused!`
|
|
827
|
-
);
|
|
828
|
-
},
|
|
829
|
-
getTotalRows: () => toValue(rows),
|
|
830
|
-
getTotalCols: () => toValue(cols)
|
|
879
|
+
var LazyResolverFactory = ({ rows, cols, requestLazyLoad }) => ({
|
|
880
|
+
mapCellToIndex: (cell) => Number(cell.getAttribute(COL_INDEX_ARIA_ATTR)) - 1,
|
|
881
|
+
mapRowToIndex: (row) => Number(row.getAttribute(ROW_INDEX_ARIA_ATTR)) - 1,
|
|
882
|
+
resolveCell: async (cellIndex, rowIndex, table) => {
|
|
883
|
+
const queryCell = () => table.querySelector(`*[${ROW_INDEX_ARIA_ATTR}="${rowIndex + 1}"] *[${COL_INDEX_ARIA_ATTR}="${cellIndex + 1}"]`);
|
|
884
|
+
let cell = queryCell();
|
|
885
|
+
if (cell) return cell;
|
|
886
|
+
await requestLazyLoad(cellIndex, rowIndex);
|
|
887
|
+
cell = queryCell();
|
|
888
|
+
if (cell) return cell;
|
|
889
|
+
throw new Error(`Table cell with row index "${rowIndex}" and column index "${cellIndex}" was not found after requested lazy loading and is unable to be focused!`);
|
|
890
|
+
},
|
|
891
|
+
getTotalRows: () => toValue(rows),
|
|
892
|
+
getTotalCols: () => toValue(cols)
|
|
831
893
|
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
role: "row"
|
|
969
|
-
}),
|
|
970
|
-
td: computed(() => ({ rowId, colKey, colIndex }) => {
|
|
971
|
-
const isSelected = colKey.toString() === selectedCell.value?.colKey.toString() && rowId.toString() === selectedCell.value?.rowId.toString();
|
|
972
|
-
return {
|
|
973
|
-
tabindex: isSelected ? "0" : "-1",
|
|
974
|
-
ref: isSelected ? selectedCellEl : void 0,
|
|
975
|
-
[COL_KEY_DATA_ATTR]: colKey.toString(),
|
|
976
|
-
// TODO: handle symbols
|
|
977
|
-
"aria-colindex": colIndex == void 0 ? void 0 : colIndex + 1,
|
|
978
|
-
role: "cell"
|
|
979
|
-
};
|
|
980
|
-
})
|
|
981
|
-
},
|
|
982
|
-
state: {
|
|
983
|
-
/**
|
|
984
|
-
* Indicates that the data grid expects a content change soon, e.g. because more or other data is loaded.
|
|
985
|
-
* If `loading` is passed in via the options, this will mirror its value.
|
|
986
|
-
* Otherwise it will be dynamically set based on the running state of the `requestLazyLoad` promises.
|
|
987
|
-
*/
|
|
988
|
-
busy
|
|
989
|
-
},
|
|
990
|
-
internals: {}
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
);
|
|
994
|
-
const createMenuButton = createBuilder((options) => {
|
|
995
|
-
const rootId = useId();
|
|
996
|
-
const menuId = useId();
|
|
997
|
-
const rootRef = createElRef();
|
|
998
|
-
const menuRef = createElRef();
|
|
999
|
-
const buttonId = useId();
|
|
1000
|
-
const position = computed(() => toValue(options.position) ?? "bottom");
|
|
1001
|
-
useGlobalEventListener({
|
|
1002
|
-
type: "keydown",
|
|
1003
|
-
listener: (e) => e.key === "Escape" && setExpanded(false),
|
|
1004
|
-
disabled: computed(() => !options.isExpanded.value)
|
|
1005
|
-
});
|
|
1006
|
-
const updateDebouncedExpanded = debounce(() => options.onToggle(), 200);
|
|
1007
|
-
watch(options.isExpanded, () => updateDebouncedExpanded.abort());
|
|
1008
|
-
const setExpanded = (expanded, debounced = false) => {
|
|
1009
|
-
if (options.disabled?.value) return;
|
|
1010
|
-
if (expanded === options.isExpanded.value) {
|
|
1011
|
-
updateDebouncedExpanded.abort();
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
if (debounced) {
|
|
1015
|
-
updateDebouncedExpanded();
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
options.onToggle();
|
|
1019
|
-
};
|
|
1020
|
-
const focusRelativeItem = (next) => {
|
|
1021
|
-
const currentMenuItem = document.activeElement;
|
|
1022
|
-
const currentMenu = currentMenuItem?.closest('[role="menu"]') || menuRef.value;
|
|
1023
|
-
if (!currentMenu) return;
|
|
1024
|
-
const menuItems = Array.from(currentMenu.querySelectorAll('[role="menuitem"]')).filter((item) => item.closest('[role="menu"]') === currentMenu);
|
|
1025
|
-
if (position.value === "top") menuItems.reverse();
|
|
1026
|
-
let nextIndex = 0;
|
|
1027
|
-
if (currentMenuItem) {
|
|
1028
|
-
const currentIndex = menuItems.indexOf(currentMenuItem);
|
|
1029
|
-
switch (next) {
|
|
1030
|
-
case "next":
|
|
1031
|
-
nextIndex = currentIndex + 1;
|
|
1032
|
-
break;
|
|
1033
|
-
case "prev":
|
|
1034
|
-
nextIndex = currentIndex - 1;
|
|
1035
|
-
break;
|
|
1036
|
-
case "first":
|
|
1037
|
-
nextIndex = 0;
|
|
1038
|
-
break;
|
|
1039
|
-
case "last":
|
|
1040
|
-
nextIndex = menuItems.length - 1;
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
const nextMenuItem = menuItems[nextIndex];
|
|
1045
|
-
nextMenuItem?.focus();
|
|
1046
|
-
};
|
|
1047
|
-
const handleKeydown = (event) => {
|
|
1048
|
-
switch (event.key) {
|
|
1049
|
-
case "ArrowDown":
|
|
1050
|
-
event.preventDefault();
|
|
1051
|
-
focusRelativeItem(position.value === "bottom" ? "next" : "prev");
|
|
1052
|
-
break;
|
|
1053
|
-
case "ArrowUp":
|
|
1054
|
-
event.preventDefault();
|
|
1055
|
-
focusRelativeItem(position.value === "bottom" ? "prev" : "next");
|
|
1056
|
-
break;
|
|
1057
|
-
case "Home":
|
|
1058
|
-
event.preventDefault();
|
|
1059
|
-
focusRelativeItem("first");
|
|
1060
|
-
break;
|
|
1061
|
-
case "End":
|
|
1062
|
-
event.preventDefault();
|
|
1063
|
-
focusRelativeItem("last");
|
|
1064
|
-
break;
|
|
1065
|
-
case " ":
|
|
1066
|
-
case "Enter":
|
|
1067
|
-
if (event.target instanceof HTMLInputElement) break;
|
|
1068
|
-
event.preventDefault();
|
|
1069
|
-
event.target.click();
|
|
1070
|
-
break;
|
|
1071
|
-
case "Escape":
|
|
1072
|
-
event.preventDefault();
|
|
1073
|
-
setExpanded(false);
|
|
1074
|
-
break;
|
|
1075
|
-
}
|
|
1076
|
-
};
|
|
1077
|
-
const triggerEvents = computed(() => {
|
|
1078
|
-
if (toValue(options.trigger) !== "hover") return;
|
|
1079
|
-
return {
|
|
1080
|
-
onMouseenter: () => setExpanded(true),
|
|
1081
|
-
onMouseleave: () => setExpanded(false, true)
|
|
1082
|
-
};
|
|
1083
|
-
});
|
|
1084
|
-
useOutsideClick({
|
|
1085
|
-
inside: rootRef,
|
|
1086
|
-
onOutsideClick: () => setExpanded(false),
|
|
1087
|
-
disabled: computed(() => !options.isExpanded.value),
|
|
1088
|
-
checkOnTab: true
|
|
1089
|
-
});
|
|
1090
|
-
return {
|
|
1091
|
-
elements: {
|
|
1092
|
-
root: computed(() => ({
|
|
1093
|
-
id: rootId,
|
|
1094
|
-
onKeydown: handleKeydown,
|
|
1095
|
-
ref: rootRef,
|
|
1096
|
-
...triggerEvents.value
|
|
1097
|
-
})),
|
|
1098
|
-
button: computed(
|
|
1099
|
-
() => ({
|
|
1100
|
-
"aria-controls": menuId,
|
|
1101
|
-
"aria-expanded": options.isExpanded.value,
|
|
1102
|
-
"aria-haspopup": true,
|
|
1103
|
-
onFocus: () => setExpanded(true, true),
|
|
1104
|
-
onClick: () => toValue(options.trigger) == "click" ? setExpanded(!options.isExpanded.value) : void 0,
|
|
1105
|
-
id: buttonId,
|
|
1106
|
-
disabled: options.disabled?.value
|
|
1107
|
-
})
|
|
1108
|
-
),
|
|
1109
|
-
menu: {
|
|
1110
|
-
id: menuId,
|
|
1111
|
-
ref: menuRef,
|
|
1112
|
-
role: "menu",
|
|
1113
|
-
"aria-labelledby": buttonId,
|
|
1114
|
-
onClick: () => setExpanded(false)
|
|
1115
|
-
},
|
|
1116
|
-
...createMenuItems().elements
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
894
|
+
var createDataGrid = createBuilder((options) => {
|
|
895
|
+
const tableElement = createElRef();
|
|
896
|
+
const lazy = options.lazy && toRef(options.lazy);
|
|
897
|
+
const busy = computed(() => toValue(options.loading) ?? busySet.active.value);
|
|
898
|
+
const resolver = lazy ? LazyResolverFactory({
|
|
899
|
+
cols: () => lazy.value.totalCols,
|
|
900
|
+
rows: () => lazy.value.totalRows,
|
|
901
|
+
requestLazyLoad: lazy.value.requestLazyLoad
|
|
902
|
+
}) : StaticResolver;
|
|
903
|
+
const labelId = useId();
|
|
904
|
+
const selectedCell = options.selectedCell || ref();
|
|
905
|
+
const selectedCellEl = createElRef();
|
|
906
|
+
/**
|
|
907
|
+
* Tracks if the latest `resolveCell` promise is finished, if successful the focus can be moved to the resolved cell.
|
|
908
|
+
*/
|
|
909
|
+
const focusQueue = useLastSettled((success, cell) => {
|
|
910
|
+
if (success) cell?.focus();
|
|
911
|
+
});
|
|
912
|
+
/**
|
|
913
|
+
* Tracks if any `resolveCell` promises are running.
|
|
914
|
+
*/
|
|
915
|
+
const busySet = useAllSettled();
|
|
916
|
+
const findFirstCell = () => tableElement.value?.querySelector?.(`[${ROW_ID_DATA_ATTR}] [${COL_KEY_DATA_ATTR}]`);
|
|
917
|
+
const setSelected = (element) => {
|
|
918
|
+
const colKey = element.closest(`[${COL_KEY_DATA_ATTR}]`)?.getAttribute(COL_KEY_DATA_ATTR);
|
|
919
|
+
const rowId = element.closest(`[${ROW_ID_DATA_ATTR}]`)?.getAttribute(ROW_ID_DATA_ATTR);
|
|
920
|
+
if (colKey && rowId) selectedCell.value = {
|
|
921
|
+
rowId,
|
|
922
|
+
colKey
|
|
923
|
+
};
|
|
924
|
+
};
|
|
925
|
+
const ensureTabTarget = () => {
|
|
926
|
+
if (selectedCell.value && selectedCellEl.value?.isConnected) return;
|
|
927
|
+
const firstCell = findFirstCell();
|
|
928
|
+
if (firstCell) setSelected(firstCell);
|
|
929
|
+
};
|
|
930
|
+
let mutationObserver;
|
|
931
|
+
onMounted(() => {
|
|
932
|
+
ensureTabTarget();
|
|
933
|
+
mutationObserver = new MutationObserver(ensureTabTarget);
|
|
934
|
+
watch(tableElement, () => {
|
|
935
|
+
if (!tableElement.value) return;
|
|
936
|
+
mutationObserver?.disconnect();
|
|
937
|
+
mutationObserver?.observe(tableElement.value, {
|
|
938
|
+
childList: true,
|
|
939
|
+
attributes: true,
|
|
940
|
+
subtree: true,
|
|
941
|
+
attributeFilter: ["value"]
|
|
942
|
+
});
|
|
943
|
+
}, { immediate: true });
|
|
944
|
+
});
|
|
945
|
+
onBeforeUnmount(() => {
|
|
946
|
+
mutationObserver?.disconnect();
|
|
947
|
+
});
|
|
948
|
+
/**
|
|
949
|
+
* when a cell is focused programmatically or by click, it becomes the currently selected cell.
|
|
950
|
+
*/
|
|
951
|
+
const onFocusin = (event) => {
|
|
952
|
+
setSelected(event.target);
|
|
953
|
+
focusQueue.cancel();
|
|
954
|
+
};
|
|
955
|
+
const onKeydown = (event) => {
|
|
956
|
+
const target = event.target;
|
|
957
|
+
const cellElement = target.closest("td, th");
|
|
958
|
+
const rowElement = target.closest("tr");
|
|
959
|
+
const tableElement = target.closest("table");
|
|
960
|
+
if (!cellElement || !rowElement || !tableElement) return;
|
|
961
|
+
const { getTotalRows, getTotalCols, mapRowToIndex, mapCellToIndex, resolveCell } = resolver;
|
|
962
|
+
const colIndex = mapCellToIndex(cellElement);
|
|
963
|
+
const rowIndex = mapRowToIndex(rowElement);
|
|
964
|
+
const totalRows = getTotalRows(tableElement);
|
|
965
|
+
const totalCols = getTotalCols(tableElement);
|
|
966
|
+
let newColIndex = colIndex;
|
|
967
|
+
let newRowIndex = rowIndex;
|
|
968
|
+
if (wasKeyPressed(event, {
|
|
969
|
+
ctrlKey: true,
|
|
970
|
+
key: "Home"
|
|
971
|
+
})) {
|
|
972
|
+
newColIndex = 0;
|
|
973
|
+
newRowIndex = 0;
|
|
974
|
+
} else if (wasKeyPressed(event, {
|
|
975
|
+
ctrlKey: true,
|
|
976
|
+
key: "End"
|
|
977
|
+
})) {
|
|
978
|
+
newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
|
|
979
|
+
newRowIndex = totalRows === "unknown" ? Infinity : totalRows - 1;
|
|
980
|
+
} else if (wasKeyPressed(event, "ArrowUp")) newRowIndex = rowIndex - 1;
|
|
981
|
+
else if (wasKeyPressed(event, "ArrowDown")) newRowIndex = rowIndex + 1;
|
|
982
|
+
else if (wasKeyPressed(event, "ArrowLeft")) newColIndex = colIndex - 1;
|
|
983
|
+
else if (wasKeyPressed(event, "ArrowRight")) newColIndex = colIndex + 1;
|
|
984
|
+
else if (wasKeyPressed(event, "Home")) newColIndex = 0;
|
|
985
|
+
else if (wasKeyPressed(event, "End")) newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
|
|
986
|
+
else return;
|
|
987
|
+
event.preventDefault();
|
|
988
|
+
const maxRows = totalRows === "unknown" ? Infinity : totalRows - 1;
|
|
989
|
+
newRowIndex = Math.max(Math.min(newRowIndex, maxRows), 0);
|
|
990
|
+
const maxCols = totalCols === "unknown" ? Infinity : totalCols - 1;
|
|
991
|
+
newColIndex = Math.max(Math.min(newColIndex, maxCols), 0);
|
|
992
|
+
(async () => {
|
|
993
|
+
const promiseResolveCell = resolveCell(newColIndex, newRowIndex, tableElement);
|
|
994
|
+
focusQueue.add(promiseResolveCell);
|
|
995
|
+
busySet.add(promiseResolveCell);
|
|
996
|
+
})();
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
elements: {
|
|
1000
|
+
label: { id: labelId },
|
|
1001
|
+
table: computed(() => ({
|
|
1002
|
+
ref: tableElement,
|
|
1003
|
+
onFocusin,
|
|
1004
|
+
onKeydown,
|
|
1005
|
+
role: "grid",
|
|
1006
|
+
"aria-busy": busy.value,
|
|
1007
|
+
"aria-labelledby": labelId,
|
|
1008
|
+
"aria-rowcount": lazy?.value.totalRows === "unknown" ? -1 : lazy?.value.totalRows,
|
|
1009
|
+
"aria-colcount": lazy?.value.totalCols === "unknown" ? -1 : lazy?.value.totalCols
|
|
1010
|
+
})),
|
|
1011
|
+
tr: ({ rowId, rowIndex }) => ({
|
|
1012
|
+
[ROW_ID_DATA_ATTR]: rowId.toString(),
|
|
1013
|
+
"aria-rowindex": rowIndex == void 0 ? void 0 : rowIndex + 1,
|
|
1014
|
+
role: "row"
|
|
1015
|
+
}),
|
|
1016
|
+
td: computed(() => ({ rowId, colKey, colIndex }) => {
|
|
1017
|
+
const isSelected = colKey.toString() === selectedCell.value?.colKey.toString() && rowId.toString() === selectedCell.value?.rowId.toString();
|
|
1018
|
+
return {
|
|
1019
|
+
tabindex: isSelected ? "0" : "-1",
|
|
1020
|
+
ref: isSelected ? selectedCellEl : void 0,
|
|
1021
|
+
[COL_KEY_DATA_ATTR]: colKey.toString(),
|
|
1022
|
+
"aria-colindex": colIndex == void 0 ? void 0 : colIndex + 1,
|
|
1023
|
+
role: "cell"
|
|
1024
|
+
};
|
|
1025
|
+
})
|
|
1026
|
+
},
|
|
1027
|
+
state: { busy },
|
|
1028
|
+
internals: {}
|
|
1029
|
+
};
|
|
1119
1030
|
});
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1031
|
+
//#endregion
|
|
1032
|
+
//#region src/composables/menuButton/createMenuButton.ts
|
|
1033
|
+
/**
|
|
1034
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
|
|
1035
|
+
*/
|
|
1036
|
+
var createMenuButton = createBuilder((options) => {
|
|
1037
|
+
const rootId = useId();
|
|
1038
|
+
const menuId = useId();
|
|
1039
|
+
const rootRef = createElRef();
|
|
1040
|
+
const menuRef = createElRef();
|
|
1041
|
+
const buttonId = useId();
|
|
1042
|
+
const position = computed(() => toValue(options.position) ?? "bottom");
|
|
1043
|
+
useGlobalEventListener({
|
|
1044
|
+
type: "keydown",
|
|
1045
|
+
listener: (e) => e.key === "Escape" && setExpanded(false),
|
|
1046
|
+
disabled: computed(() => !options.isExpanded.value)
|
|
1047
|
+
});
|
|
1048
|
+
/**
|
|
1049
|
+
* Debounced expanded state that will only be toggled after a given timeout.
|
|
1050
|
+
*/
|
|
1051
|
+
const updateDebouncedExpanded = debounce(() => options.onToggle(), 200);
|
|
1052
|
+
watch(options.isExpanded, () => updateDebouncedExpanded.abort());
|
|
1053
|
+
const setExpanded = (expanded, debounced = false) => {
|
|
1054
|
+
if (options.disabled?.value) return;
|
|
1055
|
+
if (expanded === options.isExpanded.value) {
|
|
1056
|
+
updateDebouncedExpanded.abort();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (debounced) {
|
|
1060
|
+
updateDebouncedExpanded();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
options.onToggle();
|
|
1064
|
+
};
|
|
1065
|
+
const focusRelativeItem = (next) => {
|
|
1066
|
+
const currentMenuItem = document.activeElement;
|
|
1067
|
+
const currentMenu = currentMenuItem?.closest("[role=\"menu\"]") || menuRef.value;
|
|
1068
|
+
if (!currentMenu) return;
|
|
1069
|
+
const menuItems = Array.from(currentMenu.querySelectorAll("[role=\"menuitem\"]")).filter((item) => item.closest("[role=\"menu\"]") === currentMenu);
|
|
1070
|
+
if (position.value === "top") menuItems.reverse();
|
|
1071
|
+
let nextIndex = 0;
|
|
1072
|
+
if (currentMenuItem) {
|
|
1073
|
+
const currentIndex = menuItems.indexOf(currentMenuItem);
|
|
1074
|
+
switch (next) {
|
|
1075
|
+
case "next":
|
|
1076
|
+
nextIndex = currentIndex + 1;
|
|
1077
|
+
break;
|
|
1078
|
+
case "prev":
|
|
1079
|
+
nextIndex = currentIndex - 1;
|
|
1080
|
+
break;
|
|
1081
|
+
case "first":
|
|
1082
|
+
nextIndex = 0;
|
|
1083
|
+
break;
|
|
1084
|
+
case "last":
|
|
1085
|
+
nextIndex = menuItems.length - 1;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
menuItems[nextIndex]?.focus();
|
|
1090
|
+
};
|
|
1091
|
+
const handleKeydown = (event) => {
|
|
1092
|
+
switch (event.key) {
|
|
1093
|
+
case "ArrowDown":
|
|
1094
|
+
event.preventDefault();
|
|
1095
|
+
focusRelativeItem(position.value === "bottom" ? "next" : "prev");
|
|
1096
|
+
break;
|
|
1097
|
+
case "ArrowUp":
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
focusRelativeItem(position.value === "bottom" ? "prev" : "next");
|
|
1100
|
+
break;
|
|
1101
|
+
case "Home":
|
|
1102
|
+
event.preventDefault();
|
|
1103
|
+
focusRelativeItem("first");
|
|
1104
|
+
break;
|
|
1105
|
+
case "End":
|
|
1106
|
+
event.preventDefault();
|
|
1107
|
+
focusRelativeItem("last");
|
|
1108
|
+
break;
|
|
1109
|
+
case " ":
|
|
1110
|
+
case "Enter":
|
|
1111
|
+
if (event.target instanceof HTMLInputElement) break;
|
|
1112
|
+
event.preventDefault();
|
|
1113
|
+
event.target.click();
|
|
1114
|
+
break;
|
|
1115
|
+
case "Escape":
|
|
1116
|
+
event.preventDefault();
|
|
1117
|
+
setExpanded(false);
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const triggerEvents = computed(() => {
|
|
1122
|
+
if (toValue(options.trigger) !== "hover") return;
|
|
1123
|
+
return {
|
|
1124
|
+
onMouseenter: () => setExpanded(true),
|
|
1125
|
+
onMouseleave: () => setExpanded(false, true)
|
|
1126
|
+
};
|
|
1127
|
+
});
|
|
1128
|
+
useOutsideClick({
|
|
1129
|
+
inside: rootRef,
|
|
1130
|
+
onOutsideClick: () => setExpanded(false),
|
|
1131
|
+
disabled: computed(() => !options.isExpanded.value),
|
|
1132
|
+
checkOnTab: true
|
|
1133
|
+
});
|
|
1134
|
+
return { elements: {
|
|
1135
|
+
root: computed(() => ({
|
|
1136
|
+
id: rootId,
|
|
1137
|
+
onKeydown: handleKeydown,
|
|
1138
|
+
ref: rootRef,
|
|
1139
|
+
...triggerEvents.value
|
|
1140
|
+
})),
|
|
1141
|
+
button: computed(() => ({
|
|
1142
|
+
"aria-controls": menuId,
|
|
1143
|
+
"aria-expanded": options.isExpanded.value,
|
|
1144
|
+
"aria-haspopup": true,
|
|
1145
|
+
onFocus: () => setExpanded(true, true),
|
|
1146
|
+
onClick: () => toValue(options.trigger) == "click" ? setExpanded(!options.isExpanded.value) : void 0,
|
|
1147
|
+
id: buttonId,
|
|
1148
|
+
disabled: options.disabled?.value
|
|
1149
|
+
})),
|
|
1150
|
+
menu: {
|
|
1151
|
+
id: menuId,
|
|
1152
|
+
ref: menuRef,
|
|
1153
|
+
role: "menu",
|
|
1154
|
+
"aria-labelledby": buttonId,
|
|
1155
|
+
onClick: () => setExpanded(false)
|
|
1156
|
+
},
|
|
1157
|
+
...createMenuItems().elements
|
|
1158
|
+
} };
|
|
1144
1159
|
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
* @param max - The maximum allowed value.
|
|
1174
|
-
* @returns The value representation of the percentage.
|
|
1175
|
-
*/
|
|
1176
|
-
percentToValue: (percent, min, max) => (max - min) * percent + min
|
|
1160
|
+
var createMenuItems = createBuilder((options) => {
|
|
1161
|
+
const onKeydown = (event) => {
|
|
1162
|
+
switch (event.key) {
|
|
1163
|
+
case "ArrowRight":
|
|
1164
|
+
case " ":
|
|
1165
|
+
case "Enter":
|
|
1166
|
+
event.preventDefault();
|
|
1167
|
+
options?.onOpen?.();
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
return { elements: {
|
|
1172
|
+
listItem: { role: "none" },
|
|
1173
|
+
menuItem: (data) => ({
|
|
1174
|
+
"aria-current": data.active ? "page" : void 0,
|
|
1175
|
+
"aria-disabled": data.disabled,
|
|
1176
|
+
role: "menuitem",
|
|
1177
|
+
onKeydown
|
|
1178
|
+
})
|
|
1179
|
+
} };
|
|
1180
|
+
});
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/utils/math.ts
|
|
1183
|
+
var MathUtils = {
|
|
1184
|
+
clamp: (number, min, max) => Math.max(Math.min(number, max), min),
|
|
1185
|
+
decimalsCount: (number) => String(number).split(".")[1]?.length ?? 0,
|
|
1186
|
+
valueToPercent: (value, min, max) => (value - min) * 100 / (max - min),
|
|
1187
|
+
percentToValue: (percent, min, max) => (max - min) * percent + min
|
|
1177
1188
|
};
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
};
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/composables/navigationMenu/createMenu.ts
|
|
1191
|
+
/**
|
|
1192
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/
|
|
1193
|
+
*/
|
|
1194
|
+
var createNavigationMenu = createBuilder(({ navigationName }) => {
|
|
1195
|
+
const navId = useId();
|
|
1196
|
+
const getMenuButtons = () => {
|
|
1197
|
+
const nav = navId ? document.getElementById(navId) : void 0;
|
|
1198
|
+
if (!nav) return [];
|
|
1199
|
+
return Array.from(nav.querySelectorAll("button[aria-expanded][aria-controls]"));
|
|
1200
|
+
};
|
|
1201
|
+
const focusRelative = (trigger, next) => {
|
|
1202
|
+
const menuButtons = getMenuButtons();
|
|
1203
|
+
const index = menuButtons.indexOf(trigger);
|
|
1204
|
+
if (index === -1) return;
|
|
1205
|
+
menuButtons[MathUtils.clamp(index + (next === "next" ? 1 : -1), 0, menuButtons.length - 1)]?.focus();
|
|
1206
|
+
};
|
|
1207
|
+
return { elements: { nav: {
|
|
1208
|
+
"aria-label": unref(navigationName),
|
|
1209
|
+
id: navId,
|
|
1210
|
+
onKeydown: (event) => {
|
|
1211
|
+
switch (event.key) {
|
|
1212
|
+
case "ArrowRight":
|
|
1213
|
+
focusRelative(event.target, "next");
|
|
1214
|
+
break;
|
|
1215
|
+
case "ArrowLeft":
|
|
1216
|
+
focusRelative(event.target, "previous");
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} } };
|
|
1214
1221
|
});
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/utils/array.ts
|
|
1224
|
+
var areArraysEqual = (arrayA, arrayB, comparer = (a, b) => a === b) => arrayA.length === arrayB.length && arrayA.every((value, index) => comparer(value, arrayB[index]));
|
|
1225
|
+
//#endregion
|
|
1226
|
+
//#region src/composables/slider/createSlider.ts
|
|
1227
|
+
var NAVIGATION_KEYS = new Set([
|
|
1228
|
+
"ArrowUp",
|
|
1229
|
+
"ArrowDown",
|
|
1230
|
+
"ArrowLeft",
|
|
1231
|
+
"ArrowRight",
|
|
1232
|
+
"PageUp",
|
|
1233
|
+
"PageDown",
|
|
1234
|
+
"Home",
|
|
1235
|
+
"End"
|
|
1236
|
+
]);
|
|
1237
|
+
var INCREMENT_KEYS = new Set([
|
|
1238
|
+
"ArrowRight",
|
|
1239
|
+
"ArrowUp",
|
|
1240
|
+
"PageUp"
|
|
1225
1241
|
]);
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const createTabs = createBuilder((options) => {
|
|
1425
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
1426
|
-
const getId = (value) => {
|
|
1427
|
-
if (!idMap.has(value)) {
|
|
1428
|
-
idMap.set(value, { tabId: useId(), panelId: useId() });
|
|
1429
|
-
}
|
|
1430
|
-
return idMap.get(value);
|
|
1431
|
-
};
|
|
1432
|
-
const handleKeydown = (event) => {
|
|
1433
|
-
const tab = event.target;
|
|
1434
|
-
const enabledTabs = Array.from(
|
|
1435
|
-
tab.parentElement?.querySelectorAll('[role="tab"]') ?? []
|
|
1436
|
-
).filter((tab2) => tab2.ariaDisabled !== "true");
|
|
1437
|
-
const currentTabIndex = enabledTabs.indexOf(tab);
|
|
1438
|
-
const focusElement = (element) => {
|
|
1439
|
-
if (element instanceof HTMLElement) element.focus();
|
|
1440
|
-
};
|
|
1441
|
-
const focusFirstTab = () => focusElement(enabledTabs.at(0));
|
|
1442
|
-
const focusLastTab = () => focusElement(enabledTabs.at(-1));
|
|
1443
|
-
const focusTab = (direction) => {
|
|
1444
|
-
if (currentTabIndex === -1) return;
|
|
1445
|
-
const newIndex = direction === "next" ? currentTabIndex + 1 : currentTabIndex - 1;
|
|
1446
|
-
if (newIndex < 0) {
|
|
1447
|
-
return focusLastTab();
|
|
1448
|
-
} else if (newIndex >= enabledTabs.length) {
|
|
1449
|
-
return focusFirstTab();
|
|
1450
|
-
}
|
|
1451
|
-
return focusElement(enabledTabs.at(newIndex));
|
|
1452
|
-
};
|
|
1453
|
-
switch (event.key) {
|
|
1454
|
-
case "ArrowRight":
|
|
1455
|
-
focusTab("next");
|
|
1456
|
-
break;
|
|
1457
|
-
case "ArrowLeft":
|
|
1458
|
-
focusTab("previous");
|
|
1459
|
-
break;
|
|
1460
|
-
case "Home":
|
|
1461
|
-
focusFirstTab();
|
|
1462
|
-
break;
|
|
1463
|
-
case "End":
|
|
1464
|
-
focusLastTab();
|
|
1465
|
-
break;
|
|
1466
|
-
case "Enter":
|
|
1467
|
-
case " ":
|
|
1468
|
-
{
|
|
1469
|
-
const tabEntry = Array.from(idMap.entries()).find(([, { tabId }]) => tabId === tab.id);
|
|
1470
|
-
if (tabEntry) options.onSelect?.(tabEntry[0]);
|
|
1471
|
-
}
|
|
1472
|
-
break;
|
|
1473
|
-
}
|
|
1474
|
-
};
|
|
1475
|
-
return {
|
|
1476
|
-
elements: {
|
|
1477
|
-
tablist: computed(() => ({
|
|
1478
|
-
role: "tablist",
|
|
1479
|
-
"aria-label": unref(options.label),
|
|
1480
|
-
onKeydown: handleKeydown
|
|
1481
|
-
})),
|
|
1482
|
-
tab: computed(() => {
|
|
1483
|
-
return (data) => {
|
|
1484
|
-
const { tabId: selectedTabId } = getId(unref(options.selectedTab));
|
|
1485
|
-
const { tabId, panelId } = getId(data.value);
|
|
1486
|
-
const isSelected = tabId === selectedTabId;
|
|
1487
|
-
return {
|
|
1488
|
-
id: tabId,
|
|
1489
|
-
role: "tab",
|
|
1490
|
-
"aria-selected": isSelected,
|
|
1491
|
-
"aria-controls": panelId,
|
|
1492
|
-
"aria-disabled": data.disabled ? true : void 0,
|
|
1493
|
-
onClick: () => options.onSelect?.(data.value),
|
|
1494
|
-
tabindex: isSelected && !data.disabled ? 0 : -1
|
|
1495
|
-
};
|
|
1496
|
-
};
|
|
1497
|
-
}),
|
|
1498
|
-
tabpanel: computed(() => {
|
|
1499
|
-
return (data) => {
|
|
1500
|
-
const { tabId, panelId } = getId(data.value);
|
|
1501
|
-
return {
|
|
1502
|
-
id: panelId,
|
|
1503
|
-
role: "tabpanel",
|
|
1504
|
-
"aria-labelledby": tabId
|
|
1505
|
-
};
|
|
1506
|
-
};
|
|
1507
|
-
})
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1242
|
+
var DECREMENT_KEYS = new Set([
|
|
1243
|
+
"ArrowLeft",
|
|
1244
|
+
"ArrowDown",
|
|
1245
|
+
"PageDown"
|
|
1246
|
+
]);
|
|
1247
|
+
/**
|
|
1248
|
+
* Composable for creating an accessibility-compliant slider.
|
|
1249
|
+
* For supported keyboard shortcuts, see: https://www.w3.org/WAI/ARIA/apg/patterns/slider/
|
|
1250
|
+
*/
|
|
1251
|
+
var createSlider = createBuilder((options) => {
|
|
1252
|
+
const sliderRef = createElRef();
|
|
1253
|
+
const firstThumbRef = createElRef();
|
|
1254
|
+
const secondThumbRef = createElRef();
|
|
1255
|
+
const min = computed(() => toValue(options.min) ?? 0);
|
|
1256
|
+
const max = computed(() => toValue(options.max) ?? 100);
|
|
1257
|
+
const step = computed(() => toValue(options.step) ?? 1);
|
|
1258
|
+
/**
|
|
1259
|
+
* Index of the thumb that is currently dragged.
|
|
1260
|
+
*/
|
|
1261
|
+
const draggingThumbIndex = ref();
|
|
1262
|
+
watch(draggingThumbIndex, (newThumbIndex) => {
|
|
1263
|
+
if (newThumbIndex == void 0) return;
|
|
1264
|
+
focusThumb(newThumbIndex);
|
|
1265
|
+
});
|
|
1266
|
+
const focusThumb = (index) => {
|
|
1267
|
+
if (index === 0) firstThumbRef.value?.focus();
|
|
1268
|
+
else if (index === 1) secondThumbRef.value?.focus();
|
|
1269
|
+
};
|
|
1270
|
+
const shiftStep = computed(() => {
|
|
1271
|
+
const shiftStep = toValue(options.shiftStep);
|
|
1272
|
+
if (shiftStep != void 0) return shiftStep;
|
|
1273
|
+
return Math.max(1, Math.round((max.value - min.value) * .1 / step.value)) * step.value;
|
|
1274
|
+
});
|
|
1275
|
+
/**
|
|
1276
|
+
* Normalizes the given slider (values) by ensuring that:
|
|
1277
|
+
* 1. Value is between min and max range
|
|
1278
|
+
* 2. Values are matching the `step` property (are multiples of it)
|
|
1279
|
+
* 3. Are sorted ascending (if range mode)
|
|
1280
|
+
*/
|
|
1281
|
+
const getNormalizedValue = computed(() => {
|
|
1282
|
+
return (value) => {
|
|
1283
|
+
let values = typeof value === "number" ? [value] : value;
|
|
1284
|
+
values = values.map((value) => MathUtils.clamp(value, min.value, max.value)).map((value) => {
|
|
1285
|
+
const stepDecimals = MathUtils.decimalsCount(step.value);
|
|
1286
|
+
return Number((Math.round((value - min.value) / step.value) * step.value + min.value).toFixed(stepDecimals));
|
|
1287
|
+
}).sort((a, b) => a - b);
|
|
1288
|
+
return values;
|
|
1289
|
+
};
|
|
1290
|
+
});
|
|
1291
|
+
/**
|
|
1292
|
+
* Current slider value(s) normalized to an array.
|
|
1293
|
+
*/
|
|
1294
|
+
const normalizedValue = computed(() => getNormalizedValue.value(toValue(options.value)));
|
|
1295
|
+
/**
|
|
1296
|
+
* Updates the current value with the given value. Will normalize the value.
|
|
1297
|
+
*/
|
|
1298
|
+
const updateValue = (value, index) => {
|
|
1299
|
+
const currentValue = normalizedValue.value.slice();
|
|
1300
|
+
const otherIndex = index === 0 ? 1 : 0;
|
|
1301
|
+
const otherValue = currentValue[otherIndex];
|
|
1302
|
+
if (otherValue != void 0) {
|
|
1303
|
+
if (index < otherIndex && value > otherValue) value = otherValue;
|
|
1304
|
+
else if (index > otherIndex && value < otherValue) value = otherValue;
|
|
1305
|
+
}
|
|
1306
|
+
currentValue[index] = value;
|
|
1307
|
+
const normalized = getNormalizedValue.value(currentValue);
|
|
1308
|
+
const newValue = normalized.length > 1 ? normalized : normalized[0];
|
|
1309
|
+
if (areArraysEqual(normalized, normalizedValue.value)) return;
|
|
1310
|
+
options.onChange?.(newValue);
|
|
1311
|
+
};
|
|
1312
|
+
/**
|
|
1313
|
+
* Increases / decreases the value by a single step. Will also ensure focus on the thumb.
|
|
1314
|
+
* Useful if e.g. adding custom buttons for changing the slider value.
|
|
1315
|
+
*/
|
|
1316
|
+
const updateValueByStep = (direction) => {
|
|
1317
|
+
const index = 0;
|
|
1318
|
+
const stepValue = direction === "increase" ? step.value : -step.value;
|
|
1319
|
+
const currentValue = normalizedValue.value[index];
|
|
1320
|
+
updateValue(currentValue + stepValue, index);
|
|
1321
|
+
focusThumb(index);
|
|
1322
|
+
};
|
|
1323
|
+
/**
|
|
1324
|
+
* Gets the given value in percentage relative to the sliders min/max range.
|
|
1325
|
+
*/
|
|
1326
|
+
const getValueInPercentage = computed(() => {
|
|
1327
|
+
return (value) => {
|
|
1328
|
+
const percentage = MathUtils.valueToPercent(value, min.value, max.value);
|
|
1329
|
+
return MathUtils.clamp(percentage, 0, 100);
|
|
1330
|
+
};
|
|
1331
|
+
});
|
|
1332
|
+
const handleKeydown = (event, thumbIndex) => {
|
|
1333
|
+
if (!NAVIGATION_KEYS.has(event.key)) return;
|
|
1334
|
+
event.preventDefault();
|
|
1335
|
+
const currentValue = normalizedValue.value.slice();
|
|
1336
|
+
if (currentValue[thumbIndex] == void 0) return;
|
|
1337
|
+
const stepSize = event.shiftKey ? shiftStep.value : step.value;
|
|
1338
|
+
if (event.key === "Home") return updateValue(min.value, thumbIndex);
|
|
1339
|
+
if (event.key === "End") return updateValue(max.value, thumbIndex);
|
|
1340
|
+
if (INCREMENT_KEYS.has(event.key)) updateValue(currentValue[thumbIndex] + stepSize, thumbIndex);
|
|
1341
|
+
else if (DECREMENT_KEYS.has(event.key)) updateValue(currentValue[thumbIndex] - stepSize, thumbIndex);
|
|
1342
|
+
};
|
|
1343
|
+
const handlePointerdown = (event) => {
|
|
1344
|
+
event.preventDefault();
|
|
1345
|
+
const value = getValueFromCoordinates(event.x);
|
|
1346
|
+
if (value == void 0) return;
|
|
1347
|
+
const thumb = normalizedValue.value.reduce((previous, thumbValue, index) => {
|
|
1348
|
+
const distance = Math.abs(thumbValue - value);
|
|
1349
|
+
if (distance < previous.distance) return {
|
|
1350
|
+
index,
|
|
1351
|
+
distance
|
|
1352
|
+
};
|
|
1353
|
+
return previous;
|
|
1354
|
+
}, {
|
|
1355
|
+
index: 0,
|
|
1356
|
+
distance: Number.POSITIVE_INFINITY
|
|
1357
|
+
});
|
|
1358
|
+
updateValue(value, thumb.index);
|
|
1359
|
+
draggingThumbIndex.value = thumb.index;
|
|
1360
|
+
};
|
|
1361
|
+
const handlePointermove = (event) => {
|
|
1362
|
+
if (draggingThumbIndex.value == void 0) return;
|
|
1363
|
+
const value = getValueFromCoordinates(event.x);
|
|
1364
|
+
if (value == void 0) return;
|
|
1365
|
+
updateValue(value, draggingThumbIndex.value);
|
|
1366
|
+
};
|
|
1367
|
+
useGlobalEventListener({
|
|
1368
|
+
type: "pointermove",
|
|
1369
|
+
listener: handlePointermove,
|
|
1370
|
+
disabled: computed(() => draggingThumbIndex.value == void 0)
|
|
1371
|
+
});
|
|
1372
|
+
const handlePointerup = () => {
|
|
1373
|
+
draggingThumbIndex.value = void 0;
|
|
1374
|
+
};
|
|
1375
|
+
useGlobalEventListener({
|
|
1376
|
+
type: "pointerup",
|
|
1377
|
+
listener: handlePointerup,
|
|
1378
|
+
disabled: computed(() => draggingThumbIndex.value == void 0)
|
|
1379
|
+
});
|
|
1380
|
+
/**
|
|
1381
|
+
* Gets the corresponding slider value for the given x coordinate across the rail.
|
|
1382
|
+
*/
|
|
1383
|
+
const getValueFromCoordinates = (x) => {
|
|
1384
|
+
const rect = sliderRef.value?.getBoundingClientRect();
|
|
1385
|
+
if (!rect || rect.width <= 0) return;
|
|
1386
|
+
const percent = MathUtils.clamp((x - rect.left) / rect.width, 0, 1);
|
|
1387
|
+
return MathUtils.percentToValue(percent, min.value, max.value);
|
|
1388
|
+
};
|
|
1389
|
+
return {
|
|
1390
|
+
elements: {
|
|
1391
|
+
root: computed(() => {
|
|
1392
|
+
const events = { onPointerdown: handlePointerdown };
|
|
1393
|
+
return {
|
|
1394
|
+
ref: sliderRef,
|
|
1395
|
+
style: { touchAction: "pan-y" },
|
|
1396
|
+
...toValue(options.disabled) ? void 0 : events
|
|
1397
|
+
};
|
|
1398
|
+
}),
|
|
1399
|
+
thumbInput: computed(() => (data) => {
|
|
1400
|
+
const events = { onKeydown: (event) => handleKeydown(event, data.index) };
|
|
1401
|
+
return {
|
|
1402
|
+
min: min.value,
|
|
1403
|
+
max: max.value,
|
|
1404
|
+
value: data.value,
|
|
1405
|
+
role: "slider",
|
|
1406
|
+
type: "range",
|
|
1407
|
+
"aria-label": toValue(options.label),
|
|
1408
|
+
"aria-valuemin": min.value,
|
|
1409
|
+
"aria-valuemax": max.value,
|
|
1410
|
+
"aria-valuenow": data.value,
|
|
1411
|
+
"aria-orientation": "horizontal",
|
|
1412
|
+
step: step.value,
|
|
1413
|
+
disabled: toValue(options.disabled),
|
|
1414
|
+
ref: data.index === 0 ? firstThumbRef : data.index === 1 ? secondThumbRef : void 0,
|
|
1415
|
+
...toValue(options.disabled) ? void 0 : events
|
|
1416
|
+
};
|
|
1417
|
+
})
|
|
1418
|
+
},
|
|
1419
|
+
state: {
|
|
1420
|
+
normalizedValue,
|
|
1421
|
+
shiftStep,
|
|
1422
|
+
track: computed(() => {
|
|
1423
|
+
const start = normalizedValue.value.length > 1 ? normalizedValue.value[0] : min.value;
|
|
1424
|
+
const startPercentage = getValueInPercentage.value(start);
|
|
1425
|
+
const end = normalizedValue.value.at(-1);
|
|
1426
|
+
return {
|
|
1427
|
+
start,
|
|
1428
|
+
startPercentage,
|
|
1429
|
+
end,
|
|
1430
|
+
endPercentage: getValueInPercentage.value(end)
|
|
1431
|
+
};
|
|
1432
|
+
})
|
|
1433
|
+
},
|
|
1434
|
+
internals: {
|
|
1435
|
+
getValueInPercentage,
|
|
1436
|
+
updateValue,
|
|
1437
|
+
updateValueByStep
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1510
1440
|
});
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/composables/tabs/createTabs.ts
|
|
1443
|
+
/**
|
|
1444
|
+
* Composable for implementing accessible tabs.
|
|
1445
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
1446
|
+
*/
|
|
1447
|
+
var createTabs = createBuilder((options) => {
|
|
1448
|
+
/**
|
|
1449
|
+
* Map for looking up tab and panel IDs for given tab keys/values defined by the user.
|
|
1450
|
+
* Key = custom value from the user, value = random generated tab and panel ID
|
|
1451
|
+
*/
|
|
1452
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1453
|
+
const getId = (value) => {
|
|
1454
|
+
if (!idMap.has(value)) idMap.set(value, {
|
|
1455
|
+
tabId: useId(),
|
|
1456
|
+
panelId: useId()
|
|
1457
|
+
});
|
|
1458
|
+
return idMap.get(value);
|
|
1459
|
+
};
|
|
1460
|
+
const handleKeydown = (event) => {
|
|
1461
|
+
const tab = event.target;
|
|
1462
|
+
const enabledTabs = Array.from(tab.parentElement?.querySelectorAll("[role=\"tab\"]") ?? []).filter((tab) => tab.ariaDisabled !== "true");
|
|
1463
|
+
const currentTabIndex = enabledTabs.indexOf(tab);
|
|
1464
|
+
const focusElement = (element) => {
|
|
1465
|
+
if (element instanceof HTMLElement) element.focus();
|
|
1466
|
+
};
|
|
1467
|
+
const focusFirstTab = () => focusElement(enabledTabs.at(0));
|
|
1468
|
+
const focusLastTab = () => focusElement(enabledTabs.at(-1));
|
|
1469
|
+
/**
|
|
1470
|
+
* Focuses the next/previous tab. Will ignore/skip disabled ones.
|
|
1471
|
+
*/
|
|
1472
|
+
const focusTab = (direction) => {
|
|
1473
|
+
if (currentTabIndex === -1) return;
|
|
1474
|
+
const newIndex = direction === "next" ? currentTabIndex + 1 : currentTabIndex - 1;
|
|
1475
|
+
if (newIndex < 0) return focusLastTab();
|
|
1476
|
+
else if (newIndex >= enabledTabs.length) return focusFirstTab();
|
|
1477
|
+
return focusElement(enabledTabs.at(newIndex));
|
|
1478
|
+
};
|
|
1479
|
+
switch (event.key) {
|
|
1480
|
+
case "ArrowRight":
|
|
1481
|
+
focusTab("next");
|
|
1482
|
+
break;
|
|
1483
|
+
case "ArrowLeft":
|
|
1484
|
+
focusTab("previous");
|
|
1485
|
+
break;
|
|
1486
|
+
case "Home":
|
|
1487
|
+
focusFirstTab();
|
|
1488
|
+
break;
|
|
1489
|
+
case "End":
|
|
1490
|
+
focusLastTab();
|
|
1491
|
+
break;
|
|
1492
|
+
case "Enter":
|
|
1493
|
+
case " ":
|
|
1494
|
+
{
|
|
1495
|
+
const tabEntry = Array.from(idMap.entries()).find(([, { tabId }]) => tabId === tab.id);
|
|
1496
|
+
if (tabEntry) options.onSelect?.(tabEntry[0]);
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
return { elements: {
|
|
1502
|
+
tablist: computed(() => ({
|
|
1503
|
+
role: "tablist",
|
|
1504
|
+
"aria-label": unref(options.label),
|
|
1505
|
+
onKeydown: handleKeydown
|
|
1506
|
+
})),
|
|
1507
|
+
tab: computed(() => {
|
|
1508
|
+
return (data) => {
|
|
1509
|
+
const { tabId: selectedTabId } = getId(unref(options.selectedTab));
|
|
1510
|
+
const { tabId, panelId } = getId(data.value);
|
|
1511
|
+
const isSelected = tabId === selectedTabId;
|
|
1512
|
+
return {
|
|
1513
|
+
id: tabId,
|
|
1514
|
+
role: "tab",
|
|
1515
|
+
"aria-selected": isSelected,
|
|
1516
|
+
"aria-controls": panelId,
|
|
1517
|
+
"aria-disabled": data.disabled ? true : void 0,
|
|
1518
|
+
onClick: () => options.onSelect?.(data.value),
|
|
1519
|
+
tabindex: isSelected && !data.disabled ? 0 : -1
|
|
1520
|
+
};
|
|
1521
|
+
};
|
|
1522
|
+
}),
|
|
1523
|
+
tabpanel: computed(() => {
|
|
1524
|
+
return (data) => {
|
|
1525
|
+
const { tabId, panelId } = getId(data.value);
|
|
1526
|
+
return {
|
|
1527
|
+
id: panelId,
|
|
1528
|
+
role: "tabpanel",
|
|
1529
|
+
"aria-labelledby": tabId
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
})
|
|
1533
|
+
} };
|
|
1525
1534
|
});
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1535
|
+
//#endregion
|
|
1536
|
+
//#region src/composables/toggleButton/createToggleButton.ts
|
|
1537
|
+
/**
|
|
1538
|
+
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/button/#:~:text=Toggle%20button
|
|
1539
|
+
*/
|
|
1540
|
+
var createToggleButton = createBuilder((options) => {
|
|
1541
|
+
return { elements: { button: computed(() => ({
|
|
1542
|
+
"aria-pressed": toValue(options.isPressed),
|
|
1543
|
+
onClick: options.onToggle
|
|
1544
|
+
})) } };
|
|
1534
1545
|
});
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
trigger: computed(() => ({
|
|
1548
|
-
id: triggerId,
|
|
1549
|
-
onClick: toggle,
|
|
1550
|
-
"aria-label": toValue(toggleLabel)
|
|
1551
|
-
})),
|
|
1552
|
-
/**
|
|
1553
|
-
* The element with the relevant toggletip content.
|
|
1554
|
-
* Only simple, textual content is allowed.
|
|
1555
|
-
*/
|
|
1556
|
-
tooltip: {
|
|
1557
|
-
onToggle: (e) => {
|
|
1558
|
-
const tooltip = e.target;
|
|
1559
|
-
_isVisible.value = tooltip.matches(":popover-open");
|
|
1560
|
-
},
|
|
1561
|
-
anchor: triggerId,
|
|
1562
|
-
popover: "auto",
|
|
1563
|
-
role: "status",
|
|
1564
|
-
tabindex: "-1"
|
|
1565
|
-
}
|
|
1566
|
-
},
|
|
1567
|
-
state: {
|
|
1568
|
-
isVisible: _isVisible
|
|
1569
|
-
}
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
);
|
|
1573
|
-
const createTooltip = createBuilder(({ debounce: debounce2, isVisible }) => {
|
|
1574
|
-
const tooltipId = useId();
|
|
1575
|
-
const _isVisible = toRef(isVisible ?? false);
|
|
1576
|
-
let timeout;
|
|
1577
|
-
const debouncedVisible = computed({
|
|
1578
|
-
get: () => _isVisible.value,
|
|
1579
|
-
set: (newValue) => {
|
|
1580
|
-
clearTimeout(timeout);
|
|
1581
|
-
timeout = setTimeout(() => {
|
|
1582
|
-
_isVisible.value = newValue;
|
|
1583
|
-
}, toValue(debounce2));
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
const hoverEvents = {
|
|
1587
|
-
onMouseover: () => debouncedVisible.value = true,
|
|
1588
|
-
onMouseout: () => debouncedVisible.value = false,
|
|
1589
|
-
onFocusin: () => _isVisible.value = true,
|
|
1590
|
-
onFocusout: () => _isVisible.value = false
|
|
1591
|
-
};
|
|
1592
|
-
useDismissible({ isExpanded: _isVisible });
|
|
1593
|
-
return {
|
|
1594
|
-
elements: {
|
|
1595
|
-
/**
|
|
1596
|
-
* The element which controls the tooltip visibility on hover.
|
|
1597
|
-
*/
|
|
1598
|
-
trigger: {
|
|
1599
|
-
"aria-describedby": tooltipId,
|
|
1600
|
-
...hoverEvents
|
|
1601
|
-
},
|
|
1602
|
-
/**
|
|
1603
|
-
* The element describing the tooltip.
|
|
1604
|
-
* Only simple, textual and non-focusable content is allowed.
|
|
1605
|
-
*/
|
|
1606
|
-
tooltip: {
|
|
1607
|
-
popover: "manual",
|
|
1608
|
-
role: "tooltip",
|
|
1609
|
-
id: tooltipId,
|
|
1610
|
-
tabindex: "-1",
|
|
1611
|
-
...hoverEvents
|
|
1612
|
-
}
|
|
1613
|
-
},
|
|
1614
|
-
state: {
|
|
1615
|
-
isVisible: _isVisible
|
|
1616
|
-
}
|
|
1617
|
-
};
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region src/composables/helpers/useDismissible.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* Composable that sets `isExpanded` to false, when the `Escape` key is pressed.
|
|
1550
|
+
* Addresses the "dismissible" aspect of https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html
|
|
1551
|
+
*/
|
|
1552
|
+
var useDismissible = ({ isExpanded }) => useGlobalEventListener({
|
|
1553
|
+
type: "keydown",
|
|
1554
|
+
listener: (e) => {
|
|
1555
|
+
if (e.key === "Escape") isExpanded.value = false;
|
|
1556
|
+
},
|
|
1557
|
+
disabled: computed(() => !isExpanded.value)
|
|
1618
1558
|
});
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1559
|
+
//#endregion
|
|
1560
|
+
//#region src/composables/tooltip/createToggletip.ts
|
|
1561
|
+
/**
|
|
1562
|
+
* Create a toggletip as described in https://inclusive-components.design/tooltips-toggletips/
|
|
1563
|
+
* Its visibility is toggled on click.
|
|
1564
|
+
* Therefore a toggletip MUST NOT be used to describe the associated trigger element.
|
|
1565
|
+
* Commonly this pattern uses a button with the ⓘ as the trigger element.
|
|
1566
|
+
* To describe the associated element use `createTooltip`.
|
|
1567
|
+
*/
|
|
1568
|
+
var createToggletip = createBuilder(({ toggleLabel, isVisible }) => {
|
|
1569
|
+
const triggerId = useId();
|
|
1570
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
1571
|
+
useDismissible({ isExpanded: _isVisible });
|
|
1572
|
+
const toggle = () => _isVisible.value = !_isVisible.value;
|
|
1573
|
+
return {
|
|
1574
|
+
elements: {
|
|
1575
|
+
trigger: computed(() => ({
|
|
1576
|
+
id: triggerId,
|
|
1577
|
+
onClick: toggle,
|
|
1578
|
+
"aria-label": toValue(toggleLabel)
|
|
1579
|
+
})),
|
|
1580
|
+
tooltip: {
|
|
1581
|
+
onToggle: (e) => {
|
|
1582
|
+
_isVisible.value = e.target.matches(":popover-open");
|
|
1583
|
+
},
|
|
1584
|
+
anchor: triggerId,
|
|
1585
|
+
popover: "auto",
|
|
1586
|
+
role: "status",
|
|
1587
|
+
tabindex: "-1"
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
state: { isVisible: _isVisible }
|
|
1591
|
+
};
|
|
1592
|
+
});
|
|
1593
|
+
//#endregion
|
|
1594
|
+
//#region src/composables/tooltip/createTooltip.ts
|
|
1595
|
+
/**
|
|
1596
|
+
* Create a tooltip as described in https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
|
|
1597
|
+
* Its visibility is toggled on hover or focus.
|
|
1598
|
+
* A tooltip MUST be used to describe the associated trigger element. E.g. The usage with the ⓘ would be incorrect.
|
|
1599
|
+
* To provide contextual information use the `createToggletip`.
|
|
1600
|
+
*/
|
|
1601
|
+
var createTooltip = createBuilder(({ debounce, isVisible }) => {
|
|
1602
|
+
const tooltipId = useId();
|
|
1603
|
+
const _isVisible = toRef(isVisible ?? false);
|
|
1604
|
+
let timeout;
|
|
1605
|
+
/**
|
|
1606
|
+
* Debounced visible state that will only be toggled after a given timeout.
|
|
1607
|
+
*/
|
|
1608
|
+
const debouncedVisible = computed({
|
|
1609
|
+
get: () => _isVisible.value,
|
|
1610
|
+
set: (newValue) => {
|
|
1611
|
+
clearTimeout(timeout);
|
|
1612
|
+
timeout = setTimeout(() => {
|
|
1613
|
+
_isVisible.value = newValue;
|
|
1614
|
+
}, toValue(debounce));
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
const hoverEvents = {
|
|
1618
|
+
onMouseover: () => debouncedVisible.value = true,
|
|
1619
|
+
onMouseout: () => debouncedVisible.value = false,
|
|
1620
|
+
onFocusin: () => _isVisible.value = true,
|
|
1621
|
+
onFocusout: () => _isVisible.value = false
|
|
1622
|
+
};
|
|
1623
|
+
useDismissible({ isExpanded: _isVisible });
|
|
1624
|
+
return {
|
|
1625
|
+
elements: {
|
|
1626
|
+
trigger: {
|
|
1627
|
+
"aria-describedby": tooltipId,
|
|
1628
|
+
...hoverEvents
|
|
1629
|
+
},
|
|
1630
|
+
tooltip: {
|
|
1631
|
+
popover: "manual",
|
|
1632
|
+
role: "tooltip",
|
|
1633
|
+
id: tooltipId,
|
|
1634
|
+
tabindex: "-1",
|
|
1635
|
+
...hoverEvents
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
state: { isVisible: _isVisible }
|
|
1639
|
+
};
|
|
1640
|
+
});
|
|
1641
|
+
//#endregion
|
|
1642
|
+
export { CLOSING_KEYS, OPENING_KEYS, _unstableCreateCalendar, createBuilder, createComboBox, createDataGrid, createElRef, createListbox, createMenuButton, createMenuItems, createNavigationMenu, createSlider, createTabs, createToggleButton, createToggletip, createTooltip, debounce, getNativeElement, isPrintableCharacter, useGlobalEventListener, useOutsideClick, wasKeyPressed };
|