@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/index.js CHANGED
@@ -1,1642 +1,1642 @@
1
- import { shallowRef, computed, toValue, ref, watch, nextTick, reactive, onBeforeMount, watchEffect, onBeforeUnmount, unref, useId, toRef, onMounted } from "vue";
2
- const createBuilder = (builder) => builder;
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
- const elementRef = shallowRef(null);
5
- return computed({
6
- set: (ref2) => {
7
- const element = Array.isArray(ref2) ? ref2[0] : ref2;
8
- elementRef.value = getNativeElement(element);
9
- },
10
- get: () => elementRef.value
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
- const getNativeElement = (element) => {
14
- if (element && typeof element === "object" && "$el" in element) {
15
- return element.$el;
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
- const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
21
- const dayNum = d.getUTCDay() || 7;
22
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
23
- const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
24
- const weekNo = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
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
- start = new Date(start);
29
- start.setHours(0, 0, 0, 0);
30
- end = new Date(end);
31
- end.setHours(23, 59, 59, 999);
32
- const time = date.getTime();
33
- return time >= start.getTime() && time <= end.getTime();
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
- const day = date.getDay();
37
- const start = WEEKDAYS.indexOf(weekStartDay);
38
- const normalizedDay = day === 0 ? 6 : day - 1;
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
- const WEEKDAYS = [
42
- "Monday",
43
- "Tuesday",
44
- "Wednesday",
45
- "Thursday",
46
- "Friday",
47
- "Saturday",
48
- "Sunday"
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
- const start = new Date(range.start);
52
- const end = range.end ? new Date(range.end) : void 0;
53
- if (end && end.getTime() < start.getTime()) {
54
- return { start: end, end: start };
55
- }
56
- return { start, end };
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
- const _unstableCreateCalendar = createBuilder((options) => {
59
- const viewMonth = computed({
60
- get: () => {
61
- const date = toValue(options.viewMonth);
62
- return date ? new Date(date) : /* @__PURE__ */ new Date();
63
- },
64
- set: (newValue) => options.onUpdateViewMonth?.(new Date(newValue))
65
- });
66
- const weekdayNames = computed(() => {
67
- const formatter = new Intl.DateTimeFormat(toValue(options.locale), {
68
- weekday: toValue(options.calendarSize) === "big" ? "long" : "short"
69
- });
70
- const names = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, 1 + i)).map(
71
- (day) => formatter.format(day)
72
- );
73
- const weekStartDay = toValue(options.weekStartDay);
74
- const index = WEEKDAYS.indexOf(weekStartDay);
75
- return names.slice(index).concat(names.slice(0, index));
76
- });
77
- const initialFocusedDate = () => {
78
- const today = /* @__PURE__ */ new Date();
79
- const view = viewMonth.value;
80
- const isTodayInViewMonth = today.getFullYear() === view.getFullYear() && today.getMonth() === view.getMonth();
81
- if (isTodayInViewMonth) {
82
- return today;
83
- } else {
84
- return new Date(view.getFullYear(), view.getMonth(), 1);
85
- }
86
- };
87
- const focusedDate = ref(initialFocusedDate());
88
- watch(
89
- () => toValue(options.modelValue),
90
- (newValue) => {
91
- if (!newValue) return;
92
- let newFocusDate;
93
- if (Array.isArray(newValue)) {
94
- newFocusDate = newValue.length ? new Date(newValue[0]) : void 0;
95
- } else if (typeof newValue === "object" && !(newValue instanceof Date)) {
96
- newFocusDate = new Date(newValue.start);
97
- } else {
98
- newFocusDate = new Date(newValue);
99
- }
100
- if (newFocusDate) {
101
- focusedDate.value = newFocusDate;
102
- viewMonth.value = newFocusDate;
103
- }
104
- },
105
- { immediate: true }
106
- );
107
- const isToday = (date) => {
108
- return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
109
- };
110
- const isSelected = computed(() => {
111
- return (date) => {
112
- const value = toValue(options.modelValue);
113
- if (!value) return false;
114
- if (Array.isArray(value)) {
115
- const values = value.map((i) => new Date(i));
116
- return values.some((d) => d.toDateString() === date.toDateString());
117
- }
118
- if (typeof value === "object" && !(value instanceof Date)) {
119
- const start = new Date(value.start);
120
- const end = value.end ? new Date(value.end) : void 0;
121
- return start.toDateString() === date.toDateString() || end?.toDateString() === date.toDateString();
122
- }
123
- return new Date(value).toDateString() === date.toDateString();
124
- };
125
- });
126
- const isFocused = computed(() => {
127
- return (date) => {
128
- return focusedDate.value?.toDateString() === date.toDateString();
129
- };
130
- });
131
- const isDisabled = computed(() => {
132
- return (date) => {
133
- const disabledPropValue = toValue(options.disabled);
134
- if (typeof disabledPropValue === "boolean") {
135
- if (disabledPropValue) return true;
136
- }
137
- if (typeof disabledPropValue === "function") {
138
- if (disabledPropValue(date)) return true;
139
- }
140
- const min = toValue(options.min);
141
- const minDate = min ? new Date(min) : void 0;
142
- minDate?.setHours(0, 0, 0, 0);
143
- const max = toValue(options.max);
144
- const maxDate = max ? new Date(max) : void 0;
145
- maxDate?.setHours(23, 59, 59, 999);
146
- if (minDate && maxDate) return !isInDateRange(date, minDate, maxDate);
147
- if (minDate) return date.getTime() < minDate.getTime();
148
- if (maxDate) return date.getTime() > maxDate.getTime();
149
- return false;
150
- };
151
- });
152
- const weeksToRender = computed(() => {
153
- const weekStartDay = toValue(options.weekStartDay);
154
- const firstDayInViewMonth = new Date(
155
- viewMonth.value.getFullYear(),
156
- viewMonth.value.getMonth(),
157
- 1
158
- );
159
- const startOffset = (firstDayInViewMonth.getDay() + 6) % 7;
160
- const daysBeforeStart = (startOffset - WEEKDAYS.indexOf(weekStartDay) + 7) % 7;
161
- const weeksToRender2 = 6;
162
- const weeks = [];
163
- for (let weekIndex = 0; weekIndex < weeksToRender2; weekIndex++) {
164
- const weekStartDate = new Date(firstDayInViewMonth);
165
- weekStartDate.setDate(weekStartDate.getDate() - daysBeforeStart + weekIndex * 7);
166
- const days = Array.from({ length: 7 }, (_, dayIndex) => {
167
- const date = new Date(weekStartDate);
168
- date.setDate(date.getDate() + dayIndex);
169
- return {
170
- date,
171
- isCurrentMonth: date.getMonth() === viewMonth.value.getMonth()
172
- };
173
- });
174
- weeks.push({
175
- weekNumber: getISOWeekNumber(weekStartDate),
176
- days
177
- });
178
- }
179
- return weeks;
180
- });
181
- const goToDate = (date, preventSelectionUpdate) => {
182
- focusedDate.value = new Date(date);
183
- if (focusedDate.value.getFullYear() !== viewMonth.value.getFullYear() || focusedDate.value.getMonth() !== viewMonth.value.getMonth()) {
184
- viewMonth.value = new Date(focusedDate.value);
185
- }
186
- const selectionMode = toValue(options.selectionMode);
187
- if (!selectionMode || preventSelectionUpdate) return;
188
- const selection = toValue(options.modelValue);
189
- switch (selectionMode) {
190
- case "single":
191
- options.onUpdateModelValue?.(new Date(date));
192
- break;
193
- case "multiple": {
194
- let values = Array.isArray(selection) ? selection.map((d) => new Date(d)) : [];
195
- if (isSelected.value(date)) {
196
- values = values.filter((d) => d.toDateString() !== date.toDateString());
197
- } else {
198
- values.push(new Date(date));
199
- }
200
- options.onUpdateModelValue?.(values);
201
- break;
202
- }
203
- case "range": {
204
- const currentRange = typeof selection === "object" && !(selection instanceof Date) && !Array.isArray(selection) ? selection : void 0;
205
- let newRange;
206
- if (currentRange?.start && currentRange.end) {
207
- newRange = { start: new Date(date) };
208
- } else {
209
- newRange = {
210
- start: currentRange?.start ? new Date(currentRange.start) : new Date(date),
211
- end: currentRange?.start ? new Date(date) : void 0
212
- };
213
- }
214
- newRange = sortDateRange(newRange);
215
- options.onUpdateModelValue?.(newRange);
216
- break;
217
- }
218
- }
219
- };
220
- const handleKeyNavigation = async (event) => {
221
- let newDate;
222
- const getNewFocusDateByDiff = (diff, type = "days") => {
223
- const date = new Date(focusedDate.value);
224
- if (type === "month") date.setMonth(date.getMonth() + diff);
225
- else date.setDate(date.getDate() + diff);
226
- return date;
227
- };
228
- switch (event.key) {
229
- case "ArrowUp":
230
- newDate = getNewFocusDateByDiff(-7);
231
- break;
232
- case "ArrowDown":
233
- newDate = getNewFocusDateByDiff(7);
234
- break;
235
- case "ArrowLeft":
236
- newDate = getNewFocusDateByDiff(-1);
237
- break;
238
- case "ArrowRight":
239
- newDate = getNewFocusDateByDiff(1);
240
- break;
241
- case "Home": {
242
- const idx = getNormalizedDayIndex(focusedDate.value, toValue(options.weekStartDay));
243
- newDate = getNewFocusDateByDiff(-idx);
244
- break;
245
- }
246
- case "End": {
247
- const idx = getNormalizedDayIndex(focusedDate.value, toValue(options.weekStartDay));
248
- newDate = getNewFocusDateByDiff(6 - idx);
249
- break;
250
- }
251
- case "PageUp":
252
- newDate = getNewFocusDateByDiff(-(event.shiftKey ? 12 : 1), "month");
253
- break;
254
- case "PageDown":
255
- newDate = getNewFocusDateByDiff(event.shiftKey ? 12 : 1, "month");
256
- break;
257
- }
258
- if (!newDate || isDisabled.value(newDate)) return;
259
- event.preventDefault();
260
- goToDate(newDate, true);
261
- await nextTick();
262
- tableRef.value?.querySelector(`[data-date="${focusedDate.value.toDateString()}"]`)?.focus();
263
- };
264
- const getRangeType = computed(() => {
265
- return (date, range) => {
266
- const selection = range ?? toValue(options.modelValue);
267
- if (!selection || typeof selection !== "object" || Array.isArray(selection) || selection instanceof Date) {
268
- return;
269
- }
270
- const sortedRange = sortDateRange(selection);
271
- const start = new Date(sortedRange.start);
272
- start.setHours(0, 0, 0, 0);
273
- const end = sortedRange.end ? new Date(sortedRange.end) : void 0;
274
- end?.setHours(23, 59, 59, 999);
275
- if (date.toDateString() === start.toDateString() && date.toDateString() === end?.toDateString())
276
- return;
277
- if (date.toDateString() === start.toDateString()) return "start";
278
- if (date.toDateString() === end?.toDateString()) return "end";
279
- if (end && isInDateRange(date, start, end)) return "middle";
280
- };
281
- });
282
- const goToMonthByOffset = (offset) => {
283
- const date = new Date(viewMonth.value);
284
- date.setMonth(date.getMonth() + offset, 1);
285
- viewMonth.value = date;
286
- };
287
- const goToToday = () => {
288
- viewMonth.value = /* @__PURE__ */ new Date();
289
- };
290
- const tableRef = createElRef();
291
- return {
292
- state: {
293
- weekdayNames,
294
- weeksToRender,
295
- focusedDate,
296
- viewMonth
297
- },
298
- elements: {
299
- table: {
300
- role: "grid",
301
- onKeydown: handleKeyNavigation,
302
- ref: tableRef
303
- },
304
- cell: computed(() => (cell) => ({
305
- role: "gridcell",
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
- const isSubsetMatching = (subset, target) => Object.entries(subset).every(
339
- ([key, value]) => target[key] === value
340
- );
341
- const wasKeyPressed = (event, key) => {
342
- if (typeof key === "string") {
343
- return event.key === key;
344
- }
345
- return isSubsetMatching(
346
- { altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, ...key },
347
- event
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
- const GRAPHEME_SEGMENTER = new Intl.Segmenter("en-US");
351
- const isPrintableCharacter = (key) => [...GRAPHEME_SEGMENTER.segment(key)].length === 1;
352
- const GLOBAL_LISTENERS = reactive(/* @__PURE__ */ new Map());
353
- const updateRemainingListeners = (type, remaining) => {
354
- if (remaining?.size) {
355
- GLOBAL_LISTENERS.set(type, remaining);
356
- return;
357
- }
358
- GLOBAL_LISTENERS.delete(type);
359
- document.removeEventListener(type, GLOBAL_HANDLER);
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
- const removeGlobalListener = (type, listener) => {
362
- const globalListener = GLOBAL_LISTENERS.get(type);
363
- globalListener?.delete(listener);
364
- updateRemainingListeners(type, globalListener);
411
+ var removeGlobalListener = (type, listener) => {
412
+ const globalListener = GLOBAL_LISTENERS.get(type);
413
+ globalListener?.delete(listener);
414
+ updateRemainingListeners(type, globalListener);
365
415
  };
366
- const addGlobalListener = (type, listener) => {
367
- const globalListener = GLOBAL_LISTENERS.get(type) ?? /* @__PURE__ */ new Set();
368
- globalListener.add(listener);
369
- GLOBAL_LISTENERS.set(type, globalListener);
370
- document.addEventListener(type, GLOBAL_HANDLER);
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
- const GLOBAL_HANDLER = (event) => {
373
- const type = event.type;
374
- GLOBAL_LISTENERS.get(type)?.forEach((cb) => cb(event));
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
- const useGlobalEventListener = ({
377
- type,
378
- listener,
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
- const useOutsideClick = ({
389
- inside,
390
- onOutsideClick,
391
- disabled,
392
- checkOnTab
393
- }) => {
394
- const isOutsideClick = (target) => {
395
- if (!target) return true;
396
- const raw = toValue(inside);
397
- const elements = Array.isArray(raw) ? raw : [raw];
398
- return !elements.some((element) => {
399
- const nativeEl = getNativeElement(element);
400
- return nativeEl?.contains(target);
401
- });
402
- };
403
- const clickListener = (event) => {
404
- if (isOutsideClick(event.target)) onOutsideClick(event);
405
- };
406
- useGlobalEventListener({ type: "mousedown", listener: clickListener, disabled });
407
- if (checkOnTab) {
408
- const keydownListener = (event) => {
409
- if (event.key !== "Tab") return;
410
- if (isOutsideClick(document.activeElement)) {
411
- onOutsideClick(event);
412
- }
413
- const controller = new AbortController();
414
- const { signal } = controller;
415
- const onFocusIn = (event2) => {
416
- const target = event2.target;
417
- if (isOutsideClick(target)) {
418
- onOutsideClick(event2);
419
- }
420
- controller.abort();
421
- };
422
- const onWindowBlur = (event2) => {
423
- if (isOutsideClick(document.activeElement)) {
424
- onOutsideClick(event2);
425
- }
426
- controller.abort();
427
- };
428
- const onKeyUp = (e) => {
429
- if (e.key === "Tab") controller.abort();
430
- };
431
- document.addEventListener("focusin", onFocusIn, {
432
- once: true,
433
- capture: true,
434
- // Capture phase to handle before component focus handlers
435
- signal
436
- });
437
- window.addEventListener("blur", onWindowBlur, { once: true, signal });
438
- document.addEventListener("keyup", onKeyUp, {
439
- once: true,
440
- capture: true,
441
- // Capture phase to prevent conflicts with prevented keydown
442
- signal
443
- });
444
- };
445
- useGlobalEventListener({ type: "keydown", listener: keydownListener, disabled });
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
- const debounce = (handler, timeout) => {
449
- let timer;
450
- const func = (...lastArgs) => {
451
- clearTimeout(timer);
452
- timer = setTimeout(() => handler(...lastArgs), toValue(timeout));
453
- };
454
- func.abort = () => clearTimeout(timer);
455
- return func;
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
- const useTypeAhead = (callback, timeout = 500) => {
458
- let inputString = "";
459
- const debouncedReset = debounce(() => inputString = "", timeout);
460
- return (event) => {
461
- if (!isPrintableCharacter(event.key)) {
462
- return;
463
- }
464
- debouncedReset();
465
- inputString = `${inputString}${event.key}`;
466
- callback(inputString);
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
- const createListbox = createBuilder(
470
- (options) => {
471
- const isMultiselect = computed(() => unref(options.multiple) ?? false);
472
- const isExpanded = computed(() => unref(options.isExpanded) ?? false);
473
- const descendantKeyIdMap = /* @__PURE__ */ new Map();
474
- const getOptionId = (value) => {
475
- if (!descendantKeyIdMap.has(value)) {
476
- descendantKeyIdMap.set(value, useId());
477
- }
478
- return descendantKeyIdMap.get(value);
479
- };
480
- const getOptionValueById = (id) => {
481
- const entries = Array.from(descendantKeyIdMap.entries());
482
- return entries.find(([_value, key]) => key === id)?.[0];
483
- };
484
- const isFocused = ref(false);
485
- watchEffect(async () => {
486
- if (!isExpanded.value || options.activeOption.value == void 0 || !isFocused.value && !options.controlled) {
487
- return;
488
- }
489
- const id = getOptionId(options.activeOption.value);
490
- await nextTick();
491
- document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
492
- });
493
- const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
494
- const handleKeydown = (event) => {
495
- switch (event.key) {
496
- case " ":
497
- event.preventDefault();
498
- if (options.activeOption.value != void 0) {
499
- options.onSelect?.(options.activeOption.value);
500
- }
501
- break;
502
- case "ArrowUp":
503
- event.preventDefault();
504
- if (options.activeOption.value == void 0) {
505
- options.onActivateLast?.();
506
- return;
507
- }
508
- options.onActivatePrevious?.(options.activeOption.value);
509
- break;
510
- case "ArrowDown":
511
- event.preventDefault();
512
- if (options.activeOption.value == void 0) {
513
- options.onActivateFirst?.();
514
- return;
515
- }
516
- options.onActivateNext?.(options.activeOption.value);
517
- break;
518
- case "Home":
519
- event.preventDefault();
520
- options.onActivateFirst?.();
521
- break;
522
- case "End":
523
- event.preventDefault();
524
- options.onActivateLast?.();
525
- break;
526
- default:
527
- typeAhead(event);
528
- }
529
- };
530
- const listbox = computed(
531
- () => options.controlled ? {
532
- role: "listbox",
533
- "aria-multiselectable": isMultiselect.value,
534
- "aria-label": unref(options.label),
535
- "aria-description": options.description,
536
- tabindex: "-1"
537
- } : {
538
- role: "listbox",
539
- "aria-multiselectable": isMultiselect.value,
540
- "aria-label": unref(options.label),
541
- "aria-description": options.description,
542
- tabindex: "0",
543
- "aria-activedescendant": options.activeOption.value != void 0 ? getOptionId(options.activeOption.value) : void 0,
544
- onFocus: () => isFocused.value = true,
545
- onBlur: () => isFocused.value = false,
546
- onKeydown: handleKeydown
547
- }
548
- );
549
- return {
550
- elements: {
551
- listbox,
552
- group: computed(() => {
553
- return (options2) => ({
554
- role: "group",
555
- "aria-label": options2.label
556
- });
557
- }),
558
- option: computed(() => {
559
- return (data) => {
560
- const selected = data.selected ?? false;
561
- return {
562
- id: getOptionId(data.value),
563
- role: "option",
564
- "aria-label": data.label,
565
- "aria-disabled": data.disabled,
566
- "aria-checked": isMultiselect.value ? selected : void 0,
567
- "aria-selected": !isMultiselect.value ? selected : void 0,
568
- onClick: () => !data.disabled && options.onSelect?.(data.value)
569
- };
570
- };
571
- })
572
- },
573
- state: {
574
- isFocused
575
- },
576
- internals: {
577
- getOptionId,
578
- getOptionValueById
579
- }
580
- };
581
- }
582
- );
583
- const OPENING_KEYS = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
584
- const CLOSING_KEYS = [
585
- "Escape",
586
- { key: "ArrowUp", altKey: true },
587
- "Enter",
588
- "Tab"
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
- const SELECTING_KEYS = ["Enter"];
591
- const isSelectingKey = (event, withSpace) => {
592
- const selectingKeys = withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS;
593
- return isKeyOfGroup(event, selectingKeys);
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
- const isKeyOfGroup = (event, group) => group.some((key) => wasKeyPressed(event, key));
596
- const createComboBox = createBuilder(
597
- ({
598
- autocomplete: autocompleteRef,
599
- onAutocomplete,
600
- onTypeAhead,
601
- multiple: multipleRef,
602
- label,
603
- listLabel,
604
- listDescription,
605
- isExpanded: isExpandedRef,
606
- activeOption,
607
- onToggle,
608
- onSelect,
609
- onActivateFirst,
610
- onActivateLast,
611
- onActivateNext,
612
- onActivatePrevious,
613
- templateRef
614
- }) => {
615
- const controlsId = useId();
616
- const autocomplete = computed(() => unref(autocompleteRef));
617
- const isExpanded = computed(() => unref(isExpandedRef));
618
- const multiple = computed(() => unref(multipleRef));
619
- const handleInput = (event) => {
620
- const inputElement = event.target;
621
- if (autocomplete.value !== "none") {
622
- onAutocomplete?.(inputElement.value);
623
- }
624
- };
625
- const typeAhead = useTypeAhead((inputString) => onTypeAhead?.(inputString));
626
- const handleSelect = (value) => {
627
- onSelect?.(value);
628
- if (!unref(multiple)) {
629
- onToggle?.();
630
- }
631
- };
632
- const handleNavigation = (event) => {
633
- switch (event.key) {
634
- case "ArrowUp":
635
- event.preventDefault();
636
- if (activeOption.value == void 0) {
637
- return onActivateLast?.();
638
- }
639
- onActivatePrevious?.(activeOption.value);
640
- break;
641
- case "ArrowDown":
642
- event.preventDefault();
643
- if (activeOption.value == void 0) {
644
- return onActivateFirst?.();
645
- }
646
- onActivateNext?.(activeOption.value);
647
- break;
648
- case "Home":
649
- event.preventDefault();
650
- onActivateFirst?.();
651
- break;
652
- case "End":
653
- event.preventDefault();
654
- onActivateLast?.();
655
- break;
656
- }
657
- };
658
- const handleKeydown = (event) => {
659
- if (event.key === "Enter") {
660
- event.preventDefault();
661
- }
662
- if (!isExpanded.value && isKeyOfGroup(event, OPENING_KEYS)) {
663
- onToggle?.();
664
- if (event.key === " ") {
665
- event.preventDefault();
666
- }
667
- if (event.key === "End") {
668
- return onActivateLast?.();
669
- }
670
- return onActivateFirst?.();
671
- }
672
- if (isSelectingKey(event, autocomplete.value === "none")) {
673
- return handleSelect(activeOption.value);
674
- }
675
- if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) {
676
- return onToggle?.();
677
- }
678
- if (autocomplete.value === "none" && isPrintableCharacter(event.key)) {
679
- !isExpanded.value && onToggle?.();
680
- return typeAhead(event);
681
- }
682
- if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
683
- !isExpanded.value && onToggle?.();
684
- return;
685
- }
686
- return handleNavigation(event);
687
- };
688
- const autocompleteInput = computed(() => {
689
- if (autocomplete.value === "none") return null;
690
- return {
691
- "aria-autocomplete": autocomplete.value,
692
- type: "text"
693
- };
694
- });
695
- const {
696
- elements: { option, group, listbox },
697
- internals: { getOptionId, getOptionValueById }
698
- } = createListbox({
699
- label: listLabel,
700
- description: listDescription,
701
- multiple,
702
- controlled: true,
703
- activeOption,
704
- isExpanded,
705
- onSelect: handleSelect
706
- });
707
- useOutsideClick({
708
- inside: templateRef,
709
- onOutsideClick() {
710
- if (!isExpanded.value) return;
711
- onToggle?.(true);
712
- }
713
- });
714
- return {
715
- elements: {
716
- option,
717
- group,
718
- /**
719
- * The listbox associated with the combobox.
720
- */
721
- listbox: computed(() => ({
722
- ...listbox.value,
723
- id: controlsId,
724
- // preventDefault to not lose focus of the combobox
725
- onMousedown: (e) => e.preventDefault()
726
- })),
727
- /**
728
- * An input that controls another element, that can dynamically pop-up to help the user set the value of the input.
729
- * The input MAY be either a single-line text field that supports editing and typing or an element that only displays the current value of the combobox.
730
- */
731
- input: computed(() => ({
732
- role: "combobox",
733
- "aria-expanded": isExpanded.value,
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
- const useLastSettled = (cb) => {
771
- const active = ref(false);
772
- let lastId = -1;
773
- const add = (promise) => {
774
- const promiseId = ++lastId;
775
- active.value = true;
776
- const onFinally = (success) => (resolved) => {
777
- if (promiseId === lastId) {
778
- active.value = false;
779
- if (success) {
780
- cb(success, resolved);
781
- } else {
782
- cb(success);
783
- }
784
- }
785
- };
786
- promise.then(onFinally(true)).catch(onFinally(false));
787
- };
788
- const cancel = () => {
789
- active.value = false;
790
- lastId++;
791
- };
792
- return { active, add, cancel };
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
- const COL_KEY_DATA_ATTR = "data-onyx-col-key";
795
- const COL_INDEX_ARIA_ATTR = "aria-colindex";
796
- const ROW_ID_DATA_ATTR = "data-onyx-row-id";
797
- const ROW_INDEX_ARIA_ATTR = "aria-rowindex";
798
- const StaticResolver = {
799
- mapCellToIndex: (cell) => Array.from(cell.closest("tr")?.cells ?? []).indexOf(cell),
800
- mapRowToIndex: (row) => Array.from(row.closest("table")?.rows ?? []).indexOf(row),
801
- resolveCell: async (cellIndex, rowIndex, table) => table.rows.item(rowIndex)?.cells.item(cellIndex),
802
- getTotalRows: (table) => table.rows.length,
803
- getTotalCols: (table) => table.rows.item(0)?.cells.length ?? 0
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
- const LazyResolverFactory = ({
806
- rows,
807
- cols,
808
- requestLazyLoad
809
- }) => ({
810
- mapCellToIndex: (cell) => Number(cell.getAttribute(COL_INDEX_ARIA_ATTR)) - 1,
811
- mapRowToIndex: (row) => Number(row.getAttribute(ROW_INDEX_ARIA_ATTR)) - 1,
812
- resolveCell: async (cellIndex, rowIndex, table) => {
813
- const queryCell = () => table.querySelector(
814
- `*[${ROW_INDEX_ARIA_ATTR}="${rowIndex + 1}"] *[${COL_INDEX_ARIA_ATTR}="${cellIndex + 1}"]`
815
- );
816
- let cell = queryCell();
817
- if (cell) {
818
- return cell;
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
- const createDataGrid = createBuilder(
833
- (options) => {
834
- const tableElement = createElRef();
835
- const lazy = options.lazy && toRef(options.lazy);
836
- const busy = computed(() => toValue(options.loading) ?? busySet.active.value);
837
- const resolver = lazy ? LazyResolverFactory({
838
- cols: () => lazy.value.totalCols,
839
- rows: () => lazy.value.totalRows,
840
- requestLazyLoad: lazy.value.requestLazyLoad
841
- }) : StaticResolver;
842
- const labelId = useId();
843
- const selectedCell = options.selectedCell || ref();
844
- const selectedCellEl = createElRef();
845
- const focusQueue = useLastSettled((success, cell) => {
846
- if (success) {
847
- cell?.focus();
848
- }
849
- });
850
- const busySet = useAllSettled();
851
- const findFirstCell = () => tableElement.value?.querySelector?.(
852
- `[${ROW_ID_DATA_ATTR}] [${COL_KEY_DATA_ATTR}]`
853
- );
854
- const setSelected = (element) => {
855
- const colKey = element.closest(`[${COL_KEY_DATA_ATTR}]`)?.getAttribute(COL_KEY_DATA_ATTR);
856
- const rowId = element.closest(`[${ROW_ID_DATA_ATTR}]`)?.getAttribute(ROW_ID_DATA_ATTR);
857
- if (colKey && rowId) {
858
- selectedCell.value = {
859
- rowId,
860
- colKey
861
- };
862
- }
863
- };
864
- const ensureTabTarget = () => {
865
- if (selectedCell.value && selectedCellEl.value?.isConnected) {
866
- return;
867
- }
868
- const firstCell = findFirstCell();
869
- if (firstCell) {
870
- setSelected(firstCell);
871
- }
872
- };
873
- let mutationObserver;
874
- onMounted(() => {
875
- ensureTabTarget();
876
- mutationObserver = new MutationObserver(ensureTabTarget);
877
- watch(
878
- tableElement,
879
- () => {
880
- if (!tableElement.value) {
881
- return;
882
- }
883
- mutationObserver?.disconnect();
884
- mutationObserver?.observe(tableElement.value, {
885
- childList: true,
886
- attributes: true,
887
- subtree: true,
888
- attributeFilter: ["value"]
889
- });
890
- },
891
- { immediate: true }
892
- );
893
- });
894
- onBeforeUnmount(() => {
895
- mutationObserver?.disconnect();
896
- });
897
- const onFocusin = (event) => {
898
- setSelected(event.target);
899
- focusQueue.cancel();
900
- };
901
- const onKeydown = (event) => {
902
- const target = event.target;
903
- const cellElement = target.closest("td, th");
904
- const rowElement = target.closest("tr");
905
- const tableElement2 = target.closest("table");
906
- if (!cellElement || !rowElement || !tableElement2) {
907
- return;
908
- }
909
- const { getTotalRows, getTotalCols, mapRowToIndex, mapCellToIndex, resolveCell } = resolver;
910
- const colIndex = mapCellToIndex(cellElement);
911
- const rowIndex = mapRowToIndex(rowElement);
912
- const totalRows = getTotalRows(tableElement2);
913
- const totalCols = getTotalCols(tableElement2);
914
- let newColIndex = colIndex;
915
- let newRowIndex = rowIndex;
916
- if (wasKeyPressed(event, { ctrlKey: true, key: "Home" })) {
917
- newColIndex = 0;
918
- newRowIndex = 0;
919
- } else if (wasKeyPressed(event, { ctrlKey: true, key: "End" })) {
920
- newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
921
- newRowIndex = totalRows === "unknown" ? Infinity : totalRows - 1;
922
- } else if (wasKeyPressed(event, "ArrowUp")) {
923
- newRowIndex = rowIndex - 1;
924
- } else if (wasKeyPressed(event, "ArrowDown")) {
925
- newRowIndex = rowIndex + 1;
926
- } else if (wasKeyPressed(event, "ArrowLeft")) {
927
- newColIndex = colIndex - 1;
928
- } else if (wasKeyPressed(event, "ArrowRight")) {
929
- newColIndex = colIndex + 1;
930
- } else if (wasKeyPressed(event, "Home")) {
931
- newColIndex = 0;
932
- } else if (wasKeyPressed(event, "End")) {
933
- newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
934
- } else {
935
- return;
936
- }
937
- event.preventDefault();
938
- const maxRows = totalRows === "unknown" ? Infinity : totalRows - 1;
939
- newRowIndex = Math.max(Math.min(newRowIndex, maxRows), 0);
940
- const maxCols = totalCols === "unknown" ? Infinity : totalCols - 1;
941
- newColIndex = Math.max(Math.min(newColIndex, maxCols), 0);
942
- (async () => {
943
- const promiseResolveCell = resolveCell(newColIndex, newRowIndex, tableElement2);
944
- focusQueue.add(promiseResolveCell);
945
- busySet.add(promiseResolveCell);
946
- })();
947
- };
948
- return {
949
- elements: {
950
- label: {
951
- id: labelId
952
- },
953
- table: computed(
954
- () => ({
955
- ref: tableElement,
956
- onFocusin,
957
- onKeydown,
958
- role: "grid",
959
- "aria-busy": busy.value,
960
- "aria-labelledby": labelId,
961
- "aria-rowcount": lazy?.value.totalRows === "unknown" ? -1 : lazy?.value.totalRows,
962
- "aria-colcount": lazy?.value.totalCols === "unknown" ? -1 : lazy?.value.totalCols
963
- })
964
- ),
965
- tr: ({ rowId, rowIndex }) => ({
966
- [ROW_ID_DATA_ATTR]: rowId.toString(),
967
- "aria-rowindex": rowIndex == void 0 ? void 0 : rowIndex + 1,
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
- const createMenuItems = createBuilder((options) => {
1121
- const onKeydown = (event) => {
1122
- switch (event.key) {
1123
- case "ArrowRight":
1124
- case " ":
1125
- case "Enter":
1126
- event.preventDefault();
1127
- options?.onOpen?.();
1128
- break;
1129
- }
1130
- };
1131
- return {
1132
- elements: {
1133
- listItem: {
1134
- role: "none"
1135
- },
1136
- menuItem: (data) => ({
1137
- "aria-current": data.active ? "page" : void 0,
1138
- "aria-disabled": data.disabled,
1139
- role: "menuitem",
1140
- onKeydown
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
- const MathUtils = {
1146
- /**
1147
- * Ensures that a given `number` is or is between a given `min` and `max`.
1148
- */
1149
- clamp: (number, min, max) => Math.max(Math.min(number, max), min),
1150
- /**
1151
- * Returns the count of decimal places in a number.
1152
- * @param number - The number to check.
1153
- * @returns The count of decimal places.
1154
- *
1155
- * decimals(1.23); // 2
1156
- * decimals(10); // 0
1157
- */
1158
- decimalsCount: (number) => String(number).split(".")[1]?.length ?? 0,
1159
- /**
1160
- * Converts a value within a range to a percentage (0-100).
1161
- *
1162
- * @param value - The value to convert.
1163
- * @param min - The minimum allowed value.
1164
- * @param max - The maximum allowed value.
1165
- * @returns The percentage representation of the value.
1166
- */
1167
- valueToPercent: (value, min, max) => (value - min) * 100 / (max - min),
1168
- /**
1169
- * Converts a percentage (0-100) to a value within a range.
1170
- *
1171
- * @param percent - The percentage to convert.
1172
- * @param min - The minimum allowed value.
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
- const createNavigationMenu = createBuilder(({ navigationName }) => {
1179
- const navId = useId();
1180
- const getMenuButtons = () => {
1181
- const nav = navId ? document.getElementById(navId) : void 0;
1182
- if (!nav) return [];
1183
- return Array.from(nav.querySelectorAll("button[aria-expanded][aria-controls]"));
1184
- };
1185
- const focusRelative = (trigger, next) => {
1186
- const menuButtons = getMenuButtons();
1187
- const index = menuButtons.indexOf(trigger);
1188
- if (index === -1) return;
1189
- const nextIndex = MathUtils.clamp(
1190
- index + (next === "next" ? 1 : -1),
1191
- 0,
1192
- menuButtons.length - 1
1193
- );
1194
- menuButtons[nextIndex]?.focus();
1195
- };
1196
- return {
1197
- elements: {
1198
- nav: {
1199
- "aria-label": unref(navigationName),
1200
- id: navId,
1201
- onKeydown: (event) => {
1202
- switch (event.key) {
1203
- case "ArrowRight":
1204
- focusRelative(event.target, "next");
1205
- break;
1206
- case "ArrowLeft":
1207
- focusRelative(event.target, "previous");
1208
- break;
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
- const areArraysEqual = (arrayA, arrayB, comparer = (a, b) => a === b) => arrayA.length === arrayB.length && arrayA.every((value, index) => comparer(value, arrayB[index]));
1216
- const NAVIGATION_KEYS = /* @__PURE__ */ new Set([
1217
- "ArrowUp",
1218
- "ArrowDown",
1219
- "ArrowLeft",
1220
- "ArrowRight",
1221
- "PageUp",
1222
- "PageDown",
1223
- "Home",
1224
- "End"
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
- const INCREMENT_KEYS = /* @__PURE__ */ new Set(["ArrowRight", "ArrowUp", "PageUp"]);
1227
- const DECREMENT_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowDown", "PageDown"]);
1228
- const createSlider = createBuilder(
1229
- (options) => {
1230
- const sliderRef = createElRef();
1231
- const firstThumbRef = createElRef();
1232
- const secondThumbRef = createElRef();
1233
- const min = computed(() => toValue(options.min) ?? 0);
1234
- const max = computed(() => toValue(options.max) ?? 100);
1235
- const step = computed(() => toValue(options.step) ?? 1);
1236
- const draggingThumbIndex = ref();
1237
- watch(draggingThumbIndex, (newThumbIndex) => {
1238
- if (newThumbIndex == void 0) return;
1239
- focusThumb(newThumbIndex);
1240
- });
1241
- const focusThumb = (index) => {
1242
- if (index === 0) firstThumbRef.value?.focus();
1243
- else if (index === 1) secondThumbRef.value?.focus();
1244
- };
1245
- const shiftStep = computed(() => {
1246
- const shiftStep2 = toValue(options.shiftStep);
1247
- if (shiftStep2 != void 0) return shiftStep2;
1248
- const stepMultiple = Math.max(1, Math.round((max.value - min.value) * 0.1 / step.value));
1249
- return stepMultiple * step.value;
1250
- });
1251
- const getNormalizedValue = computed(() => {
1252
- return (value) => {
1253
- let values = typeof value === "number" ? [value] : value;
1254
- values = values.map((value2) => MathUtils.clamp(value2, min.value, max.value)).map((value2) => {
1255
- const stepDecimals = MathUtils.decimalsCount(step.value);
1256
- return Number(
1257
- (Math.round((value2 - min.value) / step.value) * step.value + min.value).toFixed(
1258
- stepDecimals
1259
- )
1260
- );
1261
- }).sort((a, b) => a - b);
1262
- return values;
1263
- };
1264
- });
1265
- const normalizedValue = computed(
1266
- () => getNormalizedValue.value(toValue(options.value))
1267
- );
1268
- const updateValue = (value, index) => {
1269
- const currentValue = normalizedValue.value.slice();
1270
- const otherIndex = index === 0 ? 1 : 0;
1271
- const otherValue = currentValue[otherIndex];
1272
- if (otherValue != void 0) {
1273
- if (index < otherIndex && value > otherValue) {
1274
- value = otherValue;
1275
- } else if (index > otherIndex && value < otherValue) {
1276
- value = otherValue;
1277
- }
1278
- }
1279
- currentValue[index] = value;
1280
- const normalized = getNormalizedValue.value(currentValue);
1281
- const newValue = normalized.length > 1 ? normalized : normalized[0];
1282
- if (areArraysEqual(normalized, normalizedValue.value)) return;
1283
- options.onChange?.(newValue);
1284
- };
1285
- const updateValueByStep = (direction) => {
1286
- const index = 0;
1287
- const stepValue = direction === "increase" ? step.value : -step.value;
1288
- const currentValue = normalizedValue.value[index];
1289
- updateValue(currentValue + stepValue, index);
1290
- focusThumb(index);
1291
- };
1292
- const getValueInPercentage = computed(() => {
1293
- return (value) => {
1294
- const percentage = MathUtils.valueToPercent(value, min.value, max.value);
1295
- return MathUtils.clamp(percentage, 0, 100);
1296
- };
1297
- });
1298
- const handleKeydown = (event, thumbIndex) => {
1299
- if (!NAVIGATION_KEYS.has(event.key)) return;
1300
- event.preventDefault();
1301
- const currentValue = normalizedValue.value.slice();
1302
- if (currentValue[thumbIndex] == void 0) return;
1303
- const stepSize = event.shiftKey ? shiftStep.value : step.value;
1304
- if (event.key === "Home") {
1305
- return updateValue(min.value, thumbIndex);
1306
- }
1307
- if (event.key === "End") {
1308
- return updateValue(max.value, thumbIndex);
1309
- }
1310
- if (INCREMENT_KEYS.has(event.key)) {
1311
- updateValue(currentValue[thumbIndex] + stepSize, thumbIndex);
1312
- } else if (DECREMENT_KEYS.has(event.key)) {
1313
- updateValue(currentValue[thumbIndex] - stepSize, thumbIndex);
1314
- }
1315
- };
1316
- const handlePointerdown = (event) => {
1317
- event.preventDefault();
1318
- const value = getValueFromCoordinates(event.x);
1319
- if (value == void 0) return;
1320
- const thumb = normalizedValue.value.reduce(
1321
- (previous, thumbValue, index) => {
1322
- const distance = Math.abs(thumbValue - value);
1323
- if (distance < previous.distance) return { index, distance };
1324
- return previous;
1325
- },
1326
- { index: 0, distance: Number.POSITIVE_INFINITY }
1327
- );
1328
- updateValue(value, thumb.index);
1329
- draggingThumbIndex.value = thumb.index;
1330
- };
1331
- const handlePointermove = (event) => {
1332
- if (draggingThumbIndex.value == void 0) return;
1333
- const value = getValueFromCoordinates(event.x);
1334
- if (value == void 0) return;
1335
- updateValue(value, draggingThumbIndex.value);
1336
- };
1337
- useGlobalEventListener({
1338
- type: "pointermove",
1339
- listener: handlePointermove,
1340
- disabled: computed(() => draggingThumbIndex.value == void 0)
1341
- });
1342
- const handlePointerup = () => {
1343
- draggingThumbIndex.value = void 0;
1344
- };
1345
- useGlobalEventListener({
1346
- type: "pointerup",
1347
- listener: handlePointerup,
1348
- disabled: computed(() => draggingThumbIndex.value == void 0)
1349
- });
1350
- const getValueFromCoordinates = (x) => {
1351
- const rect = sliderRef.value?.getBoundingClientRect();
1352
- if (!rect || rect.width <= 0) return;
1353
- const percent = MathUtils.clamp((x - rect.left) / rect.width, 0, 1);
1354
- return MathUtils.percentToValue(percent, min.value, max.value);
1355
- };
1356
- return {
1357
- elements: {
1358
- /**
1359
- * Root slider container element
1360
- */
1361
- root: computed(() => {
1362
- const events = {
1363
- onPointerdown: handlePointerdown
1364
- };
1365
- return {
1366
- ref: sliderRef,
1367
- style: { touchAction: "pan-y" },
1368
- ...toValue(options.disabled) ? void 0 : events
1369
- };
1370
- }),
1371
- /**
1372
- * Input inside each thumb for accessibility
1373
- */
1374
- thumbInput: computed(() => (data) => {
1375
- const events = {
1376
- onKeydown: (event) => handleKeydown(event, data.index)
1377
- };
1378
- return {
1379
- min: min.value,
1380
- max: max.value,
1381
- value: data.value,
1382
- role: "slider",
1383
- type: "range",
1384
- "aria-label": toValue(options.label),
1385
- "aria-valuemin": min.value,
1386
- "aria-valuemax": max.value,
1387
- "aria-valuenow": data.value,
1388
- "aria-orientation": "horizontal",
1389
- step: step.value,
1390
- disabled: toValue(options.disabled),
1391
- ref: data.index === 0 ? firstThumbRef : data.index === 1 ? secondThumbRef : void 0,
1392
- ...toValue(options.disabled) ? void 0 : events
1393
- };
1394
- })
1395
- },
1396
- state: {
1397
- normalizedValue,
1398
- shiftStep,
1399
- /**
1400
- * Track element representing the selected range
1401
- */
1402
- track: computed(() => {
1403
- const isRange = normalizedValue.value.length > 1;
1404
- const start = isRange ? normalizedValue.value[0] : min.value;
1405
- const startPercentage = getValueInPercentage.value(start);
1406
- const end = normalizedValue.value.at(-1);
1407
- const endPercentage = getValueInPercentage.value(end);
1408
- return {
1409
- start,
1410
- startPercentage,
1411
- end,
1412
- endPercentage
1413
- };
1414
- })
1415
- },
1416
- internals: {
1417
- getValueInPercentage,
1418
- updateValue,
1419
- updateValueByStep
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
- const createToggleButton = createBuilder((options) => {
1512
- return {
1513
- elements: {
1514
- /**
1515
- * A html button element that is supposed to act as a toggle button.
1516
- */
1517
- button: computed(
1518
- () => ({
1519
- "aria-pressed": toValue(options.isPressed),
1520
- onClick: options.onToggle
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
- const useDismissible = ({ isExpanded }) => useGlobalEventListener({
1527
- type: "keydown",
1528
- listener: (e) => {
1529
- if (e.key === "Escape") {
1530
- isExpanded.value = false;
1531
- }
1532
- },
1533
- disabled: computed(() => !isExpanded.value)
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
- const createToggletip = createBuilder(
1536
- ({ toggleLabel, isVisible }) => {
1537
- const triggerId = useId();
1538
- const _isVisible = toRef(isVisible ?? false);
1539
- useDismissible({ isExpanded: _isVisible });
1540
- const toggle = () => _isVisible.value = !_isVisible.value;
1541
- return {
1542
- elements: {
1543
- /**
1544
- * The element which controls the toggletip visibility:
1545
- * Preferably a `button` element.
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
- export {
1620
- CLOSING_KEYS,
1621
- OPENING_KEYS,
1622
- _unstableCreateCalendar,
1623
- createBuilder,
1624
- createComboBox,
1625
- createDataGrid,
1626
- createElRef,
1627
- createListbox,
1628
- createMenuButton,
1629
- createMenuItems,
1630
- createNavigationMenu,
1631
- createSlider,
1632
- createTabs,
1633
- createToggleButton,
1634
- createToggletip,
1635
- createTooltip,
1636
- debounce,
1637
- getNativeElement,
1638
- isPrintableCharacter,
1639
- useGlobalEventListener,
1640
- useOutsideClick,
1641
- wasKeyPressed
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 };