stimeo-ui 0.2.1 → 0.3.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.js +10 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  7. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  8. package/dist/controllers/breadcrumb_controller.js +225 -13
  9. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  10. package/dist/controllers/calendar_controller.d.ts +11 -1
  11. package/dist/controllers/calendar_controller.js +89 -22
  12. package/dist/controllers/calendar_controller.js.map +1 -1
  13. package/dist/controllers/carousel_controller.d.ts +16 -1
  14. package/dist/controllers/carousel_controller.js +47 -6
  15. package/dist/controllers/carousel_controller.js.map +1 -1
  16. package/dist/controllers/collapsible_controller.js +2 -2
  17. package/dist/controllers/collapsible_controller.js.map +1 -1
  18. package/dist/controllers/color_picker_controller.d.ts +12 -1
  19. package/dist/controllers/color_picker_controller.js +46 -7
  20. package/dist/controllers/color_picker_controller.js.map +1 -1
  21. package/dist/controllers/combobox_controller.d.ts +14 -1
  22. package/dist/controllers/combobox_controller.js +162 -23
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +194 -17
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/confirm_controller.js.map +1 -1
  28. package/dist/controllers/context_menu_controller.d.ts +7 -6
  29. package/dist/controllers/context_menu_controller.js +32 -10
  30. package/dist/controllers/context_menu_controller.js.map +1 -1
  31. package/dist/controllers/count_up_controller.js.map +1 -1
  32. package/dist/controllers/data_grid_controller.d.ts +19 -1
  33. package/dist/controllers/data_grid_controller.js +82 -4
  34. package/dist/controllers/data_grid_controller.js.map +1 -1
  35. package/dist/controllers/date_range_picker_controller.d.ts +1 -0
  36. package/dist/controllers/date_range_picker_controller.js +27 -3
  37. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  38. package/dist/controllers/dialog_controller.js.map +1 -1
  39. package/dist/controllers/dismissible_controller.js.map +1 -1
  40. package/dist/controllers/drawer_controller.js.map +1 -1
  41. package/dist/controllers/dropdown_controller.d.ts +1 -2
  42. package/dist/controllers/dropdown_controller.js.map +1 -1
  43. package/dist/controllers/editable_controller.js +1 -0
  44. package/dist/controllers/editable_controller.js.map +1 -1
  45. package/dist/controllers/focus_controller.d.ts +1 -1
  46. package/dist/controllers/focus_controller.js.map +1 -1
  47. package/dist/controllers/form_field_controller.js.map +1 -1
  48. package/dist/controllers/form_validation_controller.js +1 -1
  49. package/dist/controllers/form_validation_controller.js.map +1 -1
  50. package/dist/controllers/highlight_controller.js.map +1 -1
  51. package/dist/controllers/hover_card_controller.d.ts +1 -1
  52. package/dist/controllers/hover_card_controller.js.map +1 -1
  53. package/dist/controllers/intersection_controller.js +36 -11
  54. package/dist/controllers/intersection_controller.js.map +1 -1
  55. package/dist/controllers/lazy_frame_controller.js +31 -10
  56. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  57. package/dist/controllers/listbox_controller.d.ts +9 -2
  58. package/dist/controllers/listbox_controller.js +257 -53
  59. package/dist/controllers/listbox_controller.js.map +1 -1
  60. package/dist/controllers/local_time_controller.js +2 -2
  61. package/dist/controllers/local_time_controller.js.map +1 -1
  62. package/dist/controllers/menu_controller.d.ts +29 -11
  63. package/dist/controllers/menu_controller.js +104 -17
  64. package/dist/controllers/menu_controller.js.map +1 -1
  65. package/dist/controllers/menubar_controller.d.ts +91 -11
  66. package/dist/controllers/menubar_controller.js +415 -63
  67. package/dist/controllers/menubar_controller.js.map +1 -1
  68. package/dist/controllers/meter_controller.js.map +1 -1
  69. package/dist/controllers/multi_select_controller.d.ts +31 -5
  70. package/dist/controllers/multi_select_controller.js +312 -29
  71. package/dist/controllers/multi_select_controller.js.map +1 -1
  72. package/dist/controllers/navigation_menu_controller.d.ts +65 -18
  73. package/dist/controllers/navigation_menu_controller.js +154 -27
  74. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  75. package/dist/controllers/nested_form_controller.d.ts +4 -5
  76. package/dist/controllers/nested_form_controller.js.map +1 -1
  77. package/dist/controllers/number_input_controller.d.ts +1 -1
  78. package/dist/controllers/number_input_controller.js +7 -0
  79. package/dist/controllers/number_input_controller.js.map +1 -1
  80. package/dist/controllers/otp_controller.js +18 -1
  81. package/dist/controllers/otp_controller.js.map +1 -1
  82. package/dist/controllers/overflow_indicator_controller.d.ts +3 -2
  83. package/dist/controllers/overflow_indicator_controller.js +81 -13
  84. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  85. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  86. package/dist/controllers/overflow_menu_controller.js +381 -57
  87. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  88. package/dist/controllers/pagination_controller.d.ts +36 -5
  89. package/dist/controllers/pagination_controller.js +163 -32
  90. package/dist/controllers/pagination_controller.js.map +1 -1
  91. package/dist/controllers/persist_controller.js +6 -6
  92. package/dist/controllers/persist_controller.js.map +1 -1
  93. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  94. package/dist/controllers/pointer_drag_controller.js +9 -1
  95. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  96. package/dist/controllers/popover_controller.d.ts +1 -2
  97. package/dist/controllers/popover_controller.js +2 -2
  98. package/dist/controllers/popover_controller.js.map +1 -1
  99. package/dist/controllers/portal_controller.d.ts +4 -4
  100. package/dist/controllers/portal_controller.js.map +1 -1
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.js +22 -3
  103. package/dist/controllers/radio_group_controller.js.map +1 -1
  104. package/dist/controllers/range_slider_controller.d.ts +14 -4
  105. package/dist/controllers/range_slider_controller.js +32 -6
  106. package/dist/controllers/range_slider_controller.js.map +1 -1
  107. package/dist/controllers/rating_controller.js +16 -2
  108. package/dist/controllers/rating_controller.js.map +1 -1
  109. package/dist/controllers/read_more_controller.js +63 -19
  110. package/dist/controllers/read_more_controller.js.map +1 -1
  111. package/dist/controllers/reading_progress_controller.js.map +1 -1
  112. package/dist/controllers/resizable_controller.d.ts +3 -1
  113. package/dist/controllers/resizable_controller.js +65 -1
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.js +17 -2
  116. package/dist/controllers/roving_controller.js.map +1 -1
  117. package/dist/controllers/scroll_area_controller.js +86 -12
  118. package/dist/controllers/scroll_area_controller.js.map +1 -1
  119. package/dist/controllers/scroll_restore_controller.js +1 -1
  120. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  122. package/dist/controllers/scroll_visibility_controller.js +33 -3
  123. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  124. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  125. package/dist/controllers/scrollspy_controller.js +346 -73
  126. package/dist/controllers/scrollspy_controller.js.map +1 -1
  127. package/dist/controllers/separator_controller.d.ts +20 -5
  128. package/dist/controllers/separator_controller.js +9 -0
  129. package/dist/controllers/separator_controller.js.map +1 -1
  130. package/dist/controllers/sidebar_controller.d.ts +1 -1
  131. package/dist/controllers/sidebar_controller.js.map +1 -1
  132. package/dist/controllers/skeleton_controller.js +1 -1
  133. package/dist/controllers/skeleton_controller.js.map +1 -1
  134. package/dist/controllers/slider_controller.d.ts +11 -1
  135. package/dist/controllers/slider_controller.js +32 -6
  136. package/dist/controllers/slider_controller.js.map +1 -1
  137. package/dist/controllers/sortable_controller.d.ts +3 -1
  138. package/dist/controllers/sortable_controller.js +34 -3
  139. package/dist/controllers/sortable_controller.js.map +1 -1
  140. package/dist/controllers/spinner_controller.js +1 -1
  141. package/dist/controllers/spinner_controller.js.map +1 -1
  142. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  143. package/dist/controllers/stick_to_bottom_controller.js +2 -1
  144. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  145. package/dist/controllers/sticky_observer_controller.js +32 -11
  146. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  147. package/dist/controllers/submit_once_controller.d.ts +2 -2
  148. package/dist/controllers/submit_once_controller.js.map +1 -1
  149. package/dist/controllers/switch_controller.js +1 -0
  150. package/dist/controllers/switch_controller.js.map +1 -1
  151. package/dist/controllers/tabs_controller.d.ts +11 -3
  152. package/dist/controllers/tabs_controller.js +26 -3
  153. package/dist/controllers/tabs_controller.js.map +1 -1
  154. package/dist/controllers/tags_input_controller.js +22 -2
  155. package/dist/controllers/tags_input_controller.js.map +1 -1
  156. package/dist/controllers/theme_controller.d.ts +1 -1
  157. package/dist/controllers/theme_controller.js +22 -3
  158. package/dist/controllers/theme_controller.js.map +1 -1
  159. package/dist/controllers/time_picker_controller.js +20 -1
  160. package/dist/controllers/time_picker_controller.js.map +1 -1
  161. package/dist/controllers/toast_controller.d.ts +4 -5
  162. package/dist/controllers/toast_controller.js +4 -5
  163. package/dist/controllers/toast_controller.js.map +1 -1
  164. package/dist/controllers/toggle_group_controller.js +23 -2
  165. package/dist/controllers/toggle_group_controller.js.map +1 -1
  166. package/dist/controllers/toolbar_controller.d.ts +53 -13
  167. package/dist/controllers/toolbar_controller.js +230 -31
  168. package/dist/controllers/toolbar_controller.js.map +1 -1
  169. package/dist/controllers/tooltip_controller.d.ts +1 -1
  170. package/dist/controllers/tooltip_controller.js.map +1 -1
  171. package/dist/controllers/transition_controller.js.map +1 -1
  172. package/dist/controllers/tree_view_controller.d.ts +87 -6
  173. package/dist/controllers/tree_view_controller.js +467 -51
  174. package/dist/controllers/tree_view_controller.js.map +1 -1
  175. package/dist/index.js +3514 -689
  176. package/dist/index.js.map +1 -1
  177. package/dist/inspector/cli.d.ts +375 -39
  178. package/dist/inspector/cli.js +228 -14
  179. package/dist/inspector/cli.js.map +1 -1
  180. package/dist/inspector/cli_bin.js +419 -48
  181. package/dist/inspector/cli_bin.js.map +1 -1
  182. package/dist/inspector/examples.json +24 -24
  183. package/dist/inspector/manifest.json +1040 -125
  184. package/package.json +5 -6
