stimeo-ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/controllers/alert_dialog_controller.js +32 -5
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +32 -6
- package/dist/controllers/announcer_controller.js +255 -20
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +2 -1
- package/dist/controllers/color_picker_controller.js +46 -0
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +32 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +32 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +19 -5
- package/dist/controllers/countdown_controller.js +112 -12
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +4 -1
- package/dist/controllers/date_range_picker_controller.js +50 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +32 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +32 -5
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +9 -4
- package/dist/controllers/empty_state_controller.js +24 -10
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +32 -5
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +14 -3
- package/dist/controllers/frame_loading_controller.js +177 -15
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +19 -3
- package/dist/controllers/local_time_controller.js +100 -6
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +22 -2
- package/dist/controllers/meter_controller.js +145 -26
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +13 -1
- package/dist/controllers/network_status_controller.js +28 -8
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +33 -6
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +16 -1
- package/dist/controllers/progress_controller.js +116 -9
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +4 -1
- package/dist/controllers/range_slider_controller.js +68 -3
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +4 -1
- package/dist/controllers/rating_controller.js +53 -0
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +13 -0
- package/dist/controllers/relative_time_controller.js +133 -12
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -11
- package/dist/controllers/sidebar_controller.js +37 -8
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +7 -2
- package/dist/controllers/skeleton_controller.js +73 -20
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js +17 -2
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +37 -4
- package/dist/controllers/spinner_controller.js +228 -27
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +12 -1
- package/dist/controllers/step_indicator_controller.js +82 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +26 -2
- package/dist/controllers/stick_to_bottom_controller.js +60 -10
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/index.js +1056 -281
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.js +785 -76
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +785 -76
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +5 -5
- package/dist/inspector/manifest.json +21 -8
- package/package.json +1 -1
|
@@ -52,6 +52,39 @@ function toISOMonthString(date) {
|
|
|
52
52
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// src/utils/microtask_coalescer.ts
|
|
56
|
+
var MicrotaskCoalescer = class {
|
|
57
|
+
#run;
|
|
58
|
+
#queued = false;
|
|
59
|
+
#active = false;
|
|
60
|
+
#generation = 0;
|
|
61
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
62
|
+
constructor(run) {
|
|
63
|
+
this.#run = run;
|
|
64
|
+
}
|
|
65
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
66
|
+
activate() {
|
|
67
|
+
this.#active = true;
|
|
68
|
+
}
|
|
69
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
70
|
+
cancel() {
|
|
71
|
+
this.#active = false;
|
|
72
|
+
this.#queued = false;
|
|
73
|
+
this.#generation += 1;
|
|
74
|
+
}
|
|
75
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
76
|
+
schedule() {
|
|
77
|
+
if (!this.#active || this.#queued) return;
|
|
78
|
+
this.#queued = true;
|
|
79
|
+
const generation = this.#generation;
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
82
|
+
this.#queued = false;
|
|
83
|
+
this.#run();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
55
88
|
// src/utils/safe_timeout.ts
|
|
56
89
|
var TimerRegistry = class {
|
|
57
90
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -129,7 +162,15 @@ var DateRangePickerController = class extends Controller {
|
|
|
129
162
|
/** Deferred focus after an async month transition (cancelled on teardown). */
|
|
130
163
|
#focusTimer = new SafeTimeout();
|
|
131
164
|
/** Seeds the range from any pre-filled hidden fields and renders the grid. */
|
|
165
|
+
/**
|
|
166
|
+
* Collapses a morph that swaps render inputs into one repaint, and refuses the
|
|
167
|
+
* pass Stimulus delivers before `connect()`.
|
|
168
|
+
*/
|
|
169
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
170
|
+
this.#render();
|
|
171
|
+
});
|
|
132
172
|
connect() {
|
|
173
|
+
this.#repaint.activate();
|
|
133
174
|
this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
|
|
134
175
|
this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
|
|
135
176
|
const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
|
|
@@ -140,8 +181,17 @@ var DateRangePickerController = class extends Controller {
|
|
|
140
181
|
}
|
|
141
182
|
/** Cancels any pending deferred focus so it never fires on a detached element. */
|
|
142
183
|
disconnect() {
|
|
184
|
+
this.#repaint.cancel();
|
|
143
185
|
this.#focusTimer.clearAll();
|
|
144
186
|
}
|
|
187
|
+
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
188
|
+
minValueChanged() {
|
|
189
|
+
this.#repaint.schedule();
|
|
190
|
+
}
|
|
191
|
+
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
192
|
+
maxValueChanged() {
|
|
193
|
+
this.#repaint.schedule();
|
|
194
|
+
}
|
|
145
195
|
/** Navigates to the previous month. */
|
|
146
196
|
prev(event) {
|
|
147
197
|
event?.preventDefault();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/dates.ts","../../src/utils/safe_timeout.ts","../../src/controllers/date_range_picker_controller.ts"],"names":[],"mappings":"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","file":"date_range_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Local-time date helpers shared by the calendar-family controllers\n * (`calendar`, `date-range-picker`).\n *\n * All conversions are intentionally **local time** (not UTC): the grid a user\n * sees is built from `Date` components in their own timezone, so round-tripping\n * through ISO strings must use the same frame to avoid off-by-one-day drift near\n * midnight. Strings are bare `YYYY-MM-DD` / `YYYY-MM` with no time or zone.\n */\n\n/** Formats a {@link Date} as a local-time `YYYY-MM-DD` string. */\nexport function toISODateString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parses a `YYYY-MM-DD` string into a local-time {@link Date}, or `null`.\n *\n * Rejects calendar-invalid dates (e.g. `2026-02-31`): the constructed `Date`\n * would silently roll over to another day, so the parsed components are checked\n * back against it and a mismatch returns `null`.\n */\nexport function parseISODateString(dateStr: string): Date | null {\n if (!dateStr) return null;\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m, d] = match;\n const year = Number(y);\n const month = Number(m);\n const day = Number(d);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/**\n * Parses a `YYYY-MM` string into its year/month (1-based) parts, or `null`.\n * Rejects months outside `1..12`.\n */\nexport function parseISOMonthString(monthStr: string): { year: number; month: number } | null {\n if (!monthStr) return null;\n const match = monthStr.match(/^(\\d{4})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m] = match;\n const month = Number(m);\n if (month < 1 || month > 12) return null;\n return { year: Number(y), month };\n}\n\n/** Formats a {@link Date} as the local-time `YYYY-MM` string of its month. */\nexport function toISOMonthString(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport {\n parseISODateString,\n parseISOMonthString,\n toISODateString,\n toISOMonthString,\n} from \"../utils/dates\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Number of cells in a six-week month grid (7 × 6). */\nconst GRID_SIZE = 42;\n\n/**\n * Headless, accessible **date range picker** behavior. A derivative of\n * {@link CalendarController}: it reuses the month-grid navigation model and adds\n * two-point range selection, in-progress range preview, and presets.\n *\n * Markup contract (identifier: `stimeo--date-range-picker`):\n * <div data-controller=\"stimeo--date-range-picker\"\n * data-stimeo--date-range-picker-min-value=\"2026-01-01\"\n * data-stimeo--date-range-picker-max-value=\"2026-12-31\">\n * <button data-action=\"stimeo--date-range-picker#prev\">Prev</button>\n * <span data-stimeo--date-range-picker-target=\"monthLabel\" aria-live=\"polite\"></span>\n * <button data-action=\"stimeo--date-range-picker#next\">Next</button>\n * <div role=\"grid\" data-stimeo--date-range-picker-target=\"grid\">\n * <!-- exactly 42 cell targets (7 days × 6 rows) -->\n * <button role=\"gridcell\" tabindex=\"-1\"\n * data-stimeo--date-range-picker-target=\"cell\"\n * data-action=\"click->stimeo--date-range-picker#selectDate\n * mouseenter->stimeo--date-range-picker#previewTo\n * focus->stimeo--date-range-picker#previewTo\n * keydown->stimeo--date-range-picker#onKeydown\"></button>\n * </div>\n * <button data-range=\"last7\" data-action=\"stimeo--date-range-picker#applyPreset\">…</button>\n * <span role=\"status\" aria-live=\"polite\" data-stimeo--date-range-picker-target=\"status\"></span>\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"startField\" />\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"endField\" />\n * </div>\n *\n * @remarks\n * Behavior only — the consumer styles the grid and renders the range using the\n * `data-range-start` / `data-in-range` / `data-range-end` hooks. Assistive tech\n * is told the two confirmed endpoints via `aria-selected`; inner cells use\n * `data-in-range` (visual only) so the announcement stays to the two ends. The\n * confirmed range is also mirrored to the live `status` region.\n *\n * Selection model: the first click/Enter sets a *pending* start and enters\n * \"selecting\" mode (preview follows the pointer/focus); the second confirms the\n * end (auto-swapped if earlier than the start) and dispatches `change`. Escape\n * abandons an in-progress selection, restoring the last confirmed range.\n */\nexport class DateRangePickerController extends Controller<HTMLElement> {\n static override targets = [\"grid\", \"monthLabel\", \"cell\", \"status\", \"startField\", \"endField\"];\n static override values = {\n min: { type: String, default: \"\" },\n max: { type: String, default: \"\" },\n };\n static actions = [\"applyPreset\", \"next\", \"onKeydown\", \"prev\", \"previewTo\", \"selectDate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly gridTarget: HTMLElement;\n declare readonly hasGridTarget: boolean;\n declare readonly monthLabelTarget: HTMLElement;\n declare readonly cellTargets: HTMLElement[];\n declare readonly statusTarget: HTMLElement;\n declare readonly startFieldTarget: HTMLInputElement;\n declare readonly endFieldTarget: HTMLInputElement;\n declare readonly hasMonthLabelTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n declare readonly hasStartFieldTarget: boolean;\n declare readonly hasEndFieldTarget: boolean;\n declare minValue: string;\n declare maxValue: string;\n\n /** The month currently rendered, as `YYYY-MM`. */\n #viewMonth = \"\";\n /** The confirmed range endpoints (ISO), or \"\" when unset. */\n #startDate = \"\";\n #endDate = \"\";\n /** The first endpoint of an in-progress selection (ISO), or \"\" when idle. */\n #pendingStart = \"\";\n /** The hovered/focused date previewed while selecting (ISO), or \"\". */\n #previewDate = \"\";\n /** The roving-focus date in the grid (local time). */\n #focusedDate = new Date();\n\n /** Deferred focus after an async month transition (cancelled on teardown). */\n readonly #focusTimer = new SafeTimeout();\n\n /** Seeds the range from any pre-filled hidden fields and renders the grid. */\n override connect(): void {\n this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : \"\";\n this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : \"\";\n\n const anchor =\n parseISODateString(this.#startDate) ?? this.#clampToBounds(new Date()) ?? new Date();\n this.#focusedDate = anchor;\n this.#viewMonth = toISOMonthString(anchor);\n // Two cells carry `aria-selected=\"true\"` whenever a range is confirmed, so the\n // grid has to say that more than one is selectable — otherwise a single-select\n // grid is claiming two selections.\n if (this.hasGridTarget) this.gridTarget.setAttribute(\"aria-multiselectable\", \"true\");\n this.#render();\n }\n\n /** Cancels any pending deferred focus so it never fires on a detached element. */\n override disconnect(): void {\n this.#focusTimer.clearAll();\n }\n\n /** Navigates to the previous month. */\n prev(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(-1);\n }\n\n /** Navigates to the next month. */\n next(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(1);\n }\n\n /** Confirms a range endpoint from a clicked cell. */\n selectDate(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date || cell.getAttribute(\"aria-disabled\") === \"true\") return;\n this.#choose(date);\n }\n\n /** Previews the range up to a hovered/focused cell while selecting. */\n previewTo(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date) return;\n // Focus moves the roving tabindex; hover does not.\n if (event.type.startsWith(\"focus\")) {\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n // Re-roll the roving tab stop only when focus landed on a cell that is not\n // already the tab stop — e.g. Tab/click/programmatic focus from outside the\n // grid's own keyboard navigation (which already rendered the new stop). This\n // guard avoids a redundant double-render on the controller's own arrow moves\n // (onKeydown → #render focuses the new stop, which then fires this focus).\n if (!this.#pendingStart && cell.getAttribute(\"tabindex\") !== \"0\") {\n this.#render();\n }\n }\n if (this.#pendingStart) {\n this.#previewDate = date;\n this.#render();\n }\n }\n\n /** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */\n applyPreset(event: Event): void {\n const button = (event.target as HTMLElement | null)?.closest(\"[data-range]\");\n const name = button?.getAttribute(\"data-range\");\n if (!name) return;\n const range = computePreset(name);\n if (!range) return;\n\n const start = this.#clampISO(range.start);\n const end = this.#clampISO(range.end);\n if (!start || !end) return;\n\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n const endDate = parseISODateString(end);\n if (endDate) this.#focusedDate = endDate;\n this.#commitFields();\n this.#transitionTo(toISOMonthString(parseISODateString(end) ?? new Date()), end);\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO move the\n // grid focus or choose a date — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const dateStr = cell.getAttribute(\"data-date\");\n const date = dateStr ? parseISODateString(dateStr) : null;\n if (!date) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n if (dateStr && cell.getAttribute(\"aria-disabled\") !== \"true\") this.#choose(dateStr);\n return;\n }\n if (event.key === \"Escape\") {\n // A press an inner handler already owned is yielded at the top of this\n // handler. What is checked here is the IME half: a press during a\n // composition steers the conversion, not the range.\n if (this.#pendingStart && !event.isComposing) {\n event.preventDefault();\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#render();\n }\n return;\n }\n\n let next: Date | null = null;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their guards\n // differ — so swapping the key keeps each guard with its own direction.\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowLeft\":\n next = addDays(date, -1);\n break;\n case \"ArrowRight\":\n next = addDays(date, 1);\n break;\n case \"ArrowUp\":\n next = addDays(date, -7);\n break;\n case \"ArrowDown\":\n next = addDays(date, 7);\n break;\n case \"Home\":\n next = addDays(date, -date.getDay());\n break;\n case \"End\":\n next = addDays(date, 6 - date.getDay());\n break;\n case \"PageUp\":\n next = shiftMonthClamped(date, -1);\n break;\n case \"PageDown\":\n next = shiftMonthClamped(date, 1);\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#moveFocusTo(next);\n }\n\n /** Records a chosen date as either the pending start or the confirmed end. */\n #choose(date: string): void {\n if (!this.#pendingStart) {\n this.#pendingStart = date;\n this.#previewDate = date;\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n this.#render();\n return;\n }\n // Second click confirms; order the two so start ≤ end.\n const [start, end] =\n date < this.#pendingStart ? [date, this.#pendingStart] : [this.#pendingStart, date];\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#commitFields();\n this.#render();\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Moves roving focus to `date`, transitioning the month when needed. */\n #moveFocusTo(date: Date): void {\n this.#focusedDate = date;\n if (this.#pendingStart) this.#previewDate = toISODateString(date);\n this.#transitionTo(toISOMonthString(date), toISODateString(date));\n }\n\n /** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */\n #transitionTo(month: string, dateStr: string): void {\n const isTransition = month !== this.#viewMonth;\n this.#viewMonth = month;\n this.#render();\n const focusCell = (): void => {\n this.cellTargets.find((c) => c.getAttribute(\"data-date\") === dateStr)?.focus();\n };\n if (isTransition) {\n // Let the synchronous render settle before focusing the freshly bound cell.\n this.#focusTimer.set(focusCell, 0);\n } else {\n focusCell();\n }\n }\n\n /** Shifts the displayed month by `delta`, keeping focus within it. */\n #shiftMonth(delta: number): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const target = new Date(info.year, info.month - 1 + delta, 1);\n this.#viewMonth = toISOMonthString(target);\n // Keep the same day-of-month where possible, clamped to the new month.\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(this.#focusedDate.getDate(), lastDay));\n this.#focusedDate = target;\n this.#render();\n }\n\n /** Builds the six-week grid and binds range/roving/disabled state per cell. */\n #render(): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const { year, month } = info;\n\n if (this.hasMonthLabelTarget) {\n const lang = document.documentElement.lang || \"en\";\n const formatter = new Intl.DateTimeFormat(lang, { month: \"long\", year: \"numeric\" });\n this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));\n }\n\n const [rangeStart, rangeEnd] = this.#visualRange();\n const days = gridDays(year, month);\n const focusedStr = toISODateString(this.#focusedDate);\n const todayStr = toISODateString(new Date());\n\n for (let i = 0; i < GRID_SIZE; i++) {\n const el = this.cellTargets[i];\n const date = days[i];\n if (!el || !date) continue;\n const iso = toISODateString(date);\n\n el.setAttribute(\"data-date\", iso);\n el.textContent = String(date.getDate());\n el.setAttribute(\"data-outside\", String(date.getMonth() !== month - 1));\n el.setAttribute(\"data-today\", String(iso === todayStr));\n el.setAttribute(\"tabindex\", iso === focusedStr ? \"0\" : \"-1\");\n\n if (this.#outOfBounds(iso)) el.setAttribute(\"aria-disabled\", \"true\");\n else el.removeAttribute(\"aria-disabled\");\n\n const isStart = !!rangeStart && iso === rangeStart;\n const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;\n const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;\n el.toggleAttribute(\"data-range-start\", isStart);\n el.toggleAttribute(\"data-range-end\", isEnd);\n el.toggleAttribute(\"data-in-range\", inside);\n // Selection is what the user committed, never what the pointer is hovering:\n // `aria-selected` marks the two confirmed endpoints only. The preview lives\n // in the `data-*` trio above, which styling reads and AT does not.\n el.setAttribute(\n \"aria-selected\",\n String(\n (!!this.#startDate && iso === this.#startDate) ||\n (!!this.#endDate && iso === this.#endDate),\n ),\n );\n }\n }\n\n /** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */\n #visualRange(): [string, string] {\n if (this.#pendingStart) {\n const other = this.#previewDate || this.#pendingStart;\n return this.#pendingStart <= other\n ? [this.#pendingStart, other]\n : [other, this.#pendingStart];\n }\n return [this.#startDate, this.#endDate];\n }\n\n /** Writes the confirmed range to the hidden fields. */\n #commitFields(): void {\n if (this.hasStartFieldTarget) this.startFieldTarget.value = this.#startDate;\n if (this.hasEndFieldTarget) this.endFieldTarget.value = this.#endDate;\n }\n\n /** Announces the confirmed range in the live status region. */\n #announce(): void {\n if (this.hasStatusTarget && this.#startDate && this.#endDate) {\n this.statusTarget.textContent = `${this.#startDate} – ${this.#endDate}`;\n }\n }\n\n /** True when `iso` falls outside the `[min, max]` bounds. */\n #outOfBounds(iso: string): boolean {\n if (this.minValue && iso < this.minValue) return true;\n if (this.maxValue && iso > this.maxValue) return true;\n return false;\n }\n\n /** Clamps an ISO date string into `[min, max]`, or \"\" when unparseable. */\n #clampISO(iso: string): string {\n if (!iso) return \"\";\n if (this.minValue && iso < this.minValue) return this.minValue;\n if (this.maxValue && iso > this.maxValue) return this.maxValue;\n return iso;\n }\n\n /** Clamps a Date into `[min, max]`, returning null only when unparseable. */\n #clampToBounds(date: Date): Date | null {\n const clamped = this.#clampISO(toISODateString(date));\n return parseISODateString(clamped);\n }\n\n /** Resolves the cell element from an event target, or null. */\n #cellFrom(target: EventTarget | null): HTMLElement | null {\n return (\n (target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--date-range-picker-target='cell']\",\n ) ?? null\n );\n }\n}\n\n/** Returns a new Date `n` days from `date`. */\nfunction addDays(date: Date, n: number): Date {\n const next = new Date(date);\n next.setDate(next.getDate() + n);\n return next;\n}\n\n/** Shifts `date` by `delta` months, clamping the day to the target month length. */\nfunction shiftMonthClamped(date: Date, delta: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(date.getDate(), lastDay));\n return target;\n}\n\n/** Builds the 42 local-time dates of a Sunday-started six-week grid for a month. */\nfunction gridDays(year: number, month: number): Date[] {\n const first = new Date(year, month - 1, 1);\n const start = new Date(first);\n start.setDate(first.getDate() - first.getDay());\n const days: Date[] = [];\n const current = new Date(start);\n for (let i = 0; i < GRID_SIZE; i++) {\n days.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n return days;\n}\n\n/** Validates and re-serializes an ISO date string, returning \"\" when invalid. */\nfunction normalizeISO(value: string): string {\n const date = parseISODateString(value.trim());\n return date ? toISODateString(date) : \"\";\n}\n\n/** Computes a preset range relative to today, or null for an unknown name. */\nfunction computePreset(name: string): { start: string; end: string } | null {\n const today = new Date();\n const todayStr = toISODateString(today);\n switch (name) {\n case \"today\":\n return { start: todayStr, end: todayStr };\n case \"last7\":\n return { start: toISODateString(addDays(today, -6)), end: todayStr };\n case \"last30\":\n return { start: toISODateString(addDays(today, -29)), end: todayStr };\n case \"thisMonth\": {\n const start = new Date(today.getFullYear(), today.getMonth(), 1);\n const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);\n return { start: toISODateString(start), end: toISODateString(end) };\n }\n default:\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/dates.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/date_range_picker_controller.ts"],"names":[],"mappings":"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","file":"date_range_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Local-time date helpers shared by the calendar-family controllers\n * (`calendar`, `date-range-picker`).\n *\n * All conversions are intentionally **local time** (not UTC): the grid a user\n * sees is built from `Date` components in their own timezone, so round-tripping\n * through ISO strings must use the same frame to avoid off-by-one-day drift near\n * midnight. Strings are bare `YYYY-MM-DD` / `YYYY-MM` with no time or zone.\n */\n\n/** Formats a {@link Date} as a local-time `YYYY-MM-DD` string. */\nexport function toISODateString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Parses a `YYYY-MM-DD` string into a local-time {@link Date}, or `null`.\n *\n * Rejects calendar-invalid dates (e.g. `2026-02-31`): the constructed `Date`\n * would silently roll over to another day, so the parsed components are checked\n * back against it and a mismatch returns `null`.\n */\nexport function parseISODateString(dateStr: string): Date | null {\n if (!dateStr) return null;\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m, d] = match;\n const year = Number(y);\n const month = Number(m);\n const day = Number(d);\n const date = new Date(year, month - 1, day);\n if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n return date;\n}\n\n/**\n * Parses a `YYYY-MM` string into its year/month (1-based) parts, or `null`.\n * Rejects months outside `1..12`.\n */\nexport function parseISOMonthString(monthStr: string): { year: number; month: number } | null {\n if (!monthStr) return null;\n const match = monthStr.match(/^(\\d{4})-(\\d{2})$/);\n if (!match) return null;\n const [, y, m] = match;\n const month = Number(m);\n if (month < 1 || month > 12) return null;\n return { year: Number(y), month };\n}\n\n/** Formats a {@link Date} as the local-time `YYYY-MM` string of its month. */\nexport function toISOMonthString(date: Date): string {\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}`;\n}\n","/**\n * Collapses many target callbacks from one DOM mutation into a single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element, so\n * replacing a list of N options delivers N callbacks — but the useful unit of\n * work is \"reconcile against the DOM that resulted\", once, after the batch has\n * settled. Every controller that owns a reconcilable target set needs the same\n * shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers the *initial* target callbacks\n * ahead of `connect()`. Reconciling there would compute a fallback against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport {\n parseISODateString,\n parseISOMonthString,\n toISODateString,\n toISOMonthString,\n} from \"../utils/dates\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Number of cells in a six-week month grid (7 × 6). */\nconst GRID_SIZE = 42;\n\n/**\n * Headless, accessible **date range picker** behavior. A derivative of\n * {@link CalendarController}: it reuses the month-grid navigation model and adds\n * two-point range selection, in-progress range preview, and presets.\n *\n * Markup contract (identifier: `stimeo--date-range-picker`):\n * <div data-controller=\"stimeo--date-range-picker\"\n * data-stimeo--date-range-picker-min-value=\"2026-01-01\"\n * data-stimeo--date-range-picker-max-value=\"2026-12-31\">\n * <button data-action=\"stimeo--date-range-picker#prev\">Prev</button>\n * <span data-stimeo--date-range-picker-target=\"monthLabel\" aria-live=\"polite\"></span>\n * <button data-action=\"stimeo--date-range-picker#next\">Next</button>\n * <div role=\"grid\" data-stimeo--date-range-picker-target=\"grid\">\n * <!-- exactly 42 cell targets (7 days × 6 rows) -->\n * <button role=\"gridcell\" tabindex=\"-1\"\n * data-stimeo--date-range-picker-target=\"cell\"\n * data-action=\"click->stimeo--date-range-picker#selectDate\n * mouseenter->stimeo--date-range-picker#previewTo\n * focus->stimeo--date-range-picker#previewTo\n * keydown->stimeo--date-range-picker#onKeydown\"></button>\n * </div>\n * <button data-range=\"last7\" data-action=\"stimeo--date-range-picker#applyPreset\">…</button>\n * <span role=\"status\" aria-live=\"polite\" data-stimeo--date-range-picker-target=\"status\"></span>\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"startField\" />\n * <input type=\"hidden\" data-stimeo--date-range-picker-target=\"endField\" />\n * </div>\n *\n * @remarks\n * Behavior only — the consumer styles the grid and renders the range using the\n * `data-range-start` / `data-in-range` / `data-range-end` hooks. Assistive tech\n * is told the two confirmed endpoints via `aria-selected`; inner cells use\n * `data-in-range` (visual only) so the announcement stays to the two ends. The\n * confirmed range is also mirrored to the live `status` region.\n *\n * Selection model: the first click/Enter sets a *pending* start and enters\n * \"selecting\" mode (preview follows the pointer/focus); the second confirms the\n * end (auto-swapped if earlier than the start) and dispatches `change`. Escape\n * abandons an in-progress selection, restoring the last confirmed range.\n */\nexport class DateRangePickerController extends Controller<HTMLElement> {\n static override targets = [\"grid\", \"monthLabel\", \"cell\", \"status\", \"startField\", \"endField\"];\n static override values = {\n min: { type: String, default: \"\" },\n max: { type: String, default: \"\" },\n };\n static actions = [\"applyPreset\", \"next\", \"onKeydown\", \"prev\", \"previewTo\", \"selectDate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly gridTarget: HTMLElement;\n declare readonly hasGridTarget: boolean;\n declare readonly monthLabelTarget: HTMLElement;\n declare readonly cellTargets: HTMLElement[];\n declare readonly statusTarget: HTMLElement;\n declare readonly startFieldTarget: HTMLInputElement;\n declare readonly endFieldTarget: HTMLInputElement;\n declare readonly hasMonthLabelTarget: boolean;\n declare readonly hasStatusTarget: boolean;\n declare readonly hasStartFieldTarget: boolean;\n declare readonly hasEndFieldTarget: boolean;\n declare minValue: string;\n declare maxValue: string;\n\n /** The month currently rendered, as `YYYY-MM`. */\n #viewMonth = \"\";\n /** The confirmed range endpoints (ISO), or \"\" when unset. */\n #startDate = \"\";\n #endDate = \"\";\n /** The first endpoint of an in-progress selection (ISO), or \"\" when idle. */\n #pendingStart = \"\";\n /** The hovered/focused date previewed while selecting (ISO), or \"\". */\n #previewDate = \"\";\n /** The roving-focus date in the grid (local time). */\n #focusedDate = new Date();\n\n /** Deferred focus after an async month transition (cancelled on teardown). */\n readonly #focusTimer = new SafeTimeout();\n\n /** Seeds the range from any pre-filled hidden fields and renders the grid. */\n /**\n * Collapses a morph that swaps render inputs into one repaint, and refuses the\n * pass Stimulus delivers before `connect()`.\n */\n readonly #repaint = new MicrotaskCoalescer(() => {\n this.#render();\n });\n\n override connect(): void {\n this.#repaint.activate();\n this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : \"\";\n this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : \"\";\n\n const anchor =\n parseISODateString(this.#startDate) ?? this.#clampToBounds(new Date()) ?? new Date();\n this.#focusedDate = anchor;\n this.#viewMonth = toISOMonthString(anchor);\n // Two cells carry `aria-selected=\"true\"` whenever a range is confirmed, so the\n // grid has to say that more than one is selectable — otherwise a single-select\n // grid is claiming two selections.\n if (this.hasGridTarget) this.gridTarget.setAttribute(\"aria-multiselectable\", \"true\");\n this.#render();\n }\n\n /** Cancels any pending deferred focus so it never fires on a detached element. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#focusTimer.clearAll();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Navigates to the previous month. */\n prev(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(-1);\n }\n\n /** Navigates to the next month. */\n next(event?: Event): void {\n event?.preventDefault();\n this.#shiftMonth(1);\n }\n\n /** Confirms a range endpoint from a clicked cell. */\n selectDate(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date || cell.getAttribute(\"aria-disabled\") === \"true\") return;\n this.#choose(date);\n }\n\n /** Previews the range up to a hovered/focused cell while selecting. */\n previewTo(event: Event): void {\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const date = cell.getAttribute(\"data-date\");\n if (!date) return;\n // Focus moves the roving tabindex; hover does not.\n if (event.type.startsWith(\"focus\")) {\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n // Re-roll the roving tab stop only when focus landed on a cell that is not\n // already the tab stop — e.g. Tab/click/programmatic focus from outside the\n // grid's own keyboard navigation (which already rendered the new stop). This\n // guard avoids a redundant double-render on the controller's own arrow moves\n // (onKeydown → #render focuses the new stop, which then fires this focus).\n if (!this.#pendingStart && cell.getAttribute(\"tabindex\") !== \"0\") {\n this.#render();\n }\n }\n if (this.#pendingStart) {\n this.#previewDate = date;\n this.#render();\n }\n }\n\n /** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */\n applyPreset(event: Event): void {\n const button = (event.target as HTMLElement | null)?.closest(\"[data-range]\");\n const name = button?.getAttribute(\"data-range\");\n if (!name) return;\n const range = computePreset(name);\n if (!range) return;\n\n const start = this.#clampISO(range.start);\n const end = this.#clampISO(range.end);\n if (!start || !end) return;\n\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n const endDate = parseISODateString(end);\n if (endDate) this.#focusedDate = endDate;\n this.#commitFields();\n this.#transitionTo(toISOMonthString(parseISODateString(end) ?? new Date()), end);\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO move the\n // grid focus or choose a date — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const cell = this.#cellFrom(event.target);\n if (!cell) return;\n const dateStr = cell.getAttribute(\"data-date\");\n const date = dateStr ? parseISODateString(dateStr) : null;\n if (!date) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n if (dateStr && cell.getAttribute(\"aria-disabled\") !== \"true\") this.#choose(dateStr);\n return;\n }\n if (event.key === \"Escape\") {\n // A press an inner handler already owned is yielded at the top of this\n // handler. What is checked here is the IME half: a press during a\n // composition steers the conversion, not the range.\n if (this.#pendingStart && !event.isComposing) {\n event.preventDefault();\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#render();\n }\n return;\n }\n\n let next: Date | null = null;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their guards\n // differ — so swapping the key keeps each guard with its own direction.\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowLeft\":\n next = addDays(date, -1);\n break;\n case \"ArrowRight\":\n next = addDays(date, 1);\n break;\n case \"ArrowUp\":\n next = addDays(date, -7);\n break;\n case \"ArrowDown\":\n next = addDays(date, 7);\n break;\n case \"Home\":\n next = addDays(date, -date.getDay());\n break;\n case \"End\":\n next = addDays(date, 6 - date.getDay());\n break;\n case \"PageUp\":\n next = shiftMonthClamped(date, -1);\n break;\n case \"PageDown\":\n next = shiftMonthClamped(date, 1);\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#moveFocusTo(next);\n }\n\n /** Records a chosen date as either the pending start or the confirmed end. */\n #choose(date: string): void {\n if (!this.#pendingStart) {\n this.#pendingStart = date;\n this.#previewDate = date;\n const parsed = parseISODateString(date);\n if (parsed) this.#focusedDate = parsed;\n this.#render();\n return;\n }\n // Second click confirms; order the two so start ≤ end.\n const [start, end] =\n date < this.#pendingStart ? [date, this.#pendingStart] : [this.#pendingStart, date];\n this.#startDate = start;\n this.#endDate = end;\n this.#pendingStart = \"\";\n this.#previewDate = \"\";\n this.#commitFields();\n this.#render();\n this.#announce();\n this.dispatch(\"change\", { detail: { start, end } });\n }\n\n /** Moves roving focus to `date`, transitioning the month when needed. */\n #moveFocusTo(date: Date): void {\n this.#focusedDate = date;\n if (this.#pendingStart) this.#previewDate = toISODateString(date);\n this.#transitionTo(toISOMonthString(date), toISODateString(date));\n }\n\n /** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */\n #transitionTo(month: string, dateStr: string): void {\n const isTransition = month !== this.#viewMonth;\n this.#viewMonth = month;\n this.#render();\n const focusCell = (): void => {\n this.cellTargets.find((c) => c.getAttribute(\"data-date\") === dateStr)?.focus();\n };\n if (isTransition) {\n // Let the synchronous render settle before focusing the freshly bound cell.\n this.#focusTimer.set(focusCell, 0);\n } else {\n focusCell();\n }\n }\n\n /** Shifts the displayed month by `delta`, keeping focus within it. */\n #shiftMonth(delta: number): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const target = new Date(info.year, info.month - 1 + delta, 1);\n this.#viewMonth = toISOMonthString(target);\n // Keep the same day-of-month where possible, clamped to the new month.\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(this.#focusedDate.getDate(), lastDay));\n this.#focusedDate = target;\n this.#render();\n }\n\n /** Builds the six-week grid and binds range/roving/disabled state per cell. */\n #render(): void {\n const info = parseISOMonthString(this.#viewMonth);\n if (!info) return;\n const { year, month } = info;\n\n if (this.hasMonthLabelTarget) {\n const lang = document.documentElement.lang || \"en\";\n const formatter = new Intl.DateTimeFormat(lang, { month: \"long\", year: \"numeric\" });\n this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));\n }\n\n const [rangeStart, rangeEnd] = this.#visualRange();\n const days = gridDays(year, month);\n const focusedStr = toISODateString(this.#focusedDate);\n const todayStr = toISODateString(new Date());\n\n for (let i = 0; i < GRID_SIZE; i++) {\n const el = this.cellTargets[i];\n const date = days[i];\n if (!el || !date) continue;\n const iso = toISODateString(date);\n\n el.setAttribute(\"data-date\", iso);\n el.textContent = String(date.getDate());\n el.setAttribute(\"data-outside\", String(date.getMonth() !== month - 1));\n el.setAttribute(\"data-today\", String(iso === todayStr));\n el.setAttribute(\"tabindex\", iso === focusedStr ? \"0\" : \"-1\");\n\n if (this.#outOfBounds(iso)) el.setAttribute(\"aria-disabled\", \"true\");\n else el.removeAttribute(\"aria-disabled\");\n\n const isStart = !!rangeStart && iso === rangeStart;\n const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;\n const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;\n el.toggleAttribute(\"data-range-start\", isStart);\n el.toggleAttribute(\"data-range-end\", isEnd);\n el.toggleAttribute(\"data-in-range\", inside);\n // Selection is what the user committed, never what the pointer is hovering:\n // `aria-selected` marks the two confirmed endpoints only. The preview lives\n // in the `data-*` trio above, which styling reads and AT does not.\n el.setAttribute(\n \"aria-selected\",\n String(\n (!!this.#startDate && iso === this.#startDate) ||\n (!!this.#endDate && iso === this.#endDate),\n ),\n );\n }\n }\n\n /** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */\n #visualRange(): [string, string] {\n if (this.#pendingStart) {\n const other = this.#previewDate || this.#pendingStart;\n return this.#pendingStart <= other\n ? [this.#pendingStart, other]\n : [other, this.#pendingStart];\n }\n return [this.#startDate, this.#endDate];\n }\n\n /** Writes the confirmed range to the hidden fields. */\n #commitFields(): void {\n if (this.hasStartFieldTarget) this.startFieldTarget.value = this.#startDate;\n if (this.hasEndFieldTarget) this.endFieldTarget.value = this.#endDate;\n }\n\n /** Announces the confirmed range in the live status region. */\n #announce(): void {\n if (this.hasStatusTarget && this.#startDate && this.#endDate) {\n this.statusTarget.textContent = `${this.#startDate} – ${this.#endDate}`;\n }\n }\n\n /** True when `iso` falls outside the `[min, max]` bounds. */\n #outOfBounds(iso: string): boolean {\n if (this.minValue && iso < this.minValue) return true;\n if (this.maxValue && iso > this.maxValue) return true;\n return false;\n }\n\n /** Clamps an ISO date string into `[min, max]`, or \"\" when unparseable. */\n #clampISO(iso: string): string {\n if (!iso) return \"\";\n if (this.minValue && iso < this.minValue) return this.minValue;\n if (this.maxValue && iso > this.maxValue) return this.maxValue;\n return iso;\n }\n\n /** Clamps a Date into `[min, max]`, returning null only when unparseable. */\n #clampToBounds(date: Date): Date | null {\n const clamped = this.#clampISO(toISODateString(date));\n return parseISODateString(clamped);\n }\n\n /** Resolves the cell element from an event target, or null. */\n #cellFrom(target: EventTarget | null): HTMLElement | null {\n return (\n (target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--date-range-picker-target='cell']\",\n ) ?? null\n );\n }\n}\n\n/** Returns a new Date `n` days from `date`. */\nfunction addDays(date: Date, n: number): Date {\n const next = new Date(date);\n next.setDate(next.getDate() + n);\n return next;\n}\n\n/** Shifts `date` by `delta` months, clamping the day to the target month length. */\nfunction shiftMonthClamped(date: Date, delta: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();\n target.setDate(Math.min(date.getDate(), lastDay));\n return target;\n}\n\n/** Builds the 42 local-time dates of a Sunday-started six-week grid for a month. */\nfunction gridDays(year: number, month: number): Date[] {\n const first = new Date(year, month - 1, 1);\n const start = new Date(first);\n start.setDate(first.getDate() - first.getDay());\n const days: Date[] = [];\n const current = new Date(start);\n for (let i = 0; i < GRID_SIZE; i++) {\n days.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n return days;\n}\n\n/** Validates and re-serializes an ISO date string, returning \"\" when invalid. */\nfunction normalizeISO(value: string): string {\n const date = parseISODateString(value.trim());\n return date ? toISODateString(date) : \"\";\n}\n\n/** Computes a preset range relative to today, or null for an unknown name. */\nfunction computePreset(name: string): { start: string; end: string } | null {\n const today = new Date();\n const todayStr = toISODateString(today);\n switch (name) {\n case \"today\":\n return { start: todayStr, end: todayStr };\n case \"last7\":\n return { start: toISODateString(addDays(today, -6)), end: todayStr };\n case \"last30\":\n return { start: toISODateString(addDays(today, -29)), end: todayStr };\n case \"thisMonth\": {\n const start = new Date(today.getFullYear(), today.getMonth(), 1);\n const end = new Date(today.getFullYear(), today.getMonth() + 1, 0);\n return { start: toISODateString(start), end: toISODateString(end) };\n }\n default:\n return null;\n }\n}\n"]}
|
|
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/dialog_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/before_cache_reset.ts
|
|
6
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
+
static #onBeforeCache = () => {
|
|
11
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
+
};
|
|
13
|
+
#rewind;
|
|
14
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
+
constructor(rewind) {
|
|
16
|
+
this.#rewind = rewind;
|
|
17
|
+
}
|
|
18
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
+
activate() {
|
|
20
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
+
if (first) {
|
|
23
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
+
deactivate() {
|
|
28
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
5
34
|
// src/utils/escape_layer.ts
|
|
6
35
|
var EscapeLayer = class _EscapeLayer {
|
|
7
36
|
static #registries = /* @__PURE__ */ new WeakMap();
|
|
@@ -135,7 +164,7 @@ var FocusTrap = class {
|
|
|
135
164
|
}
|
|
136
165
|
if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
|
|
137
166
|
document.addEventListener("keydown", this.#onKeydown);
|
|
138
|
-
|
|
167
|
+
this.#beforeCache.activate();
|
|
139
168
|
const onEscape = this.#options.onEscape;
|
|
140
169
|
if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
|
|
141
170
|
if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
|
|
@@ -152,7 +181,7 @@ var FocusTrap = class {
|
|
|
152
181
|
this.#activeState = false;
|
|
153
182
|
this.#escapeLayer.deactivate();
|
|
154
183
|
document.removeEventListener("keydown", this.#onKeydown);
|
|
155
|
-
|
|
184
|
+
this.#beforeCache.deactivate();
|
|
156
185
|
if (this.#scrollLocked) {
|
|
157
186
|
document.body.style.overflow = this.#previousBodyOverflow;
|
|
158
187
|
this.#scrollLocked = false;
|
|
@@ -176,9 +205,7 @@ var FocusTrap = class {
|
|
|
176
205
|
* untouched (restore-open designs reopen against a clean baseline), and focus
|
|
177
206
|
* is left alone mid-navigation. The listener lives only while active.
|
|
178
207
|
*/
|
|
179
|
-
#
|
|
180
|
-
this.deactivate({ restoreFocus: false });
|
|
181
|
-
};
|
|
208
|
+
#beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
|
|
182
209
|
/**
|
|
183
210
|
* Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
|
|
184
211
|
* shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":";;;;;AAuEO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA,EACvB,OAAgB,WAAA,mBAAc,IAAI,OAAA,EAAuC;AAAA,EAEzE,cAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,UAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,OAAA,GAAkC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,QAAA,CAAS,aAAA,GAA0B,QAAA,EAAU,OAAA,EAAmC;AAC9E,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AACxD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,aAAY,eAAA,EAAgB;AACvC,MAAA,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAA,EAAe,QAAQ,CAAA;AACnD,MAAA,aAAA,CAAc,gBAAA,CAAiB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAAA,IAC9D;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,aAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,MAAA,IAAU,IAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,GAAmB;AACjB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AAC7C,MAAA,IAAI,SAAS,CAAA,EAAG,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9C,MAAA,IAAI,QAAA,CAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC/B,QAAA,aAAA,CAAc,mBAAA,CAAoB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAC/D,QAAA,YAAA,CAAY,WAAA,CAAY,OAAO,aAAa,CAAA;AAAA,MAC9C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAA,GAAsB;AACxB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AACtB,IAAA,OAAO,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA,KAAM,IAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,eAAA,GAAuC;AAC5C,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,OAAO,EAAC;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,KAA+B;AACzC,QAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,QAAA,MAAM,KAAA,GAAQ,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA;AACtD,QAAA,IAAI,CAAC,KAAA,EAAO;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,UAAA,IAAa;AAAA,MACrB;AAAA,KACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,cAAc,KAAA,EAA0C;AAC7D,IAAA,KAAA,IAAS,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,KAAA,IAAS,GAAG,KAAA,EAAA,EAAS;AACtD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,SAAQ,EAAG;AACvC,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;;;AC9HO,IAAM,SAAA,GACX,2IAAA;AAgDK,IAAM,YAAN,MAAgB;AAAA;AAAA,EAErB,kBAAA,GAAyC,IAAA;AAAA;AAAA,EAEzC,qBAAA,GAAwB,EAAA;AAAA;AAAA,EAExB,aAAA,GAAgB,KAAA;AAAA;AAAA,EAEhB,mBAAkC,EAAC;AAAA;AAAA,EAEnC,YAAA,GAAe,KAAA;AAAA;AAAA,EAEN,YAAA,GAAe,IAAI,WAAA,EAAY;AAAA;AAAA,EAG/B,aAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,WAAA,CAAY,YAAA,EAAiC,OAAA,GAA4B,EAAC,EAAG;AAC3E,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,YAAA,EAAc;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAIpB,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAA,CAAK,qBACH,MAAA,YAAkB,WAAA,IAAe,MAAA,KAAW,QAAA,CAAS,OAAO,MAAA,GAAS,IAAA;AACvE,IAAA,IAAI,KAAK,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,IAAI,CAAA,EAAG;AAC9C,MAAA,IAAA,CAAK,qBAAA,GAAwB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AACjD,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,MAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,IACvB;AACA,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA,OAAQ,kBAAA,EAAmB;AACrE,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AACnE,IAAA,MAAM,QAAA,GAAW,KAAK,QAAA,CAAS,QAAA;AAC/B,IAAA,IAAI,QAAA,EAAU,IAAA,CAAK,YAAA,CAAa,QAAA,CAAS,QAAA,EAAU,EAAE,SAAA,EAAW,MAAM,QAAA,EAAS,EAAG,CAAA;AAClF,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,WAAW,IAAI,CAAA,OAAQ,aAAA,EAAc;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAW,EAAE,YAAA,GAAe,IAAA,EAAK,GAAgC,EAAC,EAAS;AACzE,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AACpB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACvD,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AACtE,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,IAAA,CAAK,qBAAA;AACpC,MAAA,IAAA,CAAK,aAAA,GAAgB,KAAA;AAAA,IACvB;AACA,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,MAAM,SAAS,IAAA,CAAK,kBAAA,IAAsB,IAAA,CAAK,QAAA,CAAS,iBAAgB,IAAK,IAAA;AAC7E,MAAA,MAAA,EAAQ,KAAA,EAAM;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,KAAA,CAAM,QAA+C,QAAA,EAA4B;AAC/E,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,QAAA;AACjC,IAAA,OAAO,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,EAAO,GAAI,MAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUS,iBAAiB,MAAY;AACpC,IAAA,IAAA,CAAK,UAAA,CAAW,EAAE,YAAA,EAAc,KAAA,EAAO,CAAA;AAAA,EACzC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EAC9C,CAAA;AAAA;AAAA,EAGA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,MAAM,IAAA,GAAO,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAC3C,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AAGxB,IAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,IAAS,CAAC,KAAK,aAAA,EAAc,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG;AACvE,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,CAAM,QAAA,IAAY,MAAA,KAAW,KAAA,EAAO;AACtC,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,EAAM,KAAA,EAAM;AAAA,IACd,CAAA,MAAA,IAAW,CAAC,KAAA,CAAM,QAAA,IAAY,WAAW,IAAA,EAAM;AAC7C,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,kBAAA,GAA2B;AACzB,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,IAAA,CAAK,mBAAmB,EAAC;AACzB,IAAA,KAAA,MAAW,WAAW,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACxD,MAAA,IAAI,EAAE,mBAAmB,WAAA,CAAA,EAAc;AACvC,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,SAAS,CAAA,IAAK,QAAQ,KAAA,EAAO;AAClD,MAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAChB,MAAA,IAAA,CAAK,gBAAA,CAAiB,KAAK,OAAO,CAAA;AAAA,IACpC;AAAA,EACF;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,gBAAA,EAAkB;AAC3C,MAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,IAClB;AACA,IAAA,IAAA,CAAK,mBAAmB,EAAC;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,YAAA,IAAe;AAC/C,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,CAAC,CAAA,EAAG;AAChB,MAAA,SAAA,CAAU,CAAC,EAAE,KAAA,EAAM;AACnB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,SAAA,CAAU,QAAA,GAAW,EAAA;AACrB,IAAA,SAAA,CAAU,KAAA,EAAM;AAAA,EAClB;AAAA;AAAA,EAGA,kBAAA,GAAoC;AAClC,IAAA,OAAO,KAAA,CAAM,KAAK,IAAA,CAAK,aAAA,GAAgB,gBAAA,CAA8B,SAAS,CAAC,CAAA,CAAE,MAAA;AAAA,MAC/E,CAAC,EAAA,KAAO,CAAC,EAAA,CAAG;AAAA,KACd;AAAA,EACF;AACF,CAAA;;;AC7OO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EAC9C,OAAO,OAAA,GAAU,CAAC,OAAA,EAAS,mBAAmB,MAAM,CAAA;AAAA;AAAA,EAQ3C,KAAA,GAAQ,IAAI,SAAA,CAAU,MAAM,KAAK,YAAA,EAAc;AAAA,IACtD,QAAA,EAAU,MAAM,IAAA,CAAK,KAAA,EAAM;AAAA,IAC3B,aAAA,EAAe,MAAO,IAAA,CAAK,gBAAA,GAAmB,KAAK,aAAA,GAAgB;AAAA,GACpE,CAAA;AAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,MAAA,GAAS,IAAA;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAE,YAAA,EAAc,OAAO,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,OAAA,EAAS;AAC3C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,CAAC,KAAK,OAAA,EAAS;AAC5C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,IAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA,EAGA,gBAAgB,KAAA,EAAyB;AACvC,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,YAAA,OAAmB,KAAA,EAAM;AAAA,EACrD;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,CAAC,IAAA,CAAK,YAAA,CAAa,MAAA;AAAA,EACpD;AACF","file":"dialog_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","import { EscapeLayer } from \"./escape_layer\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/**\n * Selector matching the elements considered focusable. Shared by the trap's Tab\n * cycling and by form-validation's invalid-focus delegation.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #onBeforeCache = (): void => {\n this.deactivate({ restoreFocus: false });\n };\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return Array.from(this.#getContainer().querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hidden,\n );\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":"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","file":"dialog_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/**\n * Selector matching the elements considered focusable. Shared by the trap's Tab\n * cycling and by form-validation's invalid-focus delegation.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return Array.from(this.#getContainer().querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hidden,\n );\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
|