stimeo-ui 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/dist/controllers/alert_dialog_controller.js +75 -4
- data/dist/controllers/aspect_ratio_controller.js +19 -11
- data/dist/controllers/avatar_controller.js +237 -40
- data/dist/controllers/carousel_controller.js +85 -9
- data/dist/controllers/character_counter_controller.js +338 -63
- data/dist/controllers/checkbox_controller.js +136 -25
- data/dist/controllers/color_picker_controller.js +35 -9
- data/dist/controllers/command_palette_controller.js +75 -4
- data/dist/controllers/conditional_fields_controller.js +345 -51
- data/dist/controllers/confirm_controller.js +75 -4
- data/dist/controllers/date_range_picker_controller.js +157 -30
- data/dist/controllers/dialog_controller.js +75 -4
- data/dist/controllers/direct_upload_controller.js +201 -45
- data/dist/controllers/dirty_form_controller.js +192 -29
- data/dist/controllers/dismissible_controller.js +83 -18
- data/dist/controllers/drawer_controller.js +75 -4
- data/dist/controllers/file_dropzone_controller.js +26 -3
- data/dist/controllers/focus_controller.js +75 -4
- data/dist/controllers/form_field_controller.js +280 -62
- data/dist/controllers/form_validation_controller.js +208 -83
- data/dist/controllers/idle_controller.js +27 -5
- data/dist/controllers/menubar_controller.js +5 -3
- data/dist/controllers/multi_select_controller.js +460 -151
- data/dist/controllers/number_input_controller.js +317 -51
- data/dist/controllers/overflow_menu_controller.js +6 -1
- data/dist/controllers/pagination_controller.js +35 -1
- data/dist/controllers/password_strength_controller.js +20 -2
- data/dist/controllers/persist_controller.js +449 -120
- data/dist/controllers/popover_controller.js +77 -4
- data/dist/controllers/radio_group_controller.js +540 -56
- data/dist/controllers/rating_controller.js +276 -89
- data/dist/controllers/resizable_controller.js +33 -0
- data/dist/controllers/roving_controller.js +60 -5
- data/dist/controllers/scroll_area_controller.js +557 -125
- data/dist/controllers/scroll_visibility_controller.js +33 -0
- data/dist/controllers/separator_controller.js +354 -38
- data/dist/controllers/sidebar_controller.js +83 -10
- data/dist/controllers/submit_once_controller.js +399 -121
- data/dist/controllers/tags_input_controller.js +356 -120
- data/dist/controllers/theme_controller.js +8 -6
- data/dist/controllers/time_picker_controller.js +296 -107
- data/dist/controllers/toggle_group_controller.js +378 -55
- data/dist/controllers/toolbar_controller.js +5 -3
- data/dist/controllers/tree_view_controller.js +7 -4
- data/dist/index.js +4963 -1581
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -18,6 +18,35 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/before_cache_reset.ts
|
|
22
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
23
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
24
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
25
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
26
|
+
static #onBeforeCache = () => {
|
|
27
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
28
|
+
};
|
|
29
|
+
#rewind;
|
|
30
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
31
|
+
constructor(rewind) {
|
|
32
|
+
this.#rewind = rewind;
|
|
33
|
+
}
|
|
34
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
35
|
+
activate() {
|
|
36
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
37
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
38
|
+
if (first) {
|
|
39
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
43
|
+
deactivate() {
|
|
44
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
45
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
46
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
21
50
|
// src/utils/dates.ts
|
|
22
51
|
function toISODateString(date) {
|
|
23
52
|
const year = date.getFullYear();
|
|
@@ -138,16 +167,34 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
138
167
|
}
|
|
139
168
|
};
|
|
140
169
|
|
|
170
|
+
// src/utils/string_list.ts
|
|
171
|
+
function parseStringList(raw, fallback = []) {
|
|
172
|
+
const text = raw.trim();
|
|
173
|
+
if (text.length === 0) return [...fallback];
|
|
174
|
+
let parsed;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(text);
|
|
177
|
+
} catch {
|
|
178
|
+
return [...fallback];
|
|
179
|
+
}
|
|
180
|
+
if (!Array.isArray(parsed)) return [...fallback];
|
|
181
|
+
return parsed.filter((entry) => typeof entry === "string");
|
|
182
|
+
}
|
|
183
|
+
|
|
141
184
|
// src/controllers/date_range_picker_controller.ts
|
|
142
185
|
var GRID_SIZE = 42;
|
|
143
186
|
var DateRangePickerController = class extends Controller {
|
|
144
187
|
static targets = ["grid", "monthLabel", "cell", "status", "startField", "endField"];
|
|
145
188
|
static values = {
|
|
146
189
|
min: { type: String, default: "" },
|
|
147
|
-
max: { type: String, default: "" }
|
|
190
|
+
max: { type: String, default: "" },
|
|
191
|
+
// A JSON list read through `parseStringList` rather than Stimulus's `Array`
|
|
192
|
+
// type: that reader throws out of the value observer before any callback
|
|
193
|
+
// runs, so one malformed attribute would stop the picker from connecting.
|
|
194
|
+
disabledDates: { type: String, default: "" }
|
|
148
195
|
};
|
|
149
196
|
static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
|
|
150
|
-
static events = ["change"];
|
|
197
|
+
static events = ["change", "monthchange"];
|
|
151
198
|
/** The month currently rendered, as `YYYY-MM`. */
|
|
152
199
|
#viewMonth = "";
|
|
153
200
|
/** The confirmed range endpoints (ISO), or "" when unset. */
|
|
@@ -161,7 +208,12 @@ var DateRangePickerController = class extends Controller {
|
|
|
161
208
|
#focusedDate = /* @__PURE__ */ new Date();
|
|
162
209
|
/** Deferred focus after an async month transition (cancelled on teardown). */
|
|
163
210
|
#focusTimer = new SafeTimeout();
|
|
164
|
-
/**
|
|
211
|
+
/** The declared unavailable dates, indexed for the per-cell paint lookup. */
|
|
212
|
+
#disabledDates = /* @__PURE__ */ new Set();
|
|
213
|
+
/** Rewinds an unfinished selection before Turbo snapshots the page. */
|
|
214
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
215
|
+
/** Last painted month, or `null` until the initial paint has settled. */
|
|
216
|
+
#announcedMonth = null;
|
|
165
217
|
/**
|
|
166
218
|
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
167
219
|
* pass Stimulus delivers before `connect()`.
|
|
@@ -169,10 +221,17 @@ var DateRangePickerController = class extends Controller {
|
|
|
169
221
|
#repaint = new MicrotaskCoalescer(() => {
|
|
170
222
|
this.#render();
|
|
171
223
|
});
|
|
224
|
+
/** Seeds and normalizes the range from optional hidden fields, then paints the grid. */
|
|
172
225
|
connect() {
|
|
173
226
|
this.#repaint.activate();
|
|
174
|
-
this.#
|
|
175
|
-
|
|
227
|
+
this.#beforeCache.activate();
|
|
228
|
+
const authoredStart = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
|
|
229
|
+
const authoredEnd = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
|
|
230
|
+
[this.#startDate, this.#endDate] = orderRange(authoredStart, authoredEnd);
|
|
231
|
+
this.#pendingStart = "";
|
|
232
|
+
this.#previewDate = "";
|
|
233
|
+
this.#announcedMonth = null;
|
|
234
|
+
this.#commitFields();
|
|
176
235
|
const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
|
|
177
236
|
this.#focusedDate = anchor;
|
|
178
237
|
this.#viewMonth = toISOMonthString(anchor);
|
|
@@ -182,6 +241,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
182
241
|
/** Cancels any pending deferred focus so it never fires on a detached element. */
|
|
183
242
|
disconnect() {
|
|
184
243
|
this.#repaint.cancel();
|
|
244
|
+
this.#beforeCache.deactivate();
|
|
185
245
|
this.#focusTimer.clearAll();
|
|
186
246
|
}
|
|
187
247
|
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
@@ -192,6 +252,11 @@ var DateRangePickerController = class extends Controller {
|
|
|
192
252
|
maxValueChanged() {
|
|
193
253
|
this.#repaint.schedule();
|
|
194
254
|
}
|
|
255
|
+
/** Re-indexes the unavailable dates and repaints when the declared set changes. */
|
|
256
|
+
disabledDatesValueChanged() {
|
|
257
|
+
this.#disabledDates = new Set(parseStringList(this.disabledDatesValue));
|
|
258
|
+
this.#repaint.schedule();
|
|
259
|
+
}
|
|
195
260
|
/** Navigates to the previous month. */
|
|
196
261
|
prev(event) {
|
|
197
262
|
event?.preventDefault();
|
|
@@ -207,7 +272,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
207
272
|
const cell = this.#cellFrom(event.target);
|
|
208
273
|
if (!cell) return;
|
|
209
274
|
const date = cell.getAttribute("data-date");
|
|
210
|
-
if (!date ||
|
|
275
|
+
if (!date || !this.#isSelectable(date)) return;
|
|
211
276
|
this.#choose(date);
|
|
212
277
|
}
|
|
213
278
|
/** Previews the range up to a hovered/focused cell while selecting. */
|
|
@@ -216,28 +281,27 @@ var DateRangePickerController = class extends Controller {
|
|
|
216
281
|
if (!cell) return;
|
|
217
282
|
const date = cell.getAttribute("data-date");
|
|
218
283
|
if (!date) return;
|
|
284
|
+
let shouldRender = false;
|
|
219
285
|
if (event.type.startsWith("focus")) {
|
|
220
286
|
const parsed = parseISODateString(date);
|
|
221
287
|
if (parsed) this.#focusedDate = parsed;
|
|
222
|
-
|
|
223
|
-
this.#render();
|
|
224
|
-
}
|
|
288
|
+
shouldRender = cell.getAttribute("tabindex") !== "0";
|
|
225
289
|
}
|
|
226
|
-
if (this.#pendingStart) {
|
|
290
|
+
if (this.#pendingStart && this.#isSelectable(date) && this.#previewDate !== date) {
|
|
227
291
|
this.#previewDate = date;
|
|
228
|
-
|
|
292
|
+
shouldRender = true;
|
|
229
293
|
}
|
|
294
|
+
if (shouldRender) this.#render();
|
|
230
295
|
}
|
|
231
296
|
/** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */
|
|
232
297
|
applyPreset(event) {
|
|
233
298
|
const button = event.target?.closest("[data-range]");
|
|
234
|
-
const
|
|
235
|
-
if (!name) return;
|
|
236
|
-
const range = computePreset(name);
|
|
299
|
+
const range = computePreset(button?.getAttribute("data-range") ?? "");
|
|
237
300
|
if (!range) return;
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
301
|
+
const intersection = this.#intersectRange(range);
|
|
302
|
+
if (!intersection) return;
|
|
303
|
+
const { start, end } = intersection;
|
|
304
|
+
if (!this.#isSelectable(start) || !this.#isSelectable(end)) return;
|
|
241
305
|
this.#startDate = start;
|
|
242
306
|
this.#endDate = end;
|
|
243
307
|
this.#pendingStart = "";
|
|
@@ -255,12 +319,12 @@ var DateRangePickerController = class extends Controller {
|
|
|
255
319
|
if (isReservedArrowChord(event)) return;
|
|
256
320
|
const cell = this.#cellFrom(event.target);
|
|
257
321
|
if (!cell) return;
|
|
258
|
-
const dateStr = cell.getAttribute("data-date");
|
|
259
|
-
const date =
|
|
322
|
+
const dateStr = cell.getAttribute("data-date") ?? "";
|
|
323
|
+
const date = parseISODateString(dateStr);
|
|
260
324
|
if (!date) return;
|
|
261
325
|
if (event.key === "Enter" || event.key === " ") {
|
|
262
326
|
event.preventDefault();
|
|
263
|
-
if (dateStr
|
|
327
|
+
if (this.#isSelectable(dateStr)) this.#choose(dateStr);
|
|
264
328
|
return;
|
|
265
329
|
}
|
|
266
330
|
if (event.key === "Escape") {
|
|
@@ -293,10 +357,10 @@ var DateRangePickerController = class extends Controller {
|
|
|
293
357
|
next = addDays(date, 6 - date.getDay());
|
|
294
358
|
break;
|
|
295
359
|
case "PageUp":
|
|
296
|
-
next = shiftMonthClamped(date, -1);
|
|
360
|
+
next = event.shiftKey ? shiftYearClamped(date, -1) : shiftMonthClamped(date, -1);
|
|
297
361
|
break;
|
|
298
362
|
case "PageDown":
|
|
299
|
-
next = shiftMonthClamped(date, 1);
|
|
363
|
+
next = event.shiftKey ? shiftYearClamped(date, 1) : shiftMonthClamped(date, 1);
|
|
300
364
|
break;
|
|
301
365
|
default:
|
|
302
366
|
return;
|
|
@@ -327,12 +391,14 @@ var DateRangePickerController = class extends Controller {
|
|
|
327
391
|
/** Moves roving focus to `date`, transitioning the month when needed. */
|
|
328
392
|
#moveFocusTo(date) {
|
|
329
393
|
this.#focusedDate = date;
|
|
330
|
-
|
|
331
|
-
this.#
|
|
394
|
+
const iso = toISODateString(date);
|
|
395
|
+
if (this.#pendingStart && this.#isSelectable(iso)) this.#previewDate = iso;
|
|
396
|
+
this.#transitionTo(toISOMonthString(date), iso);
|
|
332
397
|
}
|
|
333
398
|
/** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */
|
|
334
399
|
#transitionTo(month, dateStr) {
|
|
335
400
|
const isTransition = month !== this.#viewMonth;
|
|
401
|
+
this.#focusTimer.clearAll();
|
|
336
402
|
this.#viewMonth = month;
|
|
337
403
|
this.#render();
|
|
338
404
|
const focusCell = () => {
|
|
@@ -364,17 +430,19 @@ var DateRangePickerController = class extends Controller {
|
|
|
364
430
|
const info = parseISOMonthString(this.#viewMonth);
|
|
365
431
|
if (!info) return;
|
|
366
432
|
const { year, month } = info;
|
|
433
|
+
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
367
434
|
if (this.hasMonthLabelTarget) {
|
|
368
435
|
const lang = document.documentElement.lang || "en";
|
|
369
|
-
const formatter =
|
|
436
|
+
const formatter = monthFormatter(lang);
|
|
370
437
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
371
438
|
}
|
|
372
439
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
373
440
|
const days = gridDays(year, month);
|
|
374
441
|
const focusedStr = toISODateString(this.#focusedDate);
|
|
375
442
|
const todayStr = toISODateString(/* @__PURE__ */ new Date());
|
|
443
|
+
const cells = this.cellTargets;
|
|
376
444
|
for (let i = 0; i < GRID_SIZE; i++) {
|
|
377
|
-
const el =
|
|
445
|
+
const el = cells[i];
|
|
378
446
|
const date = days[i];
|
|
379
447
|
if (!el || !date) continue;
|
|
380
448
|
const iso = toISODateString(date);
|
|
@@ -383,8 +451,8 @@ var DateRangePickerController = class extends Controller {
|
|
|
383
451
|
el.setAttribute("data-outside", String(date.getMonth() !== month - 1));
|
|
384
452
|
el.setAttribute("data-today", String(iso === todayStr));
|
|
385
453
|
el.setAttribute("tabindex", iso === focusedStr ? "0" : "-1");
|
|
386
|
-
if (this.#
|
|
387
|
-
else el.
|
|
454
|
+
if (this.#isSelectable(iso)) el.removeAttribute("aria-disabled");
|
|
455
|
+
else el.setAttribute("aria-disabled", "true");
|
|
388
456
|
const isStart = !!rangeStart && iso === rangeStart;
|
|
389
457
|
const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;
|
|
390
458
|
const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;
|
|
@@ -398,6 +466,16 @@ var DateRangePickerController = class extends Controller {
|
|
|
398
466
|
)
|
|
399
467
|
);
|
|
400
468
|
}
|
|
469
|
+
for (const extra of cells.slice(GRID_SIZE)) extra.setAttribute("tabindex", "-1");
|
|
470
|
+
if (!cells.some((cell) => cell.getAttribute("tabindex") === "0")) {
|
|
471
|
+
const fallback = cells.find((cell) => cell.getAttribute("data-outside") === "false") ?? cells[0];
|
|
472
|
+
fallback?.setAttribute("tabindex", "0");
|
|
473
|
+
}
|
|
474
|
+
const previous = this.#announcedMonth;
|
|
475
|
+
this.#announcedMonth = this.#viewMonth;
|
|
476
|
+
if (previous !== null && previous !== this.#viewMonth) {
|
|
477
|
+
this.dispatch("monthchange", { detail: { month: this.#viewMonth } });
|
|
478
|
+
}
|
|
401
479
|
}
|
|
402
480
|
/** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */
|
|
403
481
|
#visualRange() {
|
|
@@ -418,15 +496,25 @@ var DateRangePickerController = class extends Controller {
|
|
|
418
496
|
this.statusTarget.textContent = `${this.#startDate} \u2013 ${this.#endDate}`;
|
|
419
497
|
}
|
|
420
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* True when `iso` may be chosen as a range endpoint.
|
|
501
|
+
*
|
|
502
|
+
* The single availability question in the controller: the grid paint, the
|
|
503
|
+
* preview, and both commit paths ask it, so a cell's `aria-disabled` and what
|
|
504
|
+
* a click on that cell does are the same decision rather than two that have to
|
|
505
|
+
* be kept in step.
|
|
506
|
+
*/
|
|
507
|
+
#isSelectable(iso) {
|
|
508
|
+
return !this.#outOfBounds(iso) && !this.#disabledDates.has(iso);
|
|
509
|
+
}
|
|
421
510
|
/** True when `iso` falls outside the `[min, max]` bounds. */
|
|
422
511
|
#outOfBounds(iso) {
|
|
423
512
|
if (this.minValue && iso < this.minValue) return true;
|
|
424
513
|
if (this.maxValue && iso > this.maxValue) return true;
|
|
425
514
|
return false;
|
|
426
515
|
}
|
|
427
|
-
/** Clamps
|
|
516
|
+
/** Clamps a generated ISO date string into `[min, max]`. */
|
|
428
517
|
#clampISO(iso) {
|
|
429
|
-
if (!iso) return "";
|
|
430
518
|
if (this.minValue && iso < this.minValue) return this.minValue;
|
|
431
519
|
if (this.maxValue && iso > this.maxValue) return this.maxValue;
|
|
432
520
|
return iso;
|
|
@@ -436,6 +524,27 @@ var DateRangePickerController = class extends Controller {
|
|
|
436
524
|
const clamped = this.#clampISO(toISODateString(date));
|
|
437
525
|
return parseISODateString(clamped);
|
|
438
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Intersects a preset with `[min, max]`, rejecting a disjoint interval.
|
|
529
|
+
*
|
|
530
|
+
* Only the bounds narrow a preset here: `disabledDates` excludes single days,
|
|
531
|
+
* not sub-intervals, so it cannot shrink one to a still-contiguous range. A
|
|
532
|
+
* day it excludes is still refused as an endpoint — the caller checks that —
|
|
533
|
+
* but a preset that merely spans one keeps its span.
|
|
534
|
+
*/
|
|
535
|
+
#intersectRange(range) {
|
|
536
|
+
const start = this.minValue && range.start < this.minValue ? this.minValue : range.start;
|
|
537
|
+
const end = this.maxValue && range.end > this.maxValue ? this.maxValue : range.end;
|
|
538
|
+
return start <= end ? { start, end } : null;
|
|
539
|
+
}
|
|
540
|
+
/** Removes provisional range state before Turbo freezes a cached snapshot. */
|
|
541
|
+
#rewindForCache() {
|
|
542
|
+
this.#focusTimer.clearAll();
|
|
543
|
+
if (!this.#pendingStart && !this.#previewDate) return;
|
|
544
|
+
this.#pendingStart = "";
|
|
545
|
+
this.#previewDate = "";
|
|
546
|
+
this.#render();
|
|
547
|
+
}
|
|
439
548
|
/** Resolves the cell element from an event target, or null. */
|
|
440
549
|
#cellFrom(target) {
|
|
441
550
|
return target?.closest(
|
|
@@ -454,6 +563,12 @@ function shiftMonthClamped(date, delta) {
|
|
|
454
563
|
target.setDate(Math.min(date.getDate(), lastDay));
|
|
455
564
|
return target;
|
|
456
565
|
}
|
|
566
|
+
function shiftYearClamped(date, delta) {
|
|
567
|
+
const target = new Date(date.getFullYear() + delta, date.getMonth(), 1);
|
|
568
|
+
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
569
|
+
target.setDate(Math.min(date.getDate(), lastDay));
|
|
570
|
+
return target;
|
|
571
|
+
}
|
|
457
572
|
function gridDays(year, month) {
|
|
458
573
|
const first = new Date(year, month - 1, 1);
|
|
459
574
|
const start = new Date(first);
|
|
@@ -470,6 +585,18 @@ function normalizeISO(value) {
|
|
|
470
585
|
const date = parseISODateString(value.trim());
|
|
471
586
|
return date ? toISODateString(date) : "";
|
|
472
587
|
}
|
|
588
|
+
function orderRange(start, end) {
|
|
589
|
+
return start && end && end < start ? [end, start] : [start, end];
|
|
590
|
+
}
|
|
591
|
+
function monthFormatter(locale) {
|
|
592
|
+
const options = { month: "long", year: "numeric" };
|
|
593
|
+
try {
|
|
594
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (!(error instanceof RangeError)) throw error;
|
|
597
|
+
return new Intl.DateTimeFormat("en", options);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
473
600
|
function computePreset(name) {
|
|
474
601
|
const today = /* @__PURE__ */ new Date();
|
|
475
602
|
const todayStr = toISODateString(today);
|
|
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// src/utils/focus_candidate.ts
|
|
119
|
+
function inheritsFieldsetDisabled(control) {
|
|
120
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
121
|
+
while (fieldset) {
|
|
122
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
123
|
+
if (!legend?.contains(control)) return true;
|
|
124
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function canTakeFocus(element) {
|
|
129
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
130
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
|
+
if (!("disabled" in element)) return true;
|
|
132
|
+
if (element.disabled) return false;
|
|
133
|
+
return !inheritsFieldsetDisabled(element);
|
|
134
|
+
}
|
|
135
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
136
|
+
"a[href]",
|
|
137
|
+
"area[href]",
|
|
138
|
+
"button",
|
|
139
|
+
"input",
|
|
140
|
+
"select",
|
|
141
|
+
"textarea",
|
|
142
|
+
"summary",
|
|
143
|
+
"iframe",
|
|
144
|
+
"audio[controls]",
|
|
145
|
+
"video[controls]",
|
|
146
|
+
"[tabindex]",
|
|
147
|
+
"[contenteditable]"
|
|
148
|
+
].join(",");
|
|
149
|
+
function isRenderedForFocus(element) {
|
|
150
|
+
const check = element.checkVisibility;
|
|
151
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
152
|
+
}
|
|
153
|
+
function authoredTabindex(element) {
|
|
154
|
+
const value = element.getAttribute("tabindex");
|
|
155
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
156
|
+
return Number(value);
|
|
157
|
+
}
|
|
158
|
+
function hasNativeTabStop(element) {
|
|
159
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
160
|
+
return element.hasAttribute("href");
|
|
161
|
+
}
|
|
162
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
166
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
167
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
168
|
+
return element.hasAttribute("controls");
|
|
169
|
+
}
|
|
170
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
171
|
+
const details = element.parentElement;
|
|
172
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasEditableTabStop(element) {
|
|
177
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
178
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
179
|
+
}
|
|
180
|
+
function isTabStop(element) {
|
|
181
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
182
|
+
const tabindex = authoredTabindex(element);
|
|
183
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
184
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
|
+
}
|
|
186
|
+
function tabStopsWithin(root) {
|
|
187
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
188
|
+
isTabStop
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
// src/utils/focus_trap.ts
|
|
119
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
120
193
|
var FocusTrap = class {
|
|
121
194
|
/** The element focused before activation, restored on deactivation. */
|
|
122
195
|
#previouslyFocused = null;
|
|
@@ -278,9 +351,7 @@ var FocusTrap = class {
|
|
|
278
351
|
}
|
|
279
352
|
/** Collects the container's currently focusable descendants in DOM order. */
|
|
280
353
|
#focusableElements() {
|
|
281
|
-
return
|
|
282
|
-
(el) => !el.hidden
|
|
283
|
-
);
|
|
354
|
+
return tabStopsWithin(this.#getContainer());
|
|
284
355
|
}
|
|
285
356
|
};
|
|
286
357
|
|