@@ -2,6 +2,22 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/date_range_picker_controller.ts
4
4
 
5
+ // src/utils/logical_scroll.ts
6
+ function isRtl(element) {
7
+ return window.getComputedStyle(element).direction === "rtl";
8
+ }
9
+
10
+ // src/utils/arrow_step.ts
11
+ function logicalArrowKey(key, element) {
12
+ if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
13
+ if (!isRtl(element)) return key;
14
+ return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
15
+ }
16
+ function isReservedArrowChord(event, allow = []) {
17
+ if (!event.key.startsWith("Arrow")) return false;
18
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
+ }
20
+
5
21
  // src/utils/dates.ts
6
22
  function toISODateString(date) {
7
23
  const year = date.getFullYear();
@@ -119,6 +135,7 @@ var DateRangePickerController = class extends Controller {
119
135
  const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
120
136
  this.#focusedDate = anchor;
121
137
  this.#viewMonth = toISOMonthString(anchor);
138
+ if (this.hasGridTarget) this.gridTarget.setAttribute("aria-multiselectable", "true");
122
139
  this.#render();
123
140
  }
124
141
  /** Cancels any pending deferred focus so it never fires on a detached element. */
@@ -184,6 +201,8 @@ var DateRangePickerController = class extends Controller {
184
201
  }
185
202
  /** Grid keyboard navigation, selection (Enter/Space), and Escape-to-cancel. */
186
203
  onKeydown(event) {
204
+ if (event.defaultPrevented) return;
205
+ if (isReservedArrowChord(event)) return;
187
206
  const cell = this.#cellFrom(event.target);
188
207
  if (!cell) return;
189
208
  const dateStr = cell.getAttribute("data-date");
@@ -195,7 +214,7 @@ var DateRangePickerController = class extends Controller {
195
214
  return;
196
215
  }
197
216
  if (event.key === "Escape") {
198
- if (this.#pendingStart && !event.defaultPrevented && !event.isComposing) {
217
+ if (this.#pendingStart && !event.isComposing) {
199
218
  event.preventDefault();
200
219
  this.#pendingStart = "";
201
220
  this.#previewDate = "";
@@ -204,7 +223,7 @@ var DateRangePickerController = class extends Controller {
204
223
  return;
205
224
  }
206
225
  let next = null;
207
- switch (event.key) {
226
+ switch (logicalArrowKey(event.key, this.element)) {
208
227
  case "ArrowLeft":
209
228
  next = addDays(date, -1);
210
229
  break;
@@ -318,7 +337,12 @@ var DateRangePickerController = class extends Controller {
318
337
  el.toggleAttribute("data-range-start", isStart);
319
338
  el.toggleAttribute("data-range-end", isEnd);
320
339
  el.toggleAttribute("data-in-range", inside);
321
- el.setAttribute("aria-selected", String(isStart || isEnd));
340
+ el.setAttribute(
341
+ "aria-selected",
342
+ String(
343
+ !!this.#startDate && iso === this.#startDate || !!this.#endDate && iso === this.#endDate
344
+ )
345
+ );
322
346
  }
323
347
  }
324
348
  /** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../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":["/**\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 {\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 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 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 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 // Layered-Escape rule 1: leave a press an inner handler already owned;\n // a press during IME composition steers the conversion, not the range.\n if (this.#pendingStart && !event.defaultPrevented && !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 switch (event.key) {\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 // AT hears only the two confirmed/pending endpoints as \"selected\".\n el.setAttribute(\"aria-selected\", String(isStart || isEnd));\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/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 +1 @@
1
- {"version":3,"sources":["../../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 * 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-first half of the\n * shared layered-Escape contract. A keydown that is part of an IME composition\n * (`event.isComposing`) cancels the composition, never a layer, and is ignored\n * here for every layer 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 (the shared layered-Escape contract).\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/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 * 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 +1 @@
1
- {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/dismissible_controller.ts"],"names":[],"mappings":";;;;;AAiCO,IAAM,SAAA,GACX,2IAAA;;;ACNK,IAAM,qBAAA,GAAN,cAAoC,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,MAAA,EAAQ,UAAU,CAAA;AAAA,EAC7C,OAAgB,MAAA,GAAS;AAAA,IACvB,IAAA,EAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,QAAA,EAAS;AAAA,IACxC,aAAA,EAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA;AAAM,GACjD;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,SAAS,CAAA;AAAA,EAC3B,OAAO,MAAA,GAAS,CAAC,SAAS,CAAA;AAAA,EAUjB,OAAA,GAAgB;AACvB,IAAA,MAAM,OAAO,IAAA,CAAK,KAAA;AAClB,IAAA,IAAI,CAAC,IAAA,CAAK,YAAA,CAAa,YAAY,CAAA,EAAG;AACpC,MAAA,IAAA,CAAK,YAAA,CAAa,cAAc,MAAM,CAAA;AAAA,IACxC;AACA,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,yBAAA,GAAkC;AAChC,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,eAAA,EAAgB;AAAA,EACvB;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AAGpD,IAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,IAAA,IAAI,CAAC,KAAK,kBAAA,EAAoB;AAC9B,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,KAAK,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG;AAC/C,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,eAAA,EAAgB;AAAA,EACvB,CAAA;AAAA;AAAA,EAGA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAC3D,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAAA,IAC1D;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,KAAA,GAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,aAAA,GAAgB,IAAA,CAAK,UAAA,GAAa,IAAA,CAAK,OAAA;AAAA,EACrD;AAAA,EAEA,eAAA,GAAwB;AACtB,IAAA,MAAM,OAAO,IAAA,CAAK,KAAA;AAClB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,KAAc,MAAA,GAAS,MAAA,GAAS,QAAA;AAElD,IAAA,IAAA,CAAK,cAAc,IAAI,CAAA;AACvB,IAAA,IAAA,CAAK,YAAA,CAAa,cAAc,SAAS,CAAA;AAGzC,IAAA,IAAA,CAAK,SAAS,SAAA,EAAW,EAAE,QAAQ,EAAE,IAAA,IAAQ,CAAA;AAE7C,IAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AAAA,IAChB,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,MAAA,EAAO;AAAA,IACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,IAAA,EAAyB;AACrC,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAI,EAAE,MAAA,YAAkB,WAAA,CAAA,IAAgB,CAAC,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA,EAAG;AAEhE,IAAA,KAAA,MAAW,SAAA,IAAa,IAAA,CAAK,eAAA,CAAgB,IAAI,CAAA,EAAG;AAClD,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA,IAAI,QAAA,CAAS,kBAAkB,SAAA,EAAW;AAAA,IAC5C;AACA,IAAA,QAAA,CAAS,KAAK,KAAA,EAAM;AAAA,EACtB;AAAA;AAAA,EAGA,CAAC,gBAAgB,IAAA,EAA2C;AAC1D,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,iBAAA,GAAoB,IAAA,CAAK,cAAA,GAAiB,IAAA;AACxE,IAAA,IACE,gBAAA,IACA,CAAC,IAAA,CAAK,QAAA,CAAS,gBAAgB,CAAA,IAC/B,IAAA,CAAK,iBAAA,CAAkB,gBAAgB,CAAA,EACvC;AACA,MAAA,MAAM,gBAAA;AAAA,IACR;AAEA,IAAA,MAAM,UAAA,GAAa,QAAA,CAAS,gBAAA,CAA8B,SAAS,CAAA;AACnE,IAAA,KAAA,MAAW,WAAW,UAAA,EAAY;AAChC,MAAA,IACE,OAAA,KAAY,gBAAA,IACZ,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA,IACrB,EAAE,IAAA,CAAK,uBAAA,CAAwB,OAAO,CAAA,GAAI,IAAA,CAAK,2BAAA,CAAA,EAC/C;AACA,QAAA;AAAA,MACF;AACA,MAAA,IAAI,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAA,EAAG,MAAM,OAAA;AAAA,IAC7C;AAEA,IAAA,KAAA,IAAS,QAAQ,UAAA,CAAW,MAAA,GAAS,GAAG,KAAA,IAAS,CAAA,EAAG,SAAS,CAAA,EAAG;AAC9D,MAAA,MAAM,OAAA,GAAU,UAAA,CAAW,IAAA,CAAK,KAAK,CAAA;AACrC,MAAA,IACE,OAAA,KAAY,gBAAA,IACZ,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA,IACrB,EAAE,IAAA,CAAK,uBAAA,CAAwB,OAAO,CAAA,GAAI,IAAA,CAAK,2BAAA,CAAA,EAC/C;AACA,QAAA;AAAA,MACF;AACA,MAAA,IAAI,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAA,EAAG,MAAM,OAAA;AAAA,IAC7C;AAAA,EACF;AAAA;AAAA,EAGA,kBAAkB,OAAA,EAA+B;AAC/C,IAAA,IACE,CAAC,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAA,IAC1B,CAAC,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA,IAChC,CAAC,OAAA,CAAQ,iBAAA,EACT;AACA,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,WAAW,CAAA,EAAG,OAAO,KAAA;AACzC,IAAA,IAAI,OAAA,YAAmB,gBAAA,IAAoB,OAAA,CAAQ,IAAA,KAAS,UAAU,OAAO,KAAA;AAE7E,IAAA,KAAA,IAAS,OAAA,GAA8B,OAAA,EAAS,OAAA,EAAS,OAAA,GAAU,QAAQ,aAAA,EAAe;AACxF,MAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,KAAA,EAAO,OAAO,KAAA;AAAA,IAC9C;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"dismissible_controller.js","sourcesContent":["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 (the shared layered-Escape contract).\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 { FOCUSABLE } from \"../utils/focus_trap\";\n\n/**\n * Headless \"dismissible\" behavior for banners, notices, and inline alerts.\n *\n * Markup contract (identifier: `stimeo--dismissible`):\n * <div data-controller=\"stimeo--dismissible\"\n * data-stimeo--dismissible-mode-value=\"remove\">\n * <div data-stimeo--dismissible-target=\"root\" role=\"status\">\n * <p>Saved.</p>\n * <button type=\"button\" aria-label=\"Close\"\n * data-action=\"stimeo--dismissible#dismiss\">×</button>\n * </div>\n * </div>\n *\n * A general utility with no dedicated APG pattern. Its accessibility job is to\n * keep focus from being orphaned: if focus is inside the element being removed,\n * it is moved to a safe place first so the close button vanishing never strands\n * the user (WCAG 2.4.3).\n *\n * @remarks\n * Behavior only — the consumer owns any exit transition (use `hide` mode, which\n * adds `hidden`, and animate off `data-state`) and any semantics like\n * `role=\"alert\"`. Focus retreats to, in order: the `fallback` target → the next\n * focusable element after the root → the previous one → `document.body` as a\n * last resort (weak focus; prefer providing a `fallback`).\n */\nexport class DismissibleController extends Controller<HTMLElement> {\n static override targets = [\"root\", \"fallback\"];\n static override values = {\n mode: { type: String, default: \"remove\" },\n closeOnEscape: { type: Boolean, default: false },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"dismiss\"] as const;\n\n declare readonly rootTarget: HTMLElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasRootTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n\n declare modeValue: string;\n declare closeOnEscapeValue: boolean;\n\n override connect(): void {\n const root = this.#root;\n if (!root.hasAttribute(\"data-state\")) {\n root.setAttribute(\"data-state\", \"open\");\n }\n this.#syncEscapeListener();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */\n closeOnEscapeValueChanged(): void {\n this.#syncEscapeListener();\n }\n\n /** Dismisses the element. Bound via `data-action` (click on the close button). */\n dismiss(): void {\n this.#performDismiss();\n }\n\n /** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A press during IME composition cancels the conversion (e.g. in a text\n // field inside this element), never the element itself.\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n if (!this.closeOnEscapeValue) return;\n const active = document.activeElement;\n if (!active || !this.element.contains(active)) return;\n event.preventDefault();\n this.#performDismiss();\n };\n\n /** Adds or removes the one stable listener reference without duplicating it. */\n #syncEscapeListener(): void {\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n if (this.closeOnEscapeValue) {\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n }\n }\n\n /** The element to dismiss: the explicit `root` target, or the host element. */\n get #root(): HTMLElement {\n return this.hasRootTarget ? this.rootTarget : this.element;\n }\n\n #performDismiss(): void {\n const root = this.#root;\n const mode = this.modeValue === \"hide\" ? \"hide\" : \"remove\";\n\n this.#retreatFocus(root);\n root.setAttribute(\"data-state\", \"closing\");\n // Dispatch before removal so a listener on the (about-to-leave) element still\n // runs; `dispatch` fires synchronously on `this.element`.\n this.dispatch(\"dismiss\", { detail: { mode } });\n\n if (mode === \"hide\") {\n root.hidden = true;\n } else {\n root.remove();\n }\n }\n\n /**\n * Moves focus out of `root` *before* it is removed, but only when focus is\n * actually inside it — otherwise the user's place elsewhere is left undisturbed.\n */\n #retreatFocus(root: HTMLElement): void {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !root.contains(active)) return;\n\n for (const candidate of this.#focusFallbacks(root)) {\n candidate.focus();\n if (document.activeElement === candidate) return;\n }\n document.body.focus();\n }\n\n /** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */\n *#focusFallbacks(root: HTMLElement): Generator<HTMLElement> {\n const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;\n if (\n explicitFallback &&\n !root.contains(explicitFallback) &&\n this.#isFocusAvailable(explicitFallback)\n ) {\n yield explicitFallback;\n }\n\n const candidates = document.querySelectorAll<HTMLElement>(FOCUSABLE);\n for (const element of candidates) {\n if (\n element === explicitFallback ||\n root.contains(element) ||\n !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)\n ) {\n continue;\n }\n if (this.#isFocusAvailable(element)) yield element;\n }\n\n for (let index = candidates.length - 1; index >= 0; index -= 1) {\n const element = candidates.item(index);\n if (\n element === explicitFallback ||\n root.contains(element) ||\n !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)\n ) {\n continue;\n }\n if (this.#isFocusAvailable(element)) yield element;\n }\n }\n\n /** Whether focus is allowed by the element and its semantic ancestors. */\n #isFocusAvailable(element: HTMLElement): boolean {\n if (\n !element.matches(FOCUSABLE) &&\n !element.hasAttribute(\"tabindex\") &&\n !element.isContentEditable\n ) {\n return false;\n }\n if (element.matches(\":disabled\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n if (current.hidden || current.inert) return false;\n }\n return true;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/dismissible_controller.ts"],"names":[],"mappings":"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","file":"dismissible_controller.js","sourcesContent":["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 { FOCUSABLE } from \"../utils/focus_trap\";\n\n/**\n * Headless \"dismissible\" behavior for banners, notices, and inline alerts.\n *\n * Markup contract (identifier: `stimeo--dismissible`):\n * <div data-controller=\"stimeo--dismissible\"\n * data-stimeo--dismissible-mode-value=\"remove\">\n * <div data-stimeo--dismissible-target=\"root\" role=\"status\">\n * <p>Saved.</p>\n * <button type=\"button\" aria-label=\"Close\"\n * data-action=\"stimeo--dismissible#dismiss\">×</button>\n * </div>\n * </div>\n *\n * A general utility with no dedicated APG pattern. Its accessibility job is to\n * keep focus from being orphaned: if focus is inside the element being removed,\n * it is moved to a safe place first so the close button vanishing never strands\n * the user (WCAG 2.4.3).\n *\n * @remarks\n * Behavior only — the consumer owns any exit transition (use `hide` mode, which\n * adds `hidden`, and animate off `data-state`) and any semantics like\n * `role=\"alert\"`. Focus retreats to, in order: the `fallback` target → the next\n * focusable element after the root → the previous one → `document.body` as a\n * last resort (weak focus; prefer providing a `fallback`).\n */\nexport class DismissibleController extends Controller<HTMLElement> {\n static override targets = [\"root\", \"fallback\"];\n static override values = {\n mode: { type: String, default: \"remove\" },\n closeOnEscape: { type: Boolean, default: false },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"dismiss\"] as const;\n\n declare readonly rootTarget: HTMLElement;\n declare readonly fallbackTarget: HTMLElement;\n declare readonly hasRootTarget: boolean;\n declare readonly hasFallbackTarget: boolean;\n\n declare modeValue: string;\n declare closeOnEscapeValue: boolean;\n\n override connect(): void {\n const root = this.#root;\n if (!root.hasAttribute(\"data-state\")) {\n root.setAttribute(\"data-state\", \"open\");\n }\n this.#syncEscapeListener();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */\n closeOnEscapeValueChanged(): void {\n this.#syncEscapeListener();\n }\n\n /** Dismisses the element. Bound via `data-action` (click on the close button). */\n dismiss(): void {\n this.#performDismiss();\n }\n\n /** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A press during IME composition cancels the conversion (e.g. in a text\n // field inside this element), never the element itself.\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n if (!this.closeOnEscapeValue) return;\n const active = document.activeElement;\n if (!active || !this.element.contains(active)) return;\n event.preventDefault();\n this.#performDismiss();\n };\n\n /** Adds or removes the one stable listener reference without duplicating it. */\n #syncEscapeListener(): void {\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n if (this.closeOnEscapeValue) {\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n }\n }\n\n /** The element to dismiss: the explicit `root` target, or the host element. */\n get #root(): HTMLElement {\n return this.hasRootTarget ? this.rootTarget : this.element;\n }\n\n #performDismiss(): void {\n const root = this.#root;\n const mode = this.modeValue === \"hide\" ? \"hide\" : \"remove\";\n\n this.#retreatFocus(root);\n root.setAttribute(\"data-state\", \"closing\");\n // Dispatch before removal so a listener on the (about-to-leave) element still\n // runs; `dispatch` fires synchronously on `this.element`.\n this.dispatch(\"dismiss\", { detail: { mode } });\n\n if (mode === \"hide\") {\n root.hidden = true;\n } else {\n root.remove();\n }\n }\n\n /**\n * Moves focus out of `root` *before* it is removed, but only when focus is\n * actually inside it — otherwise the user's place elsewhere is left undisturbed.\n */\n #retreatFocus(root: HTMLElement): void {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !root.contains(active)) return;\n\n for (const candidate of this.#focusFallbacks(root)) {\n candidate.focus();\n if (document.activeElement === candidate) return;\n }\n document.body.focus();\n }\n\n /** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */\n *#focusFallbacks(root: HTMLElement): Generator<HTMLElement> {\n const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;\n if (\n explicitFallback &&\n !root.contains(explicitFallback) &&\n this.#isFocusAvailable(explicitFallback)\n ) {\n yield explicitFallback;\n }\n\n const candidates = document.querySelectorAll<HTMLElement>(FOCUSABLE);\n for (const element of candidates) {\n if (\n element === explicitFallback ||\n root.contains(element) ||\n !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)\n ) {\n continue;\n }\n if (this.#isFocusAvailable(element)) yield element;\n }\n\n for (let index = candidates.length - 1; index >= 0; index -= 1) {\n const element = candidates.item(index);\n if (\n element === explicitFallback ||\n root.contains(element) ||\n !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)\n ) {\n continue;\n }\n if (this.#isFocusAvailable(element)) yield element;\n }\n }\n\n /** Whether focus is allowed by the element and its semantic ancestors. */\n #isFocusAvailable(element: HTMLElement): boolean {\n if (\n !element.matches(FOCUSABLE) &&\n !element.hasAttribute(\"tabindex\") &&\n !element.isContentEditable\n ) {\n return false;\n }\n if (element.matches(\":disabled\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n\n for (let current: HTMLElement | null = element; current; current = current.parentElement) {\n if (current.hidden || current.inert) return false;\n }\n return true;\n }\n}\n"]}