stimeo-ui 0.15.0 → 0.16.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 (267) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +63 -15
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +8 -1
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +43 -2
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +9 -8
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +85 -23
  24. package/dist/controllers/calendar_controller.js +323 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +54 -10
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +56 -11
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/countdown_controller.d.ts +30 -1
  57. package/dist/controllers/countdown_controller.js +129 -26
  58. package/dist/controllers/countdown_controller.js.map +1 -1
  59. package/dist/controllers/currency_input_controller.d.ts +74 -15
  60. package/dist/controllers/currency_input_controller.js +205 -58
  61. package/dist/controllers/currency_input_controller.js.map +1 -1
  62. package/dist/controllers/data_grid_controller.d.ts +63 -18
  63. package/dist/controllers/data_grid_controller.js +195 -29
  64. package/dist/controllers/data_grid_controller.js.map +1 -1
  65. package/dist/controllers/date_range_picker_controller.d.ts +34 -6
  66. package/dist/controllers/date_range_picker_controller.js +139 -27
  67. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  68. package/dist/controllers/dialog_controller.d.ts +10 -3
  69. package/dist/controllers/dialog_controller.js +35 -8
  70. package/dist/controllers/dialog_controller.js.map +1 -1
  71. package/dist/controllers/direct_upload_controller.js +22 -4
  72. package/dist/controllers/direct_upload_controller.js.map +1 -1
  73. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  74. package/dist/controllers/dirty_form_controller.js +14 -1
  75. package/dist/controllers/dirty_form_controller.js.map +1 -1
  76. package/dist/controllers/dismissible_controller.js +1 -0
  77. package/dist/controllers/dismissible_controller.js.map +1 -1
  78. package/dist/controllers/drawer_controller.d.ts +18 -10
  79. package/dist/controllers/drawer_controller.js +54 -19
  80. package/dist/controllers/drawer_controller.js.map +1 -1
  81. package/dist/controllers/dropdown_controller.d.ts +9 -3
  82. package/dist/controllers/dropdown_controller.js +36 -9
  83. package/dist/controllers/dropdown_controller.js.map +1 -1
  84. package/dist/controllers/editable_controller.js +34 -0
  85. package/dist/controllers/editable_controller.js.map +1 -1
  86. package/dist/controllers/file_dropzone_controller.js +144 -51
  87. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  88. package/dist/controllers/filter_controller.d.ts +10 -4
  89. package/dist/controllers/filter_controller.js +20 -6
  90. package/dist/controllers/filter_controller.js.map +1 -1
  91. package/dist/controllers/flash_controller.d.ts +26 -6
  92. package/dist/controllers/flash_controller.js +305 -46
  93. package/dist/controllers/flash_controller.js.map +1 -1
  94. package/dist/controllers/focus_controller.js +1 -0
  95. package/dist/controllers/focus_controller.js.map +1 -1
  96. package/dist/controllers/form_field_controller.js +7 -5
  97. package/dist/controllers/form_field_controller.js.map +1 -1
  98. package/dist/controllers/form_validation_controller.js +19 -13
  99. package/dist/controllers/form_validation_controller.js.map +1 -1
  100. package/dist/controllers/frame_loading_controller.js +16 -2
  101. package/dist/controllers/frame_loading_controller.js.map +1 -1
  102. package/dist/controllers/highlight_controller.js +19 -2
  103. package/dist/controllers/highlight_controller.js.map +1 -1
  104. package/dist/controllers/hover_card_controller.d.ts +10 -2
  105. package/dist/controllers/hover_card_controller.js +40 -14
  106. package/dist/controllers/hover_card_controller.js.map +1 -1
  107. package/dist/controllers/idle_controller.d.ts +16 -3
  108. package/dist/controllers/idle_controller.js +90 -5
  109. package/dist/controllers/idle_controller.js.map +1 -1
  110. package/dist/controllers/input_mask_controller.d.ts +5 -2
  111. package/dist/controllers/input_mask_controller.js +65 -9
  112. package/dist/controllers/input_mask_controller.js.map +1 -1
  113. package/dist/controllers/intersection_controller.js +3 -0
  114. package/dist/controllers/intersection_controller.js.map +1 -1
  115. package/dist/controllers/lazy_frame_controller.js +11 -2
  116. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  117. package/dist/controllers/listbox_controller.d.ts +50 -8
  118. package/dist/controllers/listbox_controller.js +203 -45
  119. package/dist/controllers/listbox_controller.js.map +1 -1
  120. package/dist/controllers/masonry_controller.js +0 -2
  121. package/dist/controllers/masonry_controller.js.map +1 -1
  122. package/dist/controllers/menu_controller.d.ts +9 -3
  123. package/dist/controllers/menu_controller.js +45 -16
  124. package/dist/controllers/menu_controller.js.map +1 -1
  125. package/dist/controllers/menubar_controller.d.ts +11 -0
  126. package/dist/controllers/menubar_controller.js +58 -24
  127. package/dist/controllers/menubar_controller.js.map +1 -1
  128. package/dist/controllers/meter_controller.js +9 -5
  129. package/dist/controllers/meter_controller.js.map +1 -1
  130. package/dist/controllers/multi_select_controller.d.ts +18 -3
  131. package/dist/controllers/multi_select_controller.js +278 -104
  132. package/dist/controllers/multi_select_controller.js.map +1 -1
  133. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  134. package/dist/controllers/navigation_menu_controller.js +48 -15
  135. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  136. package/dist/controllers/nested_form_controller.js +37 -8
  137. package/dist/controllers/nested_form_controller.js.map +1 -1
  138. package/dist/controllers/network_status_controller.js +9 -1
  139. package/dist/controllers/network_status_controller.js.map +1 -1
  140. package/dist/controllers/number_input_controller.d.ts +36 -8
  141. package/dist/controllers/number_input_controller.js +124 -21
  142. package/dist/controllers/number_input_controller.js.map +1 -1
  143. package/dist/controllers/optimistic_controller.js +14 -1
  144. package/dist/controllers/optimistic_controller.js.map +1 -1
  145. package/dist/controllers/otp_controller.d.ts +22 -7
  146. package/dist/controllers/otp_controller.js +198 -55
  147. package/dist/controllers/otp_controller.js.map +1 -1
  148. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  149. package/dist/controllers/overflow_indicator_controller.js +84 -13
  150. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  151. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  152. package/dist/controllers/overflow_menu_controller.js +112 -41
  153. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  154. package/dist/controllers/pagination_controller.d.ts +19 -11
  155. package/dist/controllers/pagination_controller.js +74 -28
  156. package/dist/controllers/pagination_controller.js.map +1 -1
  157. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  158. package/dist/controllers/password_reveal_controller.js +59 -2
  159. package/dist/controllers/password_reveal_controller.js.map +1 -1
  160. package/dist/controllers/persist_controller.js +30 -8
  161. package/dist/controllers/persist_controller.js.map +1 -1
  162. package/dist/controllers/pointer_drag_controller.js +131 -52
  163. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  164. package/dist/controllers/popover_controller.d.ts +9 -3
  165. package/dist/controllers/popover_controller.js +45 -11
  166. package/dist/controllers/popover_controller.js.map +1 -1
  167. package/dist/controllers/portal_controller.d.ts +1 -1
  168. package/dist/controllers/portal_controller.js +6 -2
  169. package/dist/controllers/portal_controller.js.map +1 -1
  170. package/dist/controllers/preview_guard_controller.js +16 -1
  171. package/dist/controllers/preview_guard_controller.js.map +1 -1
  172. package/dist/controllers/progress_controller.js +8 -4
  173. package/dist/controllers/progress_controller.js.map +1 -1
  174. package/dist/controllers/radio_group_controller.d.ts +6 -4
  175. package/dist/controllers/radio_group_controller.js +42 -17
  176. package/dist/controllers/radio_group_controller.js.map +1 -1
  177. package/dist/controllers/range_slider_controller.d.ts +49 -1
  178. package/dist/controllers/range_slider_controller.js +88 -42
  179. package/dist/controllers/range_slider_controller.js.map +1 -1
  180. package/dist/controllers/rating_controller.d.ts +14 -3
  181. package/dist/controllers/rating_controller.js +39 -15
  182. package/dist/controllers/rating_controller.js.map +1 -1
  183. package/dist/controllers/read_more_controller.d.ts +24 -2
  184. package/dist/controllers/read_more_controller.js +69 -2
  185. package/dist/controllers/read_more_controller.js.map +1 -1
  186. package/dist/controllers/resizable_controller.d.ts +16 -2
  187. package/dist/controllers/resizable_controller.js +82 -22
  188. package/dist/controllers/resizable_controller.js.map +1 -1
  189. package/dist/controllers/scroll_area_controller.js +5 -1
  190. package/dist/controllers/scroll_area_controller.js.map +1 -1
  191. package/dist/controllers/scroll_visibility_controller.js +0 -1
  192. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  193. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  194. package/dist/controllers/scrollspy_controller.js +6 -0
  195. package/dist/controllers/scrollspy_controller.js.map +1 -1
  196. package/dist/controllers/separator_controller.d.ts +41 -14
  197. package/dist/controllers/separator_controller.js +66 -37
  198. package/dist/controllers/separator_controller.js.map +1 -1
  199. package/dist/controllers/sidebar_controller.d.ts +20 -3
  200. package/dist/controllers/sidebar_controller.js +77 -18
  201. package/dist/controllers/sidebar_controller.js.map +1 -1
  202. package/dist/controllers/skeleton_controller.js +6 -1
  203. package/dist/controllers/skeleton_controller.js.map +1 -1
  204. package/dist/controllers/slider_controller.d.ts +45 -7
  205. package/dist/controllers/slider_controller.js +82 -47
  206. package/dist/controllers/slider_controller.js.map +1 -1
  207. package/dist/controllers/smart_sticky_header_controller.js +11 -1
  208. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  209. package/dist/controllers/sortable_controller.js +17 -2
  210. package/dist/controllers/sortable_controller.js.map +1 -1
  211. package/dist/controllers/spinner_controller.js +10 -2
  212. package/dist/controllers/spinner_controller.js.map +1 -1
  213. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  214. package/dist/controllers/step_indicator_controller.js +18 -17
  215. package/dist/controllers/step_indicator_controller.js.map +1 -1
  216. package/dist/controllers/stepper_controller.d.ts +34 -9
  217. package/dist/controllers/stepper_controller.js +101 -19
  218. package/dist/controllers/stepper_controller.js.map +1 -1
  219. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  220. package/dist/controllers/stick_to_bottom_controller.js +75 -5
  221. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  222. package/dist/controllers/submit_once_controller.d.ts +3 -2
  223. package/dist/controllers/submit_once_controller.js +16 -3
  224. package/dist/controllers/submit_once_controller.js.map +1 -1
  225. package/dist/controllers/switch_controller.d.ts +29 -7
  226. package/dist/controllers/switch_controller.js +101 -10
  227. package/dist/controllers/switch_controller.js.map +1 -1
  228. package/dist/controllers/tabs_controller.d.ts +12 -0
  229. package/dist/controllers/tabs_controller.js +21 -2
  230. package/dist/controllers/tabs_controller.js.map +1 -1
  231. package/dist/controllers/tags_input_controller.d.ts +15 -3
  232. package/dist/controllers/tags_input_controller.js +209 -59
  233. package/dist/controllers/tags_input_controller.js.map +1 -1
  234. package/dist/controllers/theme_controller.d.ts +20 -3
  235. package/dist/controllers/theme_controller.js +64 -14
  236. package/dist/controllers/theme_controller.js.map +1 -1
  237. package/dist/controllers/time_picker_controller.js +23 -8
  238. package/dist/controllers/time_picker_controller.js.map +1 -1
  239. package/dist/controllers/toast_controller.d.ts +52 -15
  240. package/dist/controllers/toast_controller.js +337 -54
  241. package/dist/controllers/toast_controller.js.map +1 -1
  242. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  243. package/dist/controllers/toggle_group_controller.js +159 -23
  244. package/dist/controllers/toggle_group_controller.js.map +1 -1
  245. package/dist/controllers/toolbar_controller.js +32 -0
  246. package/dist/controllers/toolbar_controller.js.map +1 -1
  247. package/dist/controllers/tooltip_controller.d.ts +8 -0
  248. package/dist/controllers/tooltip_controller.js +39 -13
  249. package/dist/controllers/tooltip_controller.js.map +1 -1
  250. package/dist/controllers/transition_controller.js +4 -0
  251. package/dist/controllers/transition_controller.js.map +1 -1
  252. package/dist/controllers/tree_view_controller.d.ts +39 -8
  253. package/dist/controllers/tree_view_controller.js +169 -16
  254. package/dist/controllers/tree_view_controller.js.map +1 -1
  255. package/dist/index.d.ts +28 -1
  256. package/dist/index.js +4588 -1535
  257. package/dist/index.js.map +1 -1
  258. package/dist/inspector/cli.d.ts +72 -6
  259. package/dist/inspector/cli.js +262 -51
  260. package/dist/inspector/cli.js.map +1 -1
  261. package/dist/inspector/cli_bin.js +309 -51
  262. package/dist/inspector/cli_bin.js.map +1 -1
  263. package/dist/inspector/examples.json +40 -40
  264. package/dist/inspector/manifest.json +525 -59
  265. package/dist/positioning/index.js +2 -0
  266. package/dist/positioning/index.js.map +1 -1
  267. package/package.json +2 -2
@@ -10,7 +10,10 @@ import { Controller } from '@hotwired/stimulus';
10
10
  * data-stimeo--accordion-target="trigger"
11
11
  * data-action="click->stimeo--accordion#toggle
12
12
  * keydown->stimeo--accordion#onKeydown"
13
- * aria-expanded="false" aria-controls="panel-1">Section 1</button>
13
+ * aria-expanded="false" aria-controls="panel-1">
14
+ * <span data-stimeo--accordion-target="expandedLabel" hidden>Hide section 1</span>
15
+ * <span data-stimeo--accordion-target="collapsedLabel">Show section 1</span>
16
+ * </button>
14
17
  * </h3>
15
18
  * <div id="panel-1" data-stimeo--accordion-target="panel"
16
19
  * role="region" aria-labelledby="trigger-1" hidden>…</div>
@@ -32,21 +35,45 @@ import { Controller } from '@hotwired/stimulus';
32
35
  * - `ArrowDown`/`ArrowUp` move focus between headers; `Home`/`End` jump to the
33
36
  * first/last header. Hidden headers (including those inside a hidden subtree,
34
37
  * e.g. a section a filter collapsed) are skipped so focus stays perceivable.
38
+ * - A header may carry an `expandedLabel` / `collapsedLabel` pair: the half that
39
+ * matches its `aria-expanded` is shown and the other hidden, settled as soon as
40
+ * the header and both halves are connected — whichever of them arrives last.
41
+ * Both halves belong inside the same header; a header holding only one of them
42
+ * keeps it exactly as authored. The header's accessible name stays the
43
+ * consumer's to write.
35
44
  * - {@link expandAll} / {@link collapseAll} open or close every panel at once,
36
45
  * for an optional "expand all / collapse all" control pair anywhere in scope.
46
+ * - Each move of a panel is reported: `stimeo--accordion:open` and
47
+ * `stimeo--accordion:close` dispatch
48
+ * `{ reason: StateReason, index: number, trigger: HTMLElement, panel: HTMLElement }`
49
+ * — `index` is the header's position in `triggerTargets` — after
50
+ * `aria-expanded` and `hidden` are written. Both are informational, so
51
+ * neither is cancelable. {@link expandAll} / {@link collapseAll} report one
52
+ * event per pair that actually moved, so a second `expandAll` says nothing.
53
+ * The controller establishes no baseline of its own — a header and the panel it
54
+ * controls keep the state their markup carries — so a connection reports nothing.
37
55
  */
38
56
  declare class AccordionController extends Controller<HTMLElement> {
39
57
  #private;
40
58
  static targets: string[];
41
59
  static actions: readonly ["collapseAll", "expandAll", "onKeydown", "toggle"];
60
+ static events: readonly ["close", "open"];
42
61
  readonly triggerTargets: HTMLButtonElement[];
43
62
  readonly panelTargets: HTMLElement[];
63
+ readonly expandedLabelTargets: HTMLElement[];
64
+ readonly collapsedLabelTargets: HTMLElement[];
65
+ /** Settles the label pair of a header as it joins the group. */
66
+ triggerTargetConnected(trigger: HTMLButtonElement): void;
67
+ /** Settles the pair of the header an arriving expanded half belongs to. */
68
+ expandedLabelTargetConnected(label: HTMLElement): void;
69
+ /** Settles the pair of the header an arriving collapsed half belongs to. */
70
+ collapsedLabelTargetConnected(label: HTMLElement): void;
44
71
  /** Toggles the panel controlled by the activated header. */
45
72
  toggle(event: Event): void;
46
73
  /** Opens every panel. Bound via `data-action` on an "expand all" control. */
47
- expandAll(): void;
74
+ expandAll(event?: Event): void;
48
75
  /** Closes every panel. Bound via `data-action` on a "collapse all" control. */
49
- collapseAll(): void;
76
+ collapseAll(event?: Event): void;
50
77
  /**
51
78
  * Moves focus between headers per the APG keyboard model, skipping any header
52
79
  * that is hidden or nested in a hidden subtree. A consumer may hide whole
@@ -8,36 +8,125 @@ function isReservedArrowChord(event, allow = []) {
8
8
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
9
  }
10
10
 
11
+ // src/utils/state_reason.ts
12
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
13
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
14
+ function stateReasonFor(event) {
15
+ if (!event) return "api";
16
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
17
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
18
+ return "user";
19
+ }
20
+
21
+ // src/utils/state_regions.ts
22
+ var StateRegions = class {
23
+ #whenTrue;
24
+ #whenFalse;
25
+ #taken = /* @__PURE__ */ new WeakSet();
26
+ constructor(options) {
27
+ this.#whenTrue = options.whenTrue;
28
+ this.#whenFalse = options.whenFalse ?? null;
29
+ }
30
+ /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
31
+ reflect(host, isTrue) {
32
+ const shown = this.#inside(host, this.#whenTrue());
33
+ if (!this.#whenFalse) {
34
+ for (const region of shown) this.#write(region, !isTrue);
35
+ return;
36
+ }
37
+ const hiddenSide = this.#inside(host, this.#whenFalse());
38
+ if (shown.length === 0 || hiddenSide.length === 0) {
39
+ for (const region of shown) this.#give(region);
40
+ for (const region of hiddenSide) this.#give(region);
41
+ return;
42
+ }
43
+ for (const region of shown) this.#write(region, !isTrue);
44
+ for (const region of hiddenSide) this.#write(region, isTrue);
45
+ }
46
+ /** Writes `hidden` where it moves, noting which regions it takes out of view. */
47
+ #write(region, hidden) {
48
+ if (hidden) this.#taken.add(region);
49
+ else this.#taken.delete(region);
50
+ if (region.hidden !== hidden) region.hidden = hidden;
51
+ }
52
+ /** Returns a region this instance hid; one it never hid keeps what it carries. */
53
+ #give(region) {
54
+ if (this.#taken.delete(region)) region.hidden = false;
55
+ }
56
+ /** The declared regions that sit within `host`, which may be the host itself. */
57
+ #inside(host, regions) {
58
+ return regions.filter((region) => host.contains(region));
59
+ }
60
+ };
61
+
11
62
  // src/controllers/accordion_controller.ts
12
63
  var AccordionController = class extends Controller {
13
- static targets = ["trigger", "panel"];
64
+ static targets = ["trigger", "panel", "expandedLabel", "collapsedLabel"];
14
65
  static actions = ["collapseAll", "expandAll", "onKeydown", "toggle"];
66
+ static events = ["close", "open"];
67
+ /** Owns `hidden` on the label pair a header declares, narrowed to that header. */
68
+ #labels = new StateRegions({
69
+ whenTrue: () => this.expandedLabelTargets,
70
+ whenFalse: () => this.collapsedLabelTargets
71
+ });
72
+ /** Settles the label pair of a header as it joins the group. */
73
+ triggerTargetConnected(trigger) {
74
+ this.#reflectLabels(trigger);
75
+ }
76
+ /** Settles the pair of the header an arriving expanded half belongs to. */
77
+ expandedLabelTargetConnected(label) {
78
+ this.#settleLabelHost(label);
79
+ }
80
+ /** Settles the pair of the header an arriving collapsed half belongs to. */
81
+ collapsedLabelTargetConnected(label) {
82
+ this.#settleLabelHost(label);
83
+ }
84
+ /** Reflects the pair of the header a label half sits in, where a header holds it. */
85
+ #settleLabelHost(label) {
86
+ const trigger = this.triggerTargets.find((candidate) => candidate.contains(label));
87
+ if (trigger) this.#reflectLabels(trigger);
88
+ }
15
89
  /** Toggles the panel controlled by the activated header. */
16
90
  toggle(event) {
17
91
  const trigger = event.currentTarget;
18
92
  const panel = this.#panelFor(trigger);
19
93
  if (!panel) return;
20
- this.#setExpanded(trigger, panel, trigger.getAttribute("aria-expanded") !== "true");
94
+ this.#setExpanded(
95
+ trigger,
96
+ panel,
97
+ trigger.getAttribute("aria-expanded") !== "true",
98
+ stateReasonFor(event)
99
+ );
21
100
  }
22
101
  /** Opens every panel. Bound via `data-action` on an "expand all" control. */
23
- expandAll() {
24
- this.#setAll(true);
102
+ expandAll(event) {
103
+ this.#setAll(true, stateReasonFor(event));
25
104
  }
26
105
  /** Closes every panel. Bound via `data-action` on a "collapse all" control. */
27
- collapseAll() {
28
- this.#setAll(false);
106
+ collapseAll(event) {
107
+ this.#setAll(false, stateReasonFor(event));
29
108
  }
30
109
  /** Drives every header/panel pair to the same expanded state. */
31
- #setAll(open) {
110
+ #setAll(open, reason) {
32
111
  for (const trigger of this.triggerTargets) {
33
112
  const panel = this.#panelFor(trigger);
34
- if (panel) this.#setExpanded(trigger, panel, open);
113
+ if (panel) this.#setExpanded(trigger, panel, open, reason);
35
114
  }
36
115
  }
37
116
  /** Reflects one header/panel pair's state through `aria-expanded` + `hidden`. */
38
- #setExpanded(trigger, panel, open) {
117
+ #setExpanded(trigger, panel, open, reason) {
118
+ const was = trigger.getAttribute("aria-expanded") === "true";
39
119
  trigger.setAttribute("aria-expanded", open ? "true" : "false");
40
120
  panel.hidden = !open;
121
+ this.#labels.reflect(trigger, open);
122
+ if (was === open) return;
123
+ const detail = { reason, index: this.triggerTargets.indexOf(trigger), trigger, panel };
124
+ if (open) this.dispatch("open", { detail, cancelable: false });
125
+ else this.dispatch("close", { detail, cancelable: false });
126
+ }
127
+ /** Shows the half of a header's label pair that belongs to its `aria-expanded`. */
128
+ #reflectLabels(trigger) {
129
+ this.#labels.reflect(trigger, trigger.getAttribute("aria-expanded") === "true");
41
130
  }
42
131
  /**
43
132
  * Moves focus between headers per the APG keyboard model, skipping any header
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/controllers/accordion_controller.ts"],"names":[],"mappings":"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","file":"accordion_controller.js","sourcesContent":["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/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\n\n/**\n * Headless, accessible accordion behavior.\n *\n * Markup contract (identifier: `stimeo--accordion`):\n * <div data-controller=\"stimeo--accordion\">\n * <h3>\n * <button id=\"trigger-1\"\n * data-stimeo--accordion-target=\"trigger\"\n * data-action=\"click->stimeo--accordion#toggle\n * keydown->stimeo--accordion#onKeydown\"\n * aria-expanded=\"false\" aria-controls=\"panel-1\">Section 1</button>\n * </h3>\n * <div id=\"panel-1\" data-stimeo--accordion-target=\"panel\"\n * role=\"region\" aria-labelledby=\"trigger-1\" hidden>…</div>\n * <!-- repeat header/panel pairs -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Accordion** pattern. Each header button is\n * associated with its panel through `aria-controls`; the panel's `id` is the\n * single source of truth for the pairing, so headers and panels need not be\n * adjacent siblings.\n *\n * @remarks\n * Multiple panels may be open at once (this is the APG-allowed default). State\n * is reflected through `aria-expanded` on the header and the `hidden` attribute\n * on the panel — never through visual styling, which the consumer owns.\n *\n * Behavior provided:\n * - Click a header to toggle its panel (`aria-expanded` + `hidden` reflect state).\n * - `ArrowDown`/`ArrowUp` move focus between headers; `Home`/`End` jump to the\n * first/last header. Hidden headers (including those inside a hidden subtree,\n * e.g. a section a filter collapsed) are skipped so focus stays perceivable.\n * - {@link expandAll} / {@link collapseAll} open or close every panel at once,\n * for an optional \"expand all / collapse all\" control pair anywhere in scope.\n */\nexport class AccordionController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static actions = [\"collapseAll\", \"expandAll\", \"onKeydown\", \"toggle\"] as const;\n\n declare readonly triggerTargets: HTMLButtonElement[];\n declare readonly panelTargets: HTMLElement[];\n\n /** Toggles the panel controlled by the activated header. */\n toggle(event: Event): void {\n const trigger = event.currentTarget as HTMLButtonElement;\n const panel = this.#panelFor(trigger);\n if (!panel) return;\n\n this.#setExpanded(trigger, panel, trigger.getAttribute(\"aria-expanded\") !== \"true\");\n }\n\n /** Opens every panel. Bound via `data-action` on an \"expand all\" control. */\n expandAll(): void {\n this.#setAll(true);\n }\n\n /** Closes every panel. Bound via `data-action` on a \"collapse all\" control. */\n collapseAll(): void {\n this.#setAll(false);\n }\n\n /** Drives every header/panel pair to the same expanded state. */\n #setAll(open: boolean): void {\n for (const trigger of this.triggerTargets) {\n const panel = this.#panelFor(trigger);\n if (panel) this.#setExpanded(trigger, panel, open);\n }\n }\n\n /** Reflects one header/panel pair's state through `aria-expanded` + `hidden`. */\n #setExpanded(trigger: HTMLButtonElement, panel: HTMLElement, open: boolean): void {\n trigger.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n panel.hidden = !open;\n }\n\n /**\n * Moves focus between headers per the APG keyboard model, skipping any header\n * that is hidden or nested in a hidden subtree. A consumer may hide whole\n * sections (e.g. an `stimeo--filter` that collapses empty groups), and an\n * unperceivable header must never become an arrow-key target — otherwise\n * `.focus()` lands on nothing and navigation appears to stall.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const current = event.currentTarget as HTMLButtonElement;\n if (this.triggerTargets.indexOf(current) === -1) return;\n\n // `closest(\"[hidden]\")` catches both a directly-hidden header and one inside a\n // hidden ancestor (the filter-group case); navigate over the visible set only.\n const navigable = this.triggerTargets.filter((trigger) => trigger.closest(\"[hidden]\") === null);\n const here = navigable.indexOf(current);\n if (here === -1) return;\n\n let next: HTMLButtonElement | undefined;\n switch (event.key) {\n case \"ArrowDown\":\n next = navigable[(here + 1) % navigable.length];\n break;\n case \"ArrowUp\":\n next = navigable[(here - 1 + navigable.length) % navigable.length];\n break;\n case \"Home\":\n next = navigable[0];\n break;\n case \"End\":\n next = navigable[navigable.length - 1];\n break;\n default:\n return;\n }\n\n event.preventDefault();\n next?.focus();\n }\n\n /** Resolves the panel a header controls via its `aria-controls` reference. */\n #panelFor(trigger: HTMLButtonElement): HTMLElement | null {\n const id = trigger.getAttribute(\"aria-controls\");\n return id ? (this.panelTargets.find((panel) => panel.id === id) ?? null) : null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/state_reason.ts","../../src/utils/state_regions.ts","../../src/controllers/accordion_controller.ts"],"names":[],"mappings":"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","file":"accordion_controller.js","sourcesContent":["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/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Why a component's public open/closed state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","/** What a consumer declares once about the regions one boolean state reveals. */\nexport interface StateRegionsOptions {\n /** Returns the declared regions shown while the state holds. */\n readonly whenTrue: () => readonly HTMLElement[];\n /**\n * Returns the declared regions shown while the state does not hold. Omitted by a\n * state whose other side has nothing of its own to show.\n */\n readonly whenFalse?: () => readonly HTMLElement[];\n}\n\n/**\n * Owns `hidden` on the regions a boolean state reveals.\n *\n * A region is a **declared** target, so the consumer decides whether one exists at\n * all; where it does, which side is shown is a pure function of the state and the\n * controller writes it unconditionally. An authored `hidden` is therefore not read\n * back — the first reflection after a connection settles it, and a restored DOM\n * cannot leave a region contradicting the state it belongs to.\n *\n * The regions arrive as the caller's own target arrays, which Stimulus has already\n * scoped to this instance: a nested controller registered under the same identifier\n * keeps its own regions. `host` narrows that set further, to the regions inside one\n * trigger or item, so a widget with several of them reflects each independently.\n *\n * **Two sides are a pair.** Where the state has a `whenFalse` side and only one half\n * sits inside `host`, the half that is there is left in view: hiding it would take\n * the only label with it, and the static check names the missing one instead. What\n * the author wrote on a lone half is theirs and stays — only a half this instance\n * took out of view is given back, which is what a pair losing one side at runtime\n * leaves behind.\n *\n * `hidden` moves only when it changes, so a consumer watching its own subtree for\n * attribute mutations sees one record per transition rather than one per reflection.\n *\n * @example\n * ```ts\n * readonly #labels = new StateRegions({\n * whenTrue: () => this.expandedLabelTargets,\n * whenFalse: () => this.collapsedLabelTargets,\n * });\n *\n * #reflect(): void {\n * this.#labels.reflect(this.triggerTarget, this.#expanded);\n * }\n * ```\n */\nexport class StateRegions {\n readonly #whenTrue: () => readonly HTMLElement[];\n readonly #whenFalse: (() => readonly HTMLElement[]) | null;\n readonly #taken = new WeakSet<HTMLElement>();\n\n constructor(options: StateRegionsOptions) {\n this.#whenTrue = options.whenTrue;\n this.#whenFalse = options.whenFalse ?? null;\n }\n\n /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */\n reflect(host: Element, isTrue: boolean): void {\n const shown = this.#inside(host, this.#whenTrue());\n if (!this.#whenFalse) {\n for (const region of shown) this.#write(region, !isTrue);\n return;\n }\n const hiddenSide = this.#inside(host, this.#whenFalse());\n if (shown.length === 0 || hiddenSide.length === 0) {\n for (const region of shown) this.#give(region);\n for (const region of hiddenSide) this.#give(region);\n return;\n }\n for (const region of shown) this.#write(region, !isTrue);\n for (const region of hiddenSide) this.#write(region, isTrue);\n }\n\n /** Writes `hidden` where it moves, noting which regions it takes out of view. */\n #write(region: HTMLElement, hidden: boolean): void {\n if (hidden) this.#taken.add(region);\n else this.#taken.delete(region);\n if (region.hidden !== hidden) region.hidden = hidden;\n }\n\n /** Returns a region this instance hid; one it never hid keeps what it carries. */\n #give(region: HTMLElement): void {\n if (this.#taken.delete(region)) region.hidden = false;\n }\n\n /** The declared regions that sit within `host`, which may be the host itself. */\n #inside(host: Element, regions: readonly HTMLElement[]): HTMLElement[] {\n return regions.filter((region) => host.contains(region));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\nimport { StateRegions } from \"../utils/state_regions\";\n\n/**\n * Headless, accessible accordion behavior.\n *\n * Markup contract (identifier: `stimeo--accordion`):\n * <div data-controller=\"stimeo--accordion\">\n * <h3>\n * <button id=\"trigger-1\"\n * data-stimeo--accordion-target=\"trigger\"\n * data-action=\"click->stimeo--accordion#toggle\n * keydown->stimeo--accordion#onKeydown\"\n * aria-expanded=\"false\" aria-controls=\"panel-1\">\n * <span data-stimeo--accordion-target=\"expandedLabel\" hidden>Hide section 1</span>\n * <span data-stimeo--accordion-target=\"collapsedLabel\">Show section 1</span>\n * </button>\n * </h3>\n * <div id=\"panel-1\" data-stimeo--accordion-target=\"panel\"\n * role=\"region\" aria-labelledby=\"trigger-1\" hidden>…</div>\n * <!-- repeat header/panel pairs -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Accordion** pattern. Each header button is\n * associated with its panel through `aria-controls`; the panel's `id` is the\n * single source of truth for the pairing, so headers and panels need not be\n * adjacent siblings.\n *\n * @remarks\n * Multiple panels may be open at once (this is the APG-allowed default). State\n * is reflected through `aria-expanded` on the header and the `hidden` attribute\n * on the panel — never through visual styling, which the consumer owns.\n *\n * Behavior provided:\n * - Click a header to toggle its panel (`aria-expanded` + `hidden` reflect state).\n * - `ArrowDown`/`ArrowUp` move focus between headers; `Home`/`End` jump to the\n * first/last header. Hidden headers (including those inside a hidden subtree,\n * e.g. a section a filter collapsed) are skipped so focus stays perceivable.\n * - A header may carry an `expandedLabel` / `collapsedLabel` pair: the half that\n * matches its `aria-expanded` is shown and the other hidden, settled as soon as\n * the header and both halves are connected — whichever of them arrives last.\n * Both halves belong inside the same header; a header holding only one of them\n * keeps it exactly as authored. The header's accessible name stays the\n * consumer's to write.\n * - {@link expandAll} / {@link collapseAll} open or close every panel at once,\n * for an optional \"expand all / collapse all\" control pair anywhere in scope.\n * - Each move of a panel is reported: `stimeo--accordion:open` and\n * `stimeo--accordion:close` dispatch\n * `{ reason: StateReason, index: number, trigger: HTMLElement, panel: HTMLElement }`\n * — `index` is the header's position in `triggerTargets` — after\n * `aria-expanded` and `hidden` are written. Both are informational, so\n * neither is cancelable. {@link expandAll} / {@link collapseAll} report one\n * event per pair that actually moved, so a second `expandAll` says nothing.\n * The controller establishes no baseline of its own — a header and the panel it\n * controls keep the state their markup carries — so a connection reports nothing.\n */\nexport class AccordionController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\", \"expandedLabel\", \"collapsedLabel\"];\n static actions = [\"collapseAll\", \"expandAll\", \"onKeydown\", \"toggle\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly triggerTargets: HTMLButtonElement[];\n declare readonly panelTargets: HTMLElement[];\n declare readonly expandedLabelTargets: HTMLElement[];\n declare readonly collapsedLabelTargets: HTMLElement[];\n\n /** Owns `hidden` on the label pair a header declares, narrowed to that header. */\n readonly #labels = new StateRegions({\n whenTrue: () => this.expandedLabelTargets,\n whenFalse: () => this.collapsedLabelTargets,\n });\n\n /** Settles the label pair of a header as it joins the group. */\n triggerTargetConnected(trigger: HTMLButtonElement): void {\n this.#reflectLabels(trigger);\n }\n\n /** Settles the pair of the header an arriving expanded half belongs to. */\n expandedLabelTargetConnected(label: HTMLElement): void {\n this.#settleLabelHost(label);\n }\n\n /** Settles the pair of the header an arriving collapsed half belongs to. */\n collapsedLabelTargetConnected(label: HTMLElement): void {\n this.#settleLabelHost(label);\n }\n\n /** Reflects the pair of the header a label half sits in, where a header holds it. */\n #settleLabelHost(label: HTMLElement): void {\n const trigger = this.triggerTargets.find((candidate) => candidate.contains(label));\n if (trigger) this.#reflectLabels(trigger);\n }\n\n /** Toggles the panel controlled by the activated header. */\n toggle(event: Event): void {\n const trigger = event.currentTarget as HTMLButtonElement;\n const panel = this.#panelFor(trigger);\n if (!panel) return;\n\n this.#setExpanded(\n trigger,\n panel,\n trigger.getAttribute(\"aria-expanded\") !== \"true\",\n stateReasonFor(event),\n );\n }\n\n /** Opens every panel. Bound via `data-action` on an \"expand all\" control. */\n expandAll(event?: Event): void {\n this.#setAll(true, stateReasonFor(event));\n }\n\n /** Closes every panel. Bound via `data-action` on a \"collapse all\" control. */\n collapseAll(event?: Event): void {\n this.#setAll(false, stateReasonFor(event));\n }\n\n /** Drives every header/panel pair to the same expanded state. */\n #setAll(open: boolean, reason: StateReason): void {\n for (const trigger of this.triggerTargets) {\n const panel = this.#panelFor(trigger);\n if (panel) this.#setExpanded(trigger, panel, open, reason);\n }\n }\n\n /** Reflects one header/panel pair's state through `aria-expanded` + `hidden`. */\n #setExpanded(\n trigger: HTMLButtonElement,\n panel: HTMLElement,\n open: boolean,\n reason: StateReason,\n ): void {\n const was = trigger.getAttribute(\"aria-expanded\") === \"true\";\n trigger.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n panel.hidden = !open;\n this.#labels.reflect(trigger, open);\n if (was === open) return;\n const detail = { reason, index: this.triggerTargets.indexOf(trigger), trigger, panel };\n if (open) this.dispatch(\"open\", { detail, cancelable: false });\n else this.dispatch(\"close\", { detail, cancelable: false });\n }\n\n /** Shows the half of a header's label pair that belongs to its `aria-expanded`. */\n #reflectLabels(trigger: HTMLButtonElement): void {\n this.#labels.reflect(trigger, trigger.getAttribute(\"aria-expanded\") === \"true\");\n }\n\n /**\n * Moves focus between headers per the APG keyboard model, skipping any header\n * that is hidden or nested in a hidden subtree. A consumer may hide whole\n * sections (e.g. an `stimeo--filter` that collapses empty groups), and an\n * unperceivable header must never become an arrow-key target — otherwise\n * `.focus()` lands on nothing and navigation appears to stall.\n */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const current = event.currentTarget as HTMLButtonElement;\n if (this.triggerTargets.indexOf(current) === -1) return;\n\n // `closest(\"[hidden]\")` catches both a directly-hidden header and one inside a\n // hidden ancestor (the filter-group case); navigate over the visible set only.\n const navigable = this.triggerTargets.filter((trigger) => trigger.closest(\"[hidden]\") === null);\n const here = navigable.indexOf(current);\n if (here === -1) return;\n\n let next: HTMLButtonElement | undefined;\n switch (event.key) {\n case \"ArrowDown\":\n next = navigable[(here + 1) % navigable.length];\n break;\n case \"ArrowUp\":\n next = navigable[(here - 1 + navigable.length) % navigable.length];\n break;\n case \"Home\":\n next = navigable[0];\n break;\n case \"End\":\n next = navigable[navigable.length - 1];\n break;\n default:\n return;\n }\n\n event.preventDefault();\n next?.focus();\n }\n\n /** Resolves the panel a header controls via its `aria-controls` reference. */\n #panelFor(trigger: HTMLButtonElement): HTMLElement | null {\n const id = trigger.getAttribute(\"aria-controls\");\n return id ? (this.panelTargets.find((panel) => panel.id === id) ?? null) : null;\n }\n}\n"]}
@@ -19,9 +19,9 @@ import { Controller } from '@hotwired/stimulus';
19
19
  * Implements the WAI-ARIA APG **Alert Dialog** pattern. It is the same modal as
20
20
  * `stimeo--dialog` with two deliberate differences that suit an *interrupting
21
21
  * confirmation*: it never closes on a backdrop click (an accidental dismissal
22
- * would be dangerous), and it exposes explicit {@link confirm}/{@link cancel}
23
- * actions that emit events so the consumer only writes the message and "what to
24
- * do on confirm".
22
+ * would be dangerous), and it exposes explicit
23
+ * {@link AlertDialogController.confirm | confirm}/{@link cancel} actions that emit
24
+ * events so the consumer only writes the message and "what to do on confirm".
25
25
  *
26
26
  * `cancel` dispatches `{ reason: "user" | "escape" }`; `confirm` carries no detail.
27
27
  *
@@ -32,11 +32,12 @@ import { Controller } from '@hotwired/stimulus';
32
32
  * and `Escape` (which cancels) — there is intentionally no backdrop close.
33
33
  *
34
34
  * Behavior provided:
35
- * - {@link open} shows the dialog and moves focus to the `initialFocus` target
36
- * (the least destructive action, by convention), else the first focusable
37
- * element.
35
+ * - {@link AlertDialogController.open | open} shows the dialog and moves focus to
36
+ * the `initialFocus` target (the least destructive action, by convention), else
37
+ * the first focusable element.
38
38
  * - `Tab`/`Shift+Tab` cycle focus within the dialog (focus trap).
39
- * - {@link confirm} closes and dispatches `stimeo--alert-dialog:confirm`.
39
+ * - {@link AlertDialogController.confirm | confirm} closes and dispatches
40
+ * `stimeo--alert-dialog:confirm`.
40
41
  * - {@link cancel} and `Escape` close and dispatch `stimeo--alert-dialog:cancel`
41
42
  * with a `reason` of `"user"` / `"escape"`. Focus returns to the opener.
42
43
  */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/alert_dialog_controller.ts"],"names":[],"mappings":"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","file":"alert_dialog_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\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/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n *\n * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\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 tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/** Reason carried by the `cancel` event: an explicit cancel vs. the Escape key. */\ntype CancelReason = \"user\" | \"escape\";\n\n/**\n * Headless, accessible **alert dialog** behavior.\n *\n * Markup contract (identifier: `stimeo--alert-dialog`):\n * <div data-controller=\"stimeo--alert-dialog\">\n * <button data-stimeo--alert-dialog-target=\"trigger\"\n * data-action=\"click->stimeo--alert-dialog#open\">Delete…</button>\n * <div data-stimeo--alert-dialog-target=\"dialog\" role=\"alertdialog\"\n * aria-modal=\"true\" aria-labelledby=\"t\" aria-describedby=\"d\" hidden>\n * <h2 id=\"t\">…</h2><p id=\"d\">…</p>\n * <button data-stimeo--alert-dialog-target=\"initialFocus\"\n * data-action=\"click->stimeo--alert-dialog#cancel\">Cancel</button>\n * <button data-action=\"click->stimeo--alert-dialog#confirm\">Delete</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Alert Dialog** pattern. It is the same modal as\n * `stimeo--dialog` with two deliberate differences that suit an *interrupting\n * confirmation*: it never closes on a backdrop click (an accidental dismissal\n * would be dangerous), and it exposes explicit {@link confirm}/{@link cancel}\n * actions that emit events so the consumer only writes the message and \"what to\n * do on confirm\".\n *\n * `cancel` dispatches `{ reason: \"user\" | \"escape\" }`; `confirm` carries no detail.\n *\n * @remarks\n * Behavior only. The modal lifecycle (focus trap, scroll lock, background\n * `inert`, focus restore, teardown reversal) is delegated to the shared\n * `FocusTrap`. The only closing affordances are the confirm/cancel actions\n * and `Escape` (which cancels) — there is intentionally no backdrop close.\n *\n * Behavior provided:\n * - {@link open} shows the dialog and moves focus to the `initialFocus` target\n * (the least destructive action, by convention), else the first focusable\n * element.\n * - `Tab`/`Shift+Tab` cycle focus within the dialog (focus trap).\n * - {@link confirm} closes and dispatches `stimeo--alert-dialog:confirm`.\n * - {@link cancel} and `Escape` close and dispatch `stimeo--alert-dialog:cancel`\n * with a `reason` of `\"user\"` / `\"escape\"`. Focus returns to the opener.\n */\nexport class AlertDialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\", \"initialFocus\"];\n static actions = [\"cancel\", \"confirm\", \"open\"] as const;\n static events = [\"cancel\", \"confirm\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly initialFocusTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n declare readonly hasInitialFocusTarget: boolean;\n\n /**\n * Owns the modal side effects. Escape takes the same cancel path as\n * {@link cancel} and emits the same event, tagged `\"escape\"` instead of `\"user\"`; focus falls\n * back to the trigger when nothing was focused before opening.\n */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.#requestCancel(\"escape\"),\n initialFocus: () => (this.hasInitialFocusTarget ? this.initialFocusTarget : null),\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 /** Reverts the modal side effects if torn down while open (Turbo navigation). */\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 /** Confirms: closes and dispatches `confirm`. Bound via `data-action`. */\n confirm(): void {\n if (!this.#isOpen) return;\n this.dispatch(\"confirm\");\n this.#closeDialog();\n }\n\n /** Cancels (user action): closes and dispatches `cancel` with `reason: \"user\"`. */\n cancel(): void {\n this.#requestCancel(\"user\");\n }\n\n /** Shared cancel path used by both the cancel action and the Escape key. */\n #requestCancel(reason: CancelReason): void {\n if (!this.#isOpen) return;\n this.dispatch(\"cancel\", { detail: { reason } });\n this.#closeDialog();\n }\n\n /** Hides the dialog and reverts the modal side effects (restoring focus). */\n #closeDialog(): void {\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/alert_dialog_controller.ts"],"names":[],"mappings":"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","file":"alert_dialog_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\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/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n *\n * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\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 tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/** Reason carried by the `cancel` event: an explicit cancel vs. the Escape key. */\ntype CancelReason = \"user\" | \"escape\";\n\n/**\n * Headless, accessible **alert dialog** behavior.\n *\n * Markup contract (identifier: `stimeo--alert-dialog`):\n * <div data-controller=\"stimeo--alert-dialog\">\n * <button data-stimeo--alert-dialog-target=\"trigger\"\n * data-action=\"click->stimeo--alert-dialog#open\">Delete…</button>\n * <div data-stimeo--alert-dialog-target=\"dialog\" role=\"alertdialog\"\n * aria-modal=\"true\" aria-labelledby=\"t\" aria-describedby=\"d\" hidden>\n * <h2 id=\"t\">…</h2><p id=\"d\">…</p>\n * <button data-stimeo--alert-dialog-target=\"initialFocus\"\n * data-action=\"click->stimeo--alert-dialog#cancel\">Cancel</button>\n * <button data-action=\"click->stimeo--alert-dialog#confirm\">Delete</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Alert Dialog** pattern. It is the same modal as\n * `stimeo--dialog` with two deliberate differences that suit an *interrupting\n * confirmation*: it never closes on a backdrop click (an accidental dismissal\n * would be dangerous), and it exposes explicit\n * {@link AlertDialogController.confirm | confirm}/{@link cancel} actions that emit\n * events so the consumer only writes the message and \"what to do on confirm\".\n *\n * `cancel` dispatches `{ reason: \"user\" | \"escape\" }`; `confirm` carries no detail.\n *\n * @remarks\n * Behavior only. The modal lifecycle (focus trap, scroll lock, background\n * `inert`, focus restore, teardown reversal) is delegated to the shared\n * `FocusTrap`. The only closing affordances are the confirm/cancel actions\n * and `Escape` (which cancels) — there is intentionally no backdrop close.\n *\n * Behavior provided:\n * - {@link AlertDialogController.open | open} shows the dialog and moves focus to\n * the `initialFocus` target (the least destructive action, by convention), else\n * the first focusable element.\n * - `Tab`/`Shift+Tab` cycle focus within the dialog (focus trap).\n * - {@link AlertDialogController.confirm | confirm} closes and dispatches\n * `stimeo--alert-dialog:confirm`.\n * - {@link cancel} and `Escape` close and dispatch `stimeo--alert-dialog:cancel`\n * with a `reason` of `\"user\"` / `\"escape\"`. Focus returns to the opener.\n */\nexport class AlertDialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\", \"initialFocus\"];\n static actions = [\"cancel\", \"confirm\", \"open\"] as const;\n static events = [\"cancel\", \"confirm\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly initialFocusTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n declare readonly hasInitialFocusTarget: boolean;\n\n /**\n * Owns the modal side effects. Escape takes the same cancel path as\n * {@link cancel} and emits the same event, tagged `\"escape\"` instead of `\"user\"`; focus falls\n * back to the trigger when nothing was focused before opening.\n */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.#requestCancel(\"escape\"),\n initialFocus: () => (this.hasInitialFocusTarget ? this.initialFocusTarget : null),\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 /** Reverts the modal side effects if torn down while open (Turbo navigation). */\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 /** Confirms: closes and dispatches `confirm`. Bound via `data-action`. */\n confirm(): void {\n if (!this.#isOpen) return;\n this.dispatch(\"confirm\");\n this.#closeDialog();\n }\n\n /** Cancels (user action): closes and dispatches `cancel` with `reason: \"user\"`. */\n cancel(): void {\n this.#requestCancel(\"user\");\n }\n\n /** Shared cancel path used by both the cancel action and the Escape key. */\n #requestCancel(reason: CancelReason): void {\n if (!this.#isOpen) return;\n this.dispatch(\"cancel\", { detail: { reason } });\n this.#closeDialog();\n }\n\n /** Hides the dialog and reverts the modal side effects (restoring focus). */\n #closeDialog(): void {\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
@@ -22,8 +22,9 @@ import { Controller } from '@hotwired/stimulus';
22
22
  * detail: { message: "12 results", assertive: false },
23
23
  * }))
24
24
  *
25
- * The announcer is the shared substrate other controllers (Auto-Submit, Flash,
26
- * Bulk Select, …) lean on instead of each carrying their own live region.
25
+ * The announcer is the shared live region other controllers hand their messages
26
+ * to (`stimeo--auto-submit`, `stimeo--flash` and `stimeo--bulk-select` among
27
+ * them) instead of each carrying their own.
27
28
  *
28
29
  * @remarks
29
30
  * Behavior only, with **one deliberate exception**: when a `polite`/`assertive`
@@ -346,6 +346,9 @@ var AnnouncerController = class extends Controller {
346
346
  * region (it has to be in the accessibility tree before the text arrives) and
347
347
  * emptying a region that already holds this exact text (an unchanged node is not
348
348
  * re-read, so `dedupeReannounce` clears first and writes on the following pass).
349
+ *
350
+ * @stimeoRuntimeOnly `dedupeReannounce` decides whether one queued message is spoken again; the
351
+ * region's text is the message itself.
349
352
  */
350
353
  #drain(level) {
351
354
  const queue = this.#queues.get(level);
@@ -368,7 +371,11 @@ var AnnouncerController = class extends Controller {
368
371
  this.#scheduleClear(region, message);
369
372
  if (queue.length > 0) this.#scheduleDrain(level);
370
373
  }
371
- /** Clears the region after `clearAfter` ms, unless a newer message replaced it. */
374
+ /**
375
+ * Clears the region after `clearAfter` ms, unless a newer message replaced it.
376
+ *
377
+ * @stimeoRuntimeOnly `clearAfter` is the delay of the one clearing timer this call arms.
378
+ */
372
379
  #scheduleClear(region, message) {
373
380
  if (this.clearAfterValue <= 0) return;
374
381
  this.#regionTimers.set(