stimeo-ui 0.9.0 → 0.11.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 (162) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/dist/cable/index.d.ts +79 -14
  3. package/dist/cable/index.js +226 -43
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js +17 -7
  7. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  8. package/dist/controllers/announcer_controller.js +1 -1
  9. package/dist/controllers/announcer_controller.js.map +1 -1
  10. package/dist/controllers/auto_submit_controller.js.map +1 -1
  11. package/dist/controllers/avatar_controller.js.map +1 -1
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/bulk_select_controller.js +2 -2
  14. package/dist/controllers/bulk_select_controller.js.map +1 -1
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.js.map +1 -1
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +1 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +1 -1
  23. package/dist/controllers/color_picker_controller.js.map +1 -1
  24. package/dist/controllers/combobox_controller.js.map +1 -1
  25. package/dist/controllers/command_palette_controller.js +15 -5
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  28. package/dist/controllers/confirm_controller.js +15 -5
  29. package/dist/controllers/confirm_controller.js.map +1 -1
  30. package/dist/controllers/context_menu_controller.d.ts +5 -4
  31. package/dist/controllers/context_menu_controller.js.map +1 -1
  32. package/dist/controllers/count_up_controller.js.map +1 -1
  33. package/dist/controllers/countdown_controller.js +1 -1
  34. package/dist/controllers/countdown_controller.js.map +1 -1
  35. package/dist/controllers/data_grid_controller.js +1 -1
  36. package/dist/controllers/data_grid_controller.js.map +1 -1
  37. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  38. package/dist/controllers/dialog_controller.js +15 -5
  39. package/dist/controllers/dialog_controller.js.map +1 -1
  40. package/dist/controllers/direct_upload_controller.d.ts +2 -2
  41. package/dist/controllers/direct_upload_controller.js +1 -5
  42. package/dist/controllers/direct_upload_controller.js.map +1 -1
  43. package/dist/controllers/dismissible_controller.js.map +1 -1
  44. package/dist/controllers/drawer_controller.d.ts +2 -2
  45. package/dist/controllers/drawer_controller.js +17 -7
  46. package/dist/controllers/drawer_controller.js.map +1 -1
  47. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  48. package/dist/controllers/flash_controller.js.map +1 -1
  49. package/dist/controllers/focus_controller.d.ts +5 -0
  50. package/dist/controllers/focus_controller.js +45 -7
  51. package/dist/controllers/focus_controller.js.map +1 -1
  52. package/dist/controllers/form_field_controller.js +1 -1
  53. package/dist/controllers/form_field_controller.js.map +1 -1
  54. package/dist/controllers/form_validation_controller.js.map +1 -1
  55. package/dist/controllers/frame_loading_controller.js.map +1 -1
  56. package/dist/controllers/highlight_controller.js.map +1 -1
  57. package/dist/controllers/input_mask_controller.js.map +1 -1
  58. package/dist/controllers/intersection_controller.d.ts +4 -3
  59. package/dist/controllers/intersection_controller.js.map +1 -1
  60. package/dist/controllers/lazy_frame_controller.d.ts +2 -2
  61. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  62. package/dist/controllers/listbox_controller.js.map +1 -1
  63. package/dist/controllers/masonry_controller.d.ts +3 -1
  64. package/dist/controllers/masonry_controller.js.map +1 -1
  65. package/dist/controllers/menu_controller.d.ts +1 -1
  66. package/dist/controllers/menu_controller.js.map +1 -1
  67. package/dist/controllers/menubar_controller.js +2 -2
  68. package/dist/controllers/menubar_controller.js.map +1 -1
  69. package/dist/controllers/meter_controller.d.ts +1 -1
  70. package/dist/controllers/meter_controller.js.map +1 -1
  71. package/dist/controllers/multi_select_controller.js +1 -1
  72. package/dist/controllers/multi_select_controller.js.map +1 -1
  73. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  74. package/dist/controllers/nested_form_controller.js.map +1 -1
  75. package/dist/controllers/number_input_controller.js.map +1 -1
  76. package/dist/controllers/otp_controller.js.map +1 -1
  77. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  78. package/dist/controllers/overflow_menu_controller.js +2 -2
  79. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  80. package/dist/controllers/pagination_controller.d.ts +1 -1
  81. package/dist/controllers/pagination_controller.js.map +1 -1
  82. package/dist/controllers/password_reveal_controller.d.ts +27 -2
  83. package/dist/controllers/password_reveal_controller.js +102 -3
  84. package/dist/controllers/password_reveal_controller.js.map +1 -1
  85. package/dist/controllers/password_strength_controller.d.ts +71 -17
  86. package/dist/controllers/password_strength_controller.js +287 -42
  87. package/dist/controllers/password_strength_controller.js.map +1 -1
  88. package/dist/controllers/pointer_drag_controller.d.ts +23 -5
  89. package/dist/controllers/pointer_drag_controller.js +64 -18
  90. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  91. package/dist/controllers/popover_controller.js.map +1 -1
  92. package/dist/controllers/portal_controller.d.ts +17 -4
  93. package/dist/controllers/portal_controller.js +32 -11
  94. package/dist/controllers/portal_controller.js.map +1 -1
  95. package/dist/controllers/preview_guard_controller.d.ts +22 -14
  96. package/dist/controllers/preview_guard_controller.js +180 -20
  97. package/dist/controllers/preview_guard_controller.js.map +1 -1
  98. package/dist/controllers/progress_controller.js.map +1 -1
  99. package/dist/controllers/radio_group_controller.js.map +1 -1
  100. package/dist/controllers/range_slider_controller.js.map +1 -1
  101. package/dist/controllers/rating_controller.js.map +1 -1
  102. package/dist/controllers/read_more_controller.js.map +1 -1
  103. package/dist/controllers/relative_time_controller.js.map +1 -1
  104. package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
  105. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  106. package/dist/controllers/resizable_controller.d.ts +1 -1
  107. package/dist/controllers/resizable_controller.js +1 -1
  108. package/dist/controllers/resizable_controller.js.map +1 -1
  109. package/dist/controllers/roving_controller.d.ts +17 -4
  110. package/dist/controllers/roving_controller.js +127 -14
  111. package/dist/controllers/roving_controller.js.map +1 -1
  112. package/dist/controllers/scroll_area_controller.js.map +1 -1
  113. package/dist/controllers/scroll_restore_controller.d.ts +33 -11
  114. package/dist/controllers/scroll_restore_controller.js +127 -27
  115. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  116. package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
  117. package/dist/controllers/scroll_visibility_controller.js +167 -14
  118. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  119. package/dist/controllers/scrollspy_controller.js.map +1 -1
  120. package/dist/controllers/separator_controller.js.map +1 -1
  121. package/dist/controllers/sidebar_controller.js +15 -5
  122. package/dist/controllers/sidebar_controller.js.map +1 -1
  123. package/dist/controllers/skeleton_controller.js.map +1 -1
  124. package/dist/controllers/slider_controller.js.map +1 -1
  125. package/dist/controllers/spinner_controller.js.map +1 -1
  126. package/dist/controllers/step_indicator_controller.js +0 -5
  127. package/dist/controllers/step_indicator_controller.js.map +1 -1
  128. package/dist/controllers/stick_to_bottom_controller.d.ts +26 -9
  129. package/dist/controllers/stick_to_bottom_controller.js +149 -28
  130. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  131. package/dist/controllers/submit_once_controller.js.map +1 -1
  132. package/dist/controllers/switch_controller.js.map +1 -1
  133. package/dist/controllers/tabs_controller.js.map +1 -1
  134. package/dist/controllers/tags_input_controller.js.map +1 -1
  135. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  136. package/dist/controllers/theme_controller.d.ts +32 -6
  137. package/dist/controllers/theme_controller.js +151 -40
  138. package/dist/controllers/theme_controller.js.map +1 -1
  139. package/dist/controllers/time_picker_controller.js.map +1 -1
  140. package/dist/controllers/toast_controller.d.ts +2 -2
  141. package/dist/controllers/toast_controller.js +2 -2
  142. package/dist/controllers/toast_controller.js.map +1 -1
  143. package/dist/controllers/toggle_group_controller.js.map +1 -1
  144. package/dist/controllers/toolbar_controller.js.map +1 -1
  145. package/dist/controllers/transition_controller.d.ts +11 -7
  146. package/dist/controllers/transition_controller.js +79 -15
  147. package/dist/controllers/transition_controller.js.map +1 -1
  148. package/dist/controllers/tree_view_controller.js.map +1 -1
  149. package/dist/index.d.ts +6 -3
  150. package/dist/index.js +1188 -318
  151. package/dist/index.js.map +1 -1
  152. package/dist/inspector/cli.d.ts +3 -4
  153. package/dist/inspector/cli.js +1 -0
  154. package/dist/inspector/cli.js.map +1 -1
  155. package/dist/inspector/cli_bin.js +6 -5
  156. package/dist/inspector/cli_bin.js.map +1 -1
  157. package/dist/inspector/examples.json +6 -6
  158. package/dist/inspector/manifest.json +15 -32
  159. package/dist/positioning/index.d.ts +18 -4
  160. package/dist/positioning/index.js +84 -25
  161. package/dist/positioning/index.js.map +1 -1
  162. package/package.json +6 -6
@@ -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 (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\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 for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\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 three 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 #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return 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 * {@link 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 is routed through {@link cancel} so it\n * emits the same event as the cancel button (tagged `\"escape\"`); 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":";;;;;AA2CO,IAAM,gBAAA,GAAN,MAAM,iBAAA,CAAiB;AAAA;AAAA,EAE5B,OAAgB,YAAA,mBAAe,IAAI,GAAA,EAAsB;AAAA;AAAA,EAGzD,OAAgB,iBAAiB,MAAY;AAC3C,IAAA,KAAA,MAAW,UAAA,IAAc,iBAAA,CAAiB,YAAA,EAAc,UAAA,CAAW,OAAA,EAAQ;AAAA,EAC7E,CAAA;AAAA,EAES,OAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAiB,YAAA,CAAa,IAAA,KAAS,CAAA;AACrD,IAAA,iBAAA,CAAiB,YAAA,CAAa,IAAI,IAAI,CAAA;AACtC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,IACjF;AAAA,EACF;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,iBAAA,CAAiB,YAAA,CAAa,OAAO,IAAI,CAAA;AACzC,IAAA,IAAI,iBAAA,CAAiB,YAAA,CAAa,IAAA,GAAO,CAAA,EAAG;AAC5C,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,EACpF;AACF,CAAA;;;ACHO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA,EACvB,OAAgB,WAAA,mBAAc,IAAI,OAAA,EAAuC;AAAA,EAEzE,cAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,UAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,OAAA,GAAkC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,QAAA,CAAS,aAAA,GAA0B,QAAA,EAAU,OAAA,EAAmC;AAC9E,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AACxD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,aAAY,eAAA,EAAgB;AACvC,MAAA,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAA,EAAe,QAAQ,CAAA;AACnD,MAAA,aAAA,CAAc,gBAAA,CAAiB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAAA,IAC9D;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,aAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,MAAA,IAAU,IAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,GAAmB;AACjB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AAC7C,MAAA,IAAI,SAAS,CAAA,EAAG,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9C,MAAA,IAAI,QAAA,CAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC/B,QAAA,aAAA,CAAc,mBAAA,CAAoB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAC/D,QAAA,YAAA,CAAY,WAAA,CAAY,OAAO,aAAa,CAAA;AAAA,MAC9C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAA,GAAsB;AACxB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AACtB,IAAA,OAAO,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA,KAAM,IAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,eAAA,GAAuC;AAC5C,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,OAAO,EAAC;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,KAA+B;AACzC,QAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,QAAA,MAAM,KAAA,GAAQ,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA;AACtD,QAAA,IAAI,CAAC,KAAA,EAAO;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,UAAA,IAAa;AAAA,MACrB;AAAA,KACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,cAAc,KAAA,EAA0C;AAC7D,IAAA,KAAA,IAAS,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,KAAA,IAAS,GAAG,KAAA,EAAA,EAAS;AACtD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,SAAQ,EAAG;AACvC,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;;;ACpJO,SAAS,yBAAyB,OAAA,EAA+B;AACtE,EAAA,IAAI,QAAA,GAA2B,OAAA,CAAQ,OAAA,CAAQ,oBAAoB,CAAA;AACnE,EAAA,OAAO,QAAA,EAAU;AACf,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,QAAQ,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,OAAO,GAAG,OAAO,IAAA;AACvC,IAAA,QAAA,GAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,oBAAoB,CAAA,IAAK,IAAA;AAAA,EACtE;AACA,EAAA,OAAO,KAAA;AACT;AAgDO,SAAS,aAAa,OAAA,EAA+B;AAC1D,EAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,mBAAmB,CAAA,EAAG,OAAO,KAAA;AACjD,EAAA,IAAI,OAAA,YAAmB,gBAAA,IAAoB,OAAA,CAAQ,IAAA,KAAS,UAAU,OAAO,KAAA;AAC7E,EAAA,IAAI,EAAE,UAAA,IAAc,OAAA,CAAA,EAAU,OAAO,IAAA;AACrC,EAAA,IAAK,OAAA,CAAgD,UAAU,OAAO,KAAA;AACtE,EAAA,OAAO,CAAC,yBAAyB,OAAO,CAAA;AAC1C;AAGO,IAAM,2BAAA,GAA8B;AAAA,EACzC,SAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,CAAE,KAAK,GAAG,CAAA;AAQH,SAAS,mBAAmB,OAAA,EAA+B;AAChE,EAAA,MAAM,QAAS,OAAA,CAA8C,eAAA;AAC7D,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA,GAAa,KAAA,CAAM,IAAA,CAAK,SAAS,EAAE,kBAAA,EAAoB,IAAA,EAAM,CAAA,GAAI,IAAA;AAC3F;AAGA,SAAS,iBAAiB,OAAA,EAAqC;AAC7D,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA;AAC7C,EAAA,IAAI,KAAA,KAAU,QAAQ,CAAC,YAAA,CAAa,KAAK,KAAA,CAAM,IAAA,EAAM,CAAA,EAAG,OAAO,IAAA;AAC/D,EAAA,OAAO,OAAO,KAAK,CAAA;AACrB;AAGA,SAAS,iBAAiB,OAAA,EAA+B;AACvD,EAAA,IAAI,OAAA,YAAmB,iBAAA,IAAqB,OAAA,YAAmB,eAAA,EAAiB;AAC9E,IAAA,OAAO,OAAA,CAAQ,aAAa,MAAM,CAAA;AAAA,EACpC;AACA,EAAA,IACE,OAAA,YAAmB,iBAAA,IACnB,OAAA,YAAmB,iBAAA,IACnB,mBAAmB,mBAAA,EACnB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAA,YAAmB,gBAAA,EAAkB,OAAO,OAAA,CAAQ,IAAA,KAAS,QAAA;AACjE,EAAA,IAAI,OAAA,YAAmB,mBAAmB,OAAO,IAAA;AACjD,EAAA,IAAI,OAAA,CAAQ,OAAA,KAAY,OAAA,IAAW,OAAA,CAAQ,YAAY,OAAA,EAAS;AAC9D,IAAA,OAAO,OAAA,CAAQ,aAAa,UAAU,CAAA;AAAA,EACxC;AACA,EAAA,IAAI,OAAA,YAAmB,WAAA,IAAe,OAAA,CAAQ,OAAA,KAAY,SAAA,EAAW;AACnE,IAAA,MAAM,UAAU,OAAA,CAAQ,aAAA;AACxB,IAAA,OACE,OAAA,YAAmB,kBAAA,IACnB,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,SAAS,CAAA,KAAM,OAAA;AAAA,EAElF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,mBAAmB,OAAA,EAA+B;AACzD,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,iBAAiB,GAAG,WAAA,EAAY;AACnE,EAAA,OAAO,KAAA,KAAU,EAAA,IAAM,KAAA,KAAU,MAAA,IAAU,KAAA,KAAU,gBAAA;AACvD;AAUO,SAAS,UAAU,OAAA,EAA+B;AACvD,EAAA,IAAI,CAAC,aAAa,OAAO,CAAA,IAAK,CAAC,kBAAA,CAAmB,OAAO,GAAG,OAAO,KAAA;AAEnE,EAAA,MAAM,QAAA,GAAW,iBAAiB,OAAO,CAAA;AACzC,EAAA,IAAI,QAAA,KAAa,IAAA,EAAM,OAAO,QAAA,IAAY,CAAA;AAC1C,EAAA,OAAO,gBAAA,CAAiB,OAAO,CAAA,IAAK,kBAAA,CAAmB,OAAO,CAAA;AAChE;AAGO,SAAS,eAAe,IAAA,EAAiC;AAC9D,EAAA,OAAO,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAA,CAA8B,2BAA2B,CAAC,CAAA,CAAE,MAAA;AAAA,IACjF;AAAA,GACF;AACF;;;ACtFO,IAAM,YAAN,MAAgB;AAAA;AAAA,EAErB,kBAAA,GAAyC,IAAA;AAAA;AAAA,EAEzC,qBAAA,GAAwB,EAAA;AAAA;AAAA,EAExB,aAAA,GAAgB,KAAA;AAAA;AAAA,EAEhB,mBAAkC,EAAC;AAAA;AAAA,EAEnC,YAAA,GAAe,KAAA;AAAA;AAAA,EAEN,YAAA,GAAe,IAAI,WAAA,EAAY;AAAA;AAAA,EAG/B,aAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,WAAA,CAAY,YAAA,EAAiC,OAAA,GAA4B,EAAC,EAAG;AAC3E,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,YAAA,EAAc;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAIpB,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAA,CAAK,qBACH,MAAA,YAAkB,WAAA,IAAe,MAAA,KAAW,QAAA,CAAS,OAAO,MAAA,GAAS,IAAA;AACvE,IAAA,IAAI,KAAK,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,IAAI,CAAA,EAAG;AAC9C,MAAA,IAAA,CAAK,qBAAA,GAAwB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AACjD,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,MAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,IACvB;AACA,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA,OAAQ,kBAAA,EAAmB;AACrE,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,IAAA,CAAK,aAAa,QAAA,EAAS;AAC3B,IAAA,MAAM,QAAA,GAAW,KAAK,QAAA,CAAS,QAAA;AAC/B,IAAA,IAAI,QAAA,EAAU,IAAA,CAAK,YAAA,CAAa,QAAA,CAAS,QAAA,EAAU,EAAE,SAAA,EAAW,MAAM,QAAA,EAAS,EAAG,CAAA;AAClF,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,WAAW,IAAI,CAAA,OAAQ,aAAA,EAAc;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAW,EAAE,YAAA,GAAe,IAAA,EAAK,GAAgC,EAAC,EAAS;AACzE,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AACpB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACvD,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,IAAA,CAAK,qBAAA;AACpC,MAAA,IAAA,CAAK,aAAA,GAAgB,KAAA;AAAA,IACvB;AACA,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,MAAM,SAAS,IAAA,CAAK,kBAAA,IAAsB,IAAA,CAAK,QAAA,CAAS,iBAAgB,IAAK,IAAA;AAC7E,MAAA,MAAA,EAAQ,KAAA,EAAM;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,KAAA,CAAM,QAA+C,QAAA,EAA4B;AAC/E,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,QAAA;AACjC,IAAA,OAAO,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,EAAO,GAAI,MAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUS,YAAA,GAAe,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,WAAW,EAAE,YAAA,EAAc,KAAA,EAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlF,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EAC9C,CAAA;AAAA;AAAA,EAGA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,MAAM,IAAA,GAAO,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAC3C,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AAGxB,IAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,IAAS,CAAC,KAAK,aAAA,EAAc,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG;AACvE,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,CAAM,QAAA,IAAY,MAAA,KAAW,KAAA,EAAO;AACtC,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,EAAM,KAAA,EAAM;AAAA,IACd,CAAA,MAAA,IAAW,CAAC,KAAA,CAAM,QAAA,IAAY,WAAW,IAAA,EAAM;AAC7C,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,kBAAA,GAA2B;AACzB,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,IAAA,CAAK,mBAAmB,EAAC;AACzB,IAAA,KAAA,IAAS,IAAA,GAAoB,SAAA,EAAW,IAAA,KAAS,QAAA,CAAS,IAAA,IAAQ;AAChE,MAAA,MAAM,SAAS,IAAA,CAAK,aAAA;AACpB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,KAAA,MAAW,OAAA,IAAW,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,QAAA,IAAI,EAAE,mBAAmB,WAAA,CAAA,EAAc;AACvC,QAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAA,CAAQ,KAAA,EAAO;AACvC,QAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAChB,QAAA,IAAA,CAAK,gBAAA,CAAiB,KAAK,OAAO,CAAA;AAAA,MACpC;AACA,MAAA,IAAA,GAAO,MAAA;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,gBAAA,EAAkB;AAC3C,MAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,IAClB;AACA,IAAA,IAAA,CAAK,mBAAmB,EAAC;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,YAAA,IAAe;AAC/C,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,CAAC,CAAA,EAAG;AAChB,MAAA,SAAA,CAAU,CAAC,EAAE,KAAA,EAAM;AACnB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,SAAA,CAAU,QAAA,GAAW,EAAA;AACrB,IAAA,SAAA,CAAU,KAAA,EAAM;AAAA,EAClB;AAAA;AAAA,EAGA,kBAAA,GAAoC;AAClC,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,EAC5C;AACF,CAAA;;;AChOO,IAAM,qBAAA,GAAN,cAAoC,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,UAAU,cAAc,CAAA;AAAA,EAC9D,OAAO,OAAA,GAAU,CAAC,QAAA,EAAU,WAAW,MAAM,CAAA;AAAA,EAC7C,OAAO,MAAA,GAAS,CAAC,QAAA,EAAU,SAAS,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAc3B,KAAA,GAAQ,IAAI,SAAA,CAAU,MAAM,KAAK,YAAA,EAAc;AAAA,IACtD,QAAA,EAAU,MAAM,IAAA,CAAK,cAAA,CAAe,QAAQ,CAAA;AAAA,IAC5C,YAAA,EAAc,MAAO,IAAA,CAAK,qBAAA,GAAwB,KAAK,kBAAA,GAAqB,IAAA;AAAA,IAC5E,aAAA,EAAe,MAAO,IAAA,CAAK,gBAAA,GAAmB,KAAK,aAAA,GAAgB;AAAA,GACpE,CAAA;AAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,MAAA,GAAS,IAAA;AAAA,EACvD;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAE,YAAA,EAAc,OAAO,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,OAAA,EAAS;AAC3C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACnB,IAAA,IAAA,CAAK,SAAS,SAAS,CAAA;AACvB,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,eAAe,MAAM,CAAA;AAAA,EAC5B;AAAA;AAAA,EAGA,eAAe,MAAA,EAA4B;AACzC,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACnB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,MAAA,IAAU,CAAA;AAC9C,IAAA,IAAA,CAAK,YAAA,EAAa;AAAA,EACpB;AAAA;AAAA,EAGA,YAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,IAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,CAAC,IAAA,CAAK,YAAA,CAAa,MAAA;AAAA,EACpD;AACF","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 three 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 * {@link 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"]}
@@ -338,7 +338,7 @@ var AnnouncerController = class extends Controller {
338
338
  }
339
339
  /**
340
340
  * Arms `region`'s single pending timer. Callers reach here with the slot
341
- * already free — `#announce` releases it, and a fired timer clears its own
341
+ * already free — `#drain` releases it before writing, and a fired timer clears its own
342
342
  * entry below — so this does not cancel again.
343
343
  */
344
344
  #schedule(region, callback, delay) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/announcer_controller.ts"],"names":[],"mappings":"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","file":"announcer_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The two politeness levels this controller keeps a region for. */\nconst LEVELS = [\"polite\", \"assertive\"] as const;\n\n/** One of the politeness levels in {@link LEVELS}. */\ntype Level = (typeof LEVELS)[number];\n\n/**\n * Headless, shared **live-region announcer** — a polite/assertive screen-reader\n * announcement base (no dedicated APG pattern; follows the WAI-ARIA \"Alert\" /\n * \"Status\" live-region guidance and WCAG 2.2 **4.1.3 Status Messages**).\n *\n * Markup contract (identifier: `stimeo--announcer`):\n * <!-- Place once per page; the consumer visually hides the regions in CSS. -->\n * <div data-controller=\"stimeo--announcer\">\n * <div data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n * <div data-stimeo--announcer-target=\"assertive\" aria-live=\"assertive\" aria-atomic=\"true\"></div>\n * </div>\n *\n * <!-- Attribute-only trigger: declare the activation event explicitly. -->\n * <button data-action=\"click->stimeo--announcer#announce\"\n * data-stimeo--announcer-message-param=\"Saved\"\n * data-stimeo--announcer-assertive-param=\"false\">Save</button>\n *\n * <!-- Programmatic trigger (e.g. from another controller / Turbo Stream). -->\n * window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", {\n * detail: { message: \"12 results\", assertive: false },\n * }))\n *\n * The announcer is the shared substrate other controllers (Auto-Submit, Flash,\n * Bulk Select, …) lean on instead of each carrying their own live region.\n *\n * @remarks\n * Behavior only, with **one deliberate exception**: when a `polite`/`assertive`\n * target is absent the controller *generates* the missing region **on connect**\n * and applies the canonical visually-hidden inline style (see {@link visuallyHide}).\n * A live region must exist and be visually hidden to do its job, and a generated\n * node has no consumer CSS hook to hide it; consumers who want to own styling\n * supply their own targets. Generation happens up front rather than on the first\n * announcement because assistive tech reports changes to a region it already\n * knows about — a region created and written within one task loses that first\n * message. The controller never moves focus — announcements must not steal it\n * (WCAG 2.2 4.1.3). Listeners and clear timers are torn down on `disconnect()`\n * (Turbo included), and any generated regions are removed — from the cached\n * snapshot too, via {@link BeforeCacheReset}, because `connect()` cannot reuse a\n * restored region (it carries no target attribute) and would pair another one\n * with it on every visit.\n *\n * **Messages are queued, one write per task.** Assistive tech announces what it\n * observes changing, so two messages written into one region within a single task\n * are one observed change and the earlier message is never read. Each politeness\n * has its own FIFO — an assertive message never waits behind polite ones — and a\n * message is written only in a task where its region already existed.\n *\n * **The region set is kept whole while connected.** Exactly one region per\n * politeness is present: an authored target retires the stand-in generated for it,\n * losing one materialises a stand-in again, and a generated region removed by a\n * morph (it is absent from the server's HTML, so a morph drops it) is put back\n * without waiting for the next message.\n */\nexport class AnnouncerController extends Controller<HTMLElement> {\n static override targets = [\"polite\", \"assertive\"];\n static override values = {\n clearAfter: { type: Number, default: 1000 },\n dedupeReannounce: { type: Boolean, default: true },\n };\n static actions = [\"announce\"] as const;\n\n declare readonly politeTarget: HTMLElement;\n declare readonly assertiveTarget: HTMLElement;\n declare readonly hasPoliteTarget: boolean;\n declare readonly hasAssertiveTarget: boolean;\n\n declare clearAfterValue: number;\n declare dedupeReannounceValue: boolean;\n\n /** Clear/re-announce timers; one `clearAll()` in disconnect tears them all down. */\n readonly #timers = new SafeTimeout();\n\n /** Live regions generated to stand in for absent targets, for teardown. */\n readonly #generated = new Map<Level, HTMLElement>();\n\n /** Messages waiting to be written, oldest first, one queue per politeness. */\n readonly #queues = new Map<Level, string[]>();\n\n /** Politeness levels whose next drain is already armed. */\n readonly #draining = new Set<Level>();\n\n /** Collapses a batch of target callbacks (and morph removals) into one pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileRegions());\n\n /**\n * Watches the host's own children for a generated region disappearing. A morph\n * drops it — the server's HTML never had it — and no target callback reports\n * that, because a generated region carries no target attribute. `subtree` stays\n * off so writing a message inside a region does not re-enter this pass.\n */\n readonly #hostWatch = new MutationObserver(() => {\n this.#reconcile.schedule();\n });\n\n /** Rewinds to an announceable initial state for the snapshot; see the remarks. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /**\n * The one timer a region may have outstanding — its dedupe re-set, then its\n * auto-clear. Held weakly so a swapped-out target is not retained; a leftover\n * id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not\n * own.\n */\n readonly #pending = new WeakMap<HTMLElement, number>();\n\n /**\n * Guards against handling the same CustomEvent twice. An event dispatched on\n * the controller element with `bubbles: true` reaches both the element and the\n * `window` listener; this WeakSet ensures it announces only once.\n */\n readonly #handled = new WeakSet<Event>();\n\n /** Receives programmatic announcements at the element or bubbled to `window`. */\n readonly #onAnnounceEvent = (event: Event): void => {\n if (this.#handled.has(event)) return;\n this.#handled.add(event);\n const detail = (event as CustomEvent<unknown>).detail;\n const message = this.#messageFromDetail(detail);\n if (!message) return;\n this.#announce(message, this.#assertiveFromDetail(detail));\n };\n\n override connect(): void {\n this.#reconcile.activate();\n this.#beforeCache.activate();\n // Materialise any missing region now, so it is in the accessibility tree\n // before the first message rather than appearing with it.\n this.#reconcileRegions();\n this.#hostWatch.observe(this.element, { childList: true });\n this.element.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n }\n\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#hostWatch.disconnect();\n this.#beforeCache.deactivate();\n this.element.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n this.#timers.clearAll();\n this.#queues.clear();\n this.#draining.clear();\n this.#removeGenerated();\n }\n\n /** Retires the stand-in once the consumer supplies a polite region. */\n politeTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's polite region goes away. */\n politeTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Retires the stand-in once the consumer supplies an assertive region. */\n assertiveTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's assertive region goes away. */\n assertiveTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the region set back to exactly one region per politeness and reports\n * whether anything had to be created.\n *\n * A region created here is not written to in the same task: assistive tech\n * reports changes to regions it already knows about, so {@link drain} waits a\n * task whenever this says a region is new.\n */\n #reconcileRegions(): boolean {\n let created = false;\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n // The consumer owns this politeness now; a stand-in would be a second\n // region for it, and an empty one nothing ever writes to.\n const generated = this.#generated.get(level);\n if (generated) {\n generated.remove();\n this.#generated.delete(level);\n }\n continue;\n }\n const existing = this.#generated.get(level);\n if (existing?.isConnected) continue;\n this.#generated.set(level, this.#createRegion(level));\n created = true;\n }\n return created;\n }\n\n /** Whether the consumer supplied a target for `level`. */\n #hasTargetFor(level: Level): boolean {\n return level === \"assertive\" ? this.hasAssertiveTarget : this.hasPoliteTarget;\n }\n\n /** Builds a visually hidden live region for `level` and attaches it. */\n #createRegion(level: Level): HTMLElement {\n const region = document.createElement(\"div\");\n region.setAttribute(\"aria-live\", level);\n region.setAttribute(\"aria-atomic\", \"true\");\n visuallyHide(region);\n this.element.appendChild(region);\n return region;\n }\n\n /**\n * Removes and forgets every region this controller generated. Authored targets\n * belong to the consumer and are left untouched. Forgetting them is what keeps\n * the live page working after a snapshot rewind: the next announcement finds an\n * empty map and materialises a fresh region.\n */\n #removeGenerated(): void {\n for (const region of this.#generated.values()) {\n region.remove();\n }\n this.#generated.clear();\n }\n\n /**\n * Announces a message. Reads the text from a Stimulus action param\n * (`message`, plus optional `assertive`) for attribute-only triggers, falling\n * back to a CustomEvent `detail` when the same handler is wired to an event.\n * An empty/non-string message is ignored so untrusted payloads cannot blank\n * the region.\n */\n announce(event: Event): void {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = params?.message;\n const message =\n typeof fromParam === \"string\" && fromParam.length > 0\n ? fromParam\n : this.#messageFromDetail((event as CustomEvent<unknown>).detail);\n if (!message) return;\n\n const assertive =\n params?.assertive === true ||\n this.#assertiveFromDetail((event as CustomEvent<unknown>).detail);\n this.#announce(message, assertive);\n }\n\n /**\n * Queues `message` for its politeness and arms the drain.\n *\n * Queuing is what makes a burst audible: assistive tech announces the changes it\n * observes, so several messages written into one region within a single task are\n * one change and only the last is read.\n */\n #announce(message: string, assertive: boolean): void {\n const level: Level = assertive ? \"assertive\" : \"polite\";\n const queue = this.#queues.get(level);\n if (queue) {\n queue.push(message);\n } else {\n this.#queues.set(level, [message]);\n }\n this.#scheduleDrain(level);\n }\n\n /** Arms one drain pass for `level`; further messages ride the pass already armed. */\n #scheduleDrain(level: Level): void {\n if (this.#draining.has(level)) return;\n this.#draining.add(level);\n this.#timers.set(() => {\n this.#draining.delete(level);\n this.#drain(level);\n }, 0);\n }\n\n /**\n * Writes one queued message, then arms the next pass while the queue holds more.\n *\n * Two steps take a whole pass without consuming the message: materialising a\n * region (it has to be in the accessibility tree before the text arrives) and\n * emptying a region that already holds this exact text (an unchanged node is not\n * re-read, so `dedupeReannounce` clears first and writes on the following pass).\n */\n #drain(level: Level): void {\n const queue = this.#queues.get(level);\n const message = queue?.[0];\n if (queue === undefined || message === undefined) return;\n\n if (this.#reconcileRegions()) {\n this.#scheduleDrain(level);\n return;\n }\n\n const region = this.#regionFor(level);\n if (this.dedupeReannounceValue && region.textContent === message) {\n this.#cancelPending(region);\n region.textContent = \"\";\n this.#scheduleDrain(level);\n return;\n }\n\n queue.shift();\n this.#cancelPending(region);\n region.textContent = message;\n this.#scheduleClear(region, message);\n if (queue.length > 0) this.#scheduleDrain(level);\n }\n\n /** Clears the region after `clearAfter` ms, unless a newer message replaced it. */\n #scheduleClear(region: HTMLElement, message: string): void {\n if (this.clearAfterValue <= 0) return;\n this.#schedule(\n region,\n () => {\n if (region.textContent === message) region.textContent = \"\";\n },\n this.clearAfterValue,\n );\n }\n\n /**\n * Arms `region`'s single pending timer. Callers reach here with the slot\n * already free — `#announce` releases it, and a fired timer clears its own\n * entry below — so this does not cancel again.\n */\n #schedule(region: HTMLElement, callback: () => void, delay: number): void {\n const id = this.#timers.set(() => {\n // Drop the id as it fires: platform timer ids are recycled, and a stale\n // one left here could later cancel the *other* region's live timer.\n this.#pending.delete(region);\n callback();\n }, delay);\n this.#pending.set(region, id);\n }\n\n /** Releases `region`'s pending timer, if it has one. */\n #cancelPending(region: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending\n // timer\" case needs no branch of its own.\n this.#timers.clear(this.#pending.get(region) ?? -1);\n this.#pending.delete(region);\n }\n\n /**\n * Resolves the live region for a politeness level.\n *\n * The remembered stand-in is used only while it is still in the document: a morph\n * can drop it, and writing into the detached node would announce nothing at all.\n */\n #regionFor(level: Level): HTMLElement {\n if (level === \"assertive\" && this.hasAssertiveTarget) return this.assertiveTarget;\n if (level === \"polite\" && this.hasPoliteTarget) return this.politeTarget;\n\n const existing = this.#generated.get(level);\n if (existing?.isConnected) return existing;\n\n const region = this.#createRegion(level);\n this.#generated.set(level, region);\n return region;\n }\n\n /**\n * Restores the announceable initial state for the snapshot Turbo is about to\n * take: queued and displayed messages go, generated regions go, and the live\n * page — which keeps running when a visit is aborted — gets its regions back on\n * the next task, after the clone.\n */\n #rewindForCache(): void {\n this.#queues.clear();\n this.#draining.clear();\n this.#timers.clearAll();\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n const target = level === \"assertive\" ? this.assertiveTarget : this.politeTarget;\n target.textContent = \"\";\n }\n }\n this.#removeGenerated();\n this.#timers.set(() => this.#reconcileRegions(), 0);\n }\n\n /** Extracts a non-empty string `message` from a CustomEvent detail, else null. */\n #messageFromDetail(detail: unknown): string | null {\n if (detail && typeof detail === \"object\" && \"message\" in detail) {\n const value = (detail as Record<string, unknown>).message;\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Reads an `assertive === true` flag from a CustomEvent detail (default polite). */\n #assertiveFromDetail(detail: unknown): boolean {\n return (\n !!detail &&\n typeof detail === \"object\" &&\n (detail as Record<string, unknown>).assertive === true\n );\n }\n}\n\n/**\n * Applies the canonical visually-hidden (\"sr-only\") inline style to a generated\n * live region so its text is announced without being seen. Inline so the library\n * stays self-contained when the consumer provides no target/CSS of its own.\n *\n * Pure (no `this`); exported for direct unit testing.\n */\nexport function visuallyHide(node: HTMLElement): void {\n const { style } = node;\n style.position = \"absolute\";\n style.width = \"1px\";\n style.height = \"1px\";\n style.margin = \"-1px\";\n style.padding = \"0\";\n style.border = \"0\";\n style.overflow = \"hidden\";\n style.clip = \"rect(0 0 0 0)\";\n style.clipPath = \"inset(50%)\";\n style.whiteSpace = \"nowrap\";\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/announcer_controller.ts"],"names":[],"mappings":"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","file":"announcer_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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The two politeness levels this controller keeps a region for. */\nconst LEVELS = [\"polite\", \"assertive\"] as const;\n\n/** One of the politeness levels in {@link LEVELS}. */\ntype Level = (typeof LEVELS)[number];\n\n/**\n * Headless, shared **live-region announcer** — a polite/assertive screen-reader\n * announcement base (no dedicated APG pattern; follows the WAI-ARIA \"Alert\" /\n * \"Status\" live-region guidance and WCAG 2.2 **4.1.3 Status Messages**).\n *\n * Markup contract (identifier: `stimeo--announcer`):\n * <!-- Place once per page; the consumer visually hides the regions in CSS. -->\n * <div data-controller=\"stimeo--announcer\">\n * <div data-stimeo--announcer-target=\"polite\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n * <div data-stimeo--announcer-target=\"assertive\" aria-live=\"assertive\" aria-atomic=\"true\"></div>\n * </div>\n *\n * <!-- Attribute-only trigger: declare the activation event explicitly. -->\n * <button data-action=\"click->stimeo--announcer#announce\"\n * data-stimeo--announcer-message-param=\"Saved\"\n * data-stimeo--announcer-assertive-param=\"false\">Save</button>\n *\n * <!-- Programmatic trigger (e.g. from another controller / Turbo Stream). -->\n * window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", {\n * detail: { message: \"12 results\", assertive: false },\n * }))\n *\n * The announcer is the shared substrate other controllers (Auto-Submit, Flash,\n * Bulk Select, …) lean on instead of each carrying their own live region.\n *\n * @remarks\n * Behavior only, with **one deliberate exception**: when a `polite`/`assertive`\n * target is absent the controller *generates* the missing region **on connect**\n * and applies the canonical visually-hidden inline style (see {@link visuallyHide}).\n * A live region must exist and be visually hidden to do its job, and a generated\n * node has no consumer CSS hook to hide it; consumers who want to own styling\n * supply their own targets. Generation happens up front rather than on the first\n * announcement because assistive tech reports changes to a region it already\n * knows about — a region created and written within one task loses that first\n * message. The controller never moves focus — announcements must not steal it\n * (WCAG 2.2 4.1.3). Listeners and clear timers are torn down on `disconnect()`\n * (Turbo included), and any generated regions are removed — from the cached\n * snapshot too, via {@link BeforeCacheReset}, because `connect()` cannot reuse a\n * restored region (it carries no target attribute) and would pair another one\n * with it on every visit.\n *\n * **Messages are queued, one write per task.** Assistive tech announces what it\n * observes changing, so two messages written into one region within a single task\n * are one observed change and the earlier message is never read. Each politeness\n * has its own FIFO — an assertive message never waits behind polite ones — and a\n * message is written only in a task where its region already existed.\n *\n * **The region set is kept whole while connected.** Exactly one region per\n * politeness is present: an authored target retires the stand-in generated for it,\n * losing one materialises a stand-in again, and a generated region removed by a\n * morph (it is absent from the server's HTML, so a morph drops it) is put back\n * without waiting for the next message.\n */\nexport class AnnouncerController extends Controller<HTMLElement> {\n static override targets = [\"polite\", \"assertive\"];\n static override values = {\n clearAfter: { type: Number, default: 1000 },\n dedupeReannounce: { type: Boolean, default: true },\n };\n static actions = [\"announce\"] as const;\n\n declare readonly politeTarget: HTMLElement;\n declare readonly assertiveTarget: HTMLElement;\n declare readonly hasPoliteTarget: boolean;\n declare readonly hasAssertiveTarget: boolean;\n\n declare clearAfterValue: number;\n declare dedupeReannounceValue: boolean;\n\n /** Clear/re-announce timers; one `clearAll()` in disconnect tears them all down. */\n readonly #timers = new SafeTimeout();\n\n /** Live regions generated to stand in for absent targets, for teardown. */\n readonly #generated = new Map<Level, HTMLElement>();\n\n /** Messages waiting to be written, oldest first, one queue per politeness. */\n readonly #queues = new Map<Level, string[]>();\n\n /** Politeness levels whose next drain is already armed. */\n readonly #draining = new Set<Level>();\n\n /** Collapses a batch of target callbacks (and morph removals) into one pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileRegions());\n\n /**\n * Watches the host's own children for a generated region disappearing. A morph\n * drops it — the server's HTML never had it — and no target callback reports\n * that, because a generated region carries no target attribute. `subtree` stays\n * off so writing a message inside a region does not re-enter this pass.\n */\n readonly #hostWatch = new MutationObserver(() => {\n this.#reconcile.schedule();\n });\n\n /** Rewinds to an announceable initial state for the snapshot; see the remarks. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /**\n * The one timer a region may have outstanding — its dedupe re-set, then its\n * auto-clear. Held weakly so a swapped-out target is not retained; a leftover\n * id is harmless because {@link SafeTimeout.clear} no-ops on an id it does not\n * own.\n */\n readonly #pending = new WeakMap<HTMLElement, number>();\n\n /**\n * Guards against handling the same CustomEvent twice. An event dispatched on\n * the controller element with `bubbles: true` reaches both the element and the\n * `window` listener; this WeakSet ensures it announces only once.\n */\n readonly #handled = new WeakSet<Event>();\n\n /** Receives programmatic announcements at the element or bubbled to `window`. */\n readonly #onAnnounceEvent = (event: Event): void => {\n if (this.#handled.has(event)) return;\n this.#handled.add(event);\n const detail = (event as CustomEvent<unknown>).detail;\n const message = this.#messageFromDetail(detail);\n if (!message) return;\n this.#announce(message, this.#assertiveFromDetail(detail));\n };\n\n override connect(): void {\n this.#reconcile.activate();\n this.#beforeCache.activate();\n // Materialise any missing region now, so it is in the accessibility tree\n // before the first message rather than appearing with it.\n this.#reconcileRegions();\n this.#hostWatch.observe(this.element, { childList: true });\n this.element.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.addEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n }\n\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#hostWatch.disconnect();\n this.#beforeCache.deactivate();\n this.element.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n window.removeEventListener(\"stimeo--announcer:announce\", this.#onAnnounceEvent);\n this.#timers.clearAll();\n this.#queues.clear();\n this.#draining.clear();\n this.#removeGenerated();\n }\n\n /** Retires the stand-in once the consumer supplies a polite region. */\n politeTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's polite region goes away. */\n politeTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Retires the stand-in once the consumer supplies an assertive region. */\n assertiveTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Materialises a stand-in once the consumer's assertive region goes away. */\n assertiveTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the region set back to exactly one region per politeness and reports\n * whether anything had to be created.\n *\n * A region created here is not written to in the same task: assistive tech\n * reports changes to regions it already knows about, so {@link drain} waits a\n * task whenever this says a region is new.\n */\n #reconcileRegions(): boolean {\n let created = false;\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n // The consumer owns this politeness now; a stand-in would be a second\n // region for it, and an empty one nothing ever writes to.\n const generated = this.#generated.get(level);\n if (generated) {\n generated.remove();\n this.#generated.delete(level);\n }\n continue;\n }\n const existing = this.#generated.get(level);\n if (existing?.isConnected) continue;\n this.#generated.set(level, this.#createRegion(level));\n created = true;\n }\n return created;\n }\n\n /** Whether the consumer supplied a target for `level`. */\n #hasTargetFor(level: Level): boolean {\n return level === \"assertive\" ? this.hasAssertiveTarget : this.hasPoliteTarget;\n }\n\n /** Builds a visually hidden live region for `level` and attaches it. */\n #createRegion(level: Level): HTMLElement {\n const region = document.createElement(\"div\");\n region.setAttribute(\"aria-live\", level);\n region.setAttribute(\"aria-atomic\", \"true\");\n visuallyHide(region);\n this.element.appendChild(region);\n return region;\n }\n\n /**\n * Removes and forgets every region this controller generated. Authored targets\n * belong to the consumer and are left untouched. Forgetting them is what keeps\n * the live page working after a snapshot rewind: the next announcement finds an\n * empty map and materialises a fresh region.\n */\n #removeGenerated(): void {\n for (const region of this.#generated.values()) {\n region.remove();\n }\n this.#generated.clear();\n }\n\n /**\n * Announces a message. Reads the text from a Stimulus action param\n * (`message`, plus optional `assertive`) for attribute-only triggers, falling\n * back to a CustomEvent `detail` when the same handler is wired to an event.\n * An empty/non-string message is ignored so untrusted payloads cannot blank\n * the region.\n */\n announce(event: Event): void {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = params?.message;\n const message =\n typeof fromParam === \"string\" && fromParam.length > 0\n ? fromParam\n : this.#messageFromDetail((event as CustomEvent<unknown>).detail);\n if (!message) return;\n\n const assertive =\n params?.assertive === true ||\n this.#assertiveFromDetail((event as CustomEvent<unknown>).detail);\n this.#announce(message, assertive);\n }\n\n /**\n * Queues `message` for its politeness and arms the drain.\n *\n * Queuing is what makes a burst audible: assistive tech announces the changes it\n * observes, so several messages written into one region within a single task are\n * one change and only the last is read.\n */\n #announce(message: string, assertive: boolean): void {\n const level: Level = assertive ? \"assertive\" : \"polite\";\n const queue = this.#queues.get(level);\n if (queue) {\n queue.push(message);\n } else {\n this.#queues.set(level, [message]);\n }\n this.#scheduleDrain(level);\n }\n\n /** Arms one drain pass for `level`; further messages ride the pass already armed. */\n #scheduleDrain(level: Level): void {\n if (this.#draining.has(level)) return;\n this.#draining.add(level);\n this.#timers.set(() => {\n this.#draining.delete(level);\n this.#drain(level);\n }, 0);\n }\n\n /**\n * Writes one queued message, then arms the next pass while the queue holds more.\n *\n * Two steps take a whole pass without consuming the message: materialising a\n * region (it has to be in the accessibility tree before the text arrives) and\n * emptying a region that already holds this exact text (an unchanged node is not\n * re-read, so `dedupeReannounce` clears first and writes on the following pass).\n */\n #drain(level: Level): void {\n const queue = this.#queues.get(level);\n const message = queue?.[0];\n if (queue === undefined || message === undefined) return;\n\n if (this.#reconcileRegions()) {\n this.#scheduleDrain(level);\n return;\n }\n\n const region = this.#regionFor(level);\n if (this.dedupeReannounceValue && region.textContent === message) {\n this.#cancelPending(region);\n region.textContent = \"\";\n this.#scheduleDrain(level);\n return;\n }\n\n queue.shift();\n this.#cancelPending(region);\n region.textContent = message;\n this.#scheduleClear(region, message);\n if (queue.length > 0) this.#scheduleDrain(level);\n }\n\n /** Clears the region after `clearAfter` ms, unless a newer message replaced it. */\n #scheduleClear(region: HTMLElement, message: string): void {\n if (this.clearAfterValue <= 0) return;\n this.#schedule(\n region,\n () => {\n if (region.textContent === message) region.textContent = \"\";\n },\n this.clearAfterValue,\n );\n }\n\n /**\n * Arms `region`'s single pending timer. Callers reach here with the slot\n * already free — `#drain` releases it before writing, and a fired timer clears its own\n * entry below — so this does not cancel again.\n */\n #schedule(region: HTMLElement, callback: () => void, delay: number): void {\n const id = this.#timers.set(() => {\n // Drop the id as it fires: platform timer ids are recycled, and a stale\n // one left here could later cancel the *other* region's live timer.\n this.#pending.delete(region);\n callback();\n }, delay);\n this.#pending.set(region, id);\n }\n\n /** Releases `region`'s pending timer, if it has one. */\n #cancelPending(region: HTMLElement): void {\n // `SafeTimeout.clear` ignores an id it does not own, so the \"no pending\n // timer\" case needs no branch of its own.\n this.#timers.clear(this.#pending.get(region) ?? -1);\n this.#pending.delete(region);\n }\n\n /**\n * Resolves the live region for a politeness level.\n *\n * The remembered stand-in is used only while it is still in the document: a morph\n * can drop it, and writing into the detached node would announce nothing at all.\n */\n #regionFor(level: Level): HTMLElement {\n if (level === \"assertive\" && this.hasAssertiveTarget) return this.assertiveTarget;\n if (level === \"polite\" && this.hasPoliteTarget) return this.politeTarget;\n\n const existing = this.#generated.get(level);\n if (existing?.isConnected) return existing;\n\n const region = this.#createRegion(level);\n this.#generated.set(level, region);\n return region;\n }\n\n /**\n * Restores the announceable initial state for the snapshot Turbo is about to\n * take: queued and displayed messages go, generated regions go, and the live\n * page — which keeps running when a visit is aborted — gets its regions back on\n * the next task, after the clone.\n */\n #rewindForCache(): void {\n this.#queues.clear();\n this.#draining.clear();\n this.#timers.clearAll();\n for (const level of LEVELS) {\n if (this.#hasTargetFor(level)) {\n const target = level === \"assertive\" ? this.assertiveTarget : this.politeTarget;\n target.textContent = \"\";\n }\n }\n this.#removeGenerated();\n this.#timers.set(() => this.#reconcileRegions(), 0);\n }\n\n /** Extracts a non-empty string `message` from a CustomEvent detail, else null. */\n #messageFromDetail(detail: unknown): string | null {\n if (detail && typeof detail === \"object\" && \"message\" in detail) {\n const value = (detail as Record<string, unknown>).message;\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Reads an `assertive === true` flag from a CustomEvent detail (default polite). */\n #assertiveFromDetail(detail: unknown): boolean {\n return (\n !!detail &&\n typeof detail === \"object\" &&\n (detail as Record<string, unknown>).assertive === true\n );\n }\n}\n\n/**\n * Applies the canonical visually-hidden (\"sr-only\") inline style to a generated\n * live region so its text is announced without being seen. Inline so the library\n * stays self-contained when the consumer provides no target/CSS of its own.\n *\n * Pure (no `this`); exported for direct unit testing.\n */\nexport function visuallyHide(node: HTMLElement): void {\n const { style } = node;\n style.position = \"absolute\";\n style.width = \"1px\";\n style.height = \"1px\";\n style.margin = \"-1px\";\n style.padding = \"0\";\n style.border = \"0\";\n style.overflow = \"hidden\";\n style.clip = \"rect(0 0 0 0)\";\n style.clipPath = \"inset(50%)\";\n style.whiteSpace = \"nowrap\";\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/composition_tracker.ts","../../src/utils/safe_timeout.ts","../../src/controllers/auto_submit_controller.ts"],"names":[],"mappings":"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","file":"auto_submit_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless **debounced auto-submit** for forms (no dedicated APG pattern). Submits\n * the form a configurable delay after `input`/`change`, so Rails search/filter\n * forms refresh via Turbo without a submit button.\n *\n * Markup contract (identifier: `stimeo--auto-submit`):\n * <form data-controller=\"stimeo--auto-submit\"\n * data-stimeo--auto-submit-debounce-value=\"300\"\n * data-action=\"input->stimeo--auto-submit#submit\n * change->stimeo--auto-submit#submit\">\n * <input type=\"search\" name=\"q\">\n * </form>\n *\n * <!-- Or with the form as a target nested under the controller element: -->\n * <div data-controller=\"stimeo--auto-submit\">\n * <form data-stimeo--auto-submit-target=\"form\"> … </form>\n * </div>\n *\n * `submit` dispatches `{ trigger }`; `done` dispatches `{ message? }`.\n * `reconcile` dispatches `{}` when the Turbo cache rewind drops a submission\n * that was pending or in flight — `done` would claim a response arrived.\n *\n * @remarks\n * Behavior only — it owns *triggering* the submit (debounce + `requestSubmit`),\n * never the submit itself (Turbo / native form submission) or validation. It\n * **never moves focus** (WCAG 2.2 3.2.2 / 4.1.3): auto-submitting must not yank the\n * caret out of the field. While a result swap is silent for screen-reader users,\n * setting `announce` bridges the completion to the shared `stimeo--announcer`\n * as a safety net; apps can also listen for `stimeo--auto-submit:done`\n * and announce richer text themselves. `aria-busy` marks the in-flight window and\n * `data-auto-submit-pending` the debounce window, for consumer CSS; both hooks are\n * rewound just before Turbo caches the page so they never burn into a snapshot.\n * The `turbo:submit-end`/composition subscriptions follow the `form` target as it\n * is added, replaced, or removed at runtime; unbinding a form drops its pending\n * debounced submit, since that submit described a form that is going away. With\n * no resolvable form (no target and a non-`<form>` root) the controller is inert.\n * During IME composition it holds the submit until `compositionend` (the confirmed\n * conversion) so it does not fire on each intermediate keystroke. The debounce\n * timer and the `turbo:submit-end`/composition listeners are torn down on\n * `disconnect()`.\n */\nexport class AutoSubmitController extends Controller<HTMLElement> {\n static override targets = [\"form\"];\n static override values = {\n debounce: { type: Number, default: 300 },\n on: { type: String, default: \"input change\" },\n announce: { type: Boolean, default: false },\n message: { type: String, default: \"\" },\n };\n static actions = [\"submit\"] as const;\n static events = [\"submit\", \"done\", \"reconcile\"] as const;\n\n declare readonly formTarget: HTMLFormElement;\n declare readonly hasFormTarget: boolean;\n\n declare debounceValue: number;\n declare onValue: string;\n declare announceValue: boolean;\n declare messageValue: string;\n\n /** Debounce timer registry; one `clearAll()` in disconnect tears it down. */\n readonly #timers = new SafeTimeout();\n /** Id of the pending debounce timer, so a new keystroke can reset it. */\n #pendingId = 0;\n /** The form the listeners are attached to; target callbacks rebind it. */\n #boundForm: HTMLFormElement | null = null;\n /** Rewinds the transient state hooks just before Turbo snapshots the page. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Clears `aria-busy` and emits completion once Turbo finishes the submit. */\n readonly #onSubmitEnd = (): void => {\n this.#boundForm?.removeAttribute(\"aria-busy\");\n const message = this.messageValue;\n this.dispatch(\"done\", { detail: { message: message || undefined } });\n // Bridge the silent result swap to the shared Announcer so SR users hear it.\n if (this.announceValue && message) {\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n };\n\n /** Owns delegated IME lifecycle state and submits confirmed input text. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n if (this.#triggers(\"input\")) this.#schedule((event.target as HTMLElement | null) ?? null);\n },\n });\n\n override connect(): void {\n this.#beforeCache.activate();\n this.#bindForm(this.#resolveForm());\n }\n\n override disconnect(): void {\n this.#beforeCache.deactivate();\n this.#bindForm(null);\n this.#composition.disconnect();\n this.#timers.clearAll();\n this.#pendingId = 0;\n }\n\n /** Follows a `form` target added (or swapped in) at runtime. */\n formTargetConnected(): void {\n this.#bindForm(this.#resolveForm());\n }\n\n /** Follows a `form` target removed (or swapped out) at runtime. */\n formTargetDisconnected(): void {\n this.#bindForm(this.#resolveForm());\n }\n\n /**\n * Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an\n * allowlist so a configured subset (e.g. only `change`) is honored even when both\n * are bound in markup. Coalesces rapid events into a single `requestSubmit`.\n */\n submit(event: Event): void {\n if (!this.#triggers(event.type)) return;\n // Ignore `input` events fired mid-IME-composition (e.g. typing kana before the\n // Japanese conversion is confirmed); the confirmed text submits on\n // `compositionend` and the browser's final post-composition `input`.\n if (event.type === \"input\" && this.#composition.isComposing(event as InputEvent)) return;\n this.#schedule((event.target as HTMLElement | null) ?? null);\n }\n\n /** Schedules (and coalesces) the debounced submit for the given trigger. */\n #schedule(trigger: HTMLElement | null): void {\n const form = this.#boundForm;\n if (!form) return;\n form.setAttribute(\"data-auto-submit-pending\", \"true\");\n this.#cancelPending();\n\n this.#pendingId = this.#timers.set(() => {\n this.#pendingId = 0;\n form.removeAttribute(\"data-auto-submit-pending\");\n this.dispatch(\"submit\", { detail: { trigger } });\n // `requestSubmit()` runs native constraint validation. If the form is\n // invalid the actual submit never happens — the browser blocks it (or, when\n // a `stimeo--form-validation` set `novalidate`, that controller cancels the\n // submit) — so no `turbo:submit-end` arrives to clear `aria-busy`. Only mark\n // the form busy when the submit will really proceed; still call\n // `requestSubmit()` either way so the validation surfaces to the user.\n if (form.checkValidity()) {\n form.setAttribute(\"aria-busy\", \"true\");\n }\n form.requestSubmit();\n }, this.debounceValue);\n }\n\n /** Cancels the pending debounced submit, if any (`clear` no-ops on unknown ids). */\n #cancelPending(): void {\n this.#timers.clear(this.#pendingId);\n this.#pendingId = 0;\n }\n\n /**\n * Replaces the subscribed form symmetrically. The outgoing form loses the\n * `turbo:submit-end`/composition listeners, its pending hook, and any pending\n * debounced submit (which described the outgoing form). An in-flight `aria-busy`\n * is left for `turbo:submit-end` or the pre-cache rewind — removing it here\n * would wipe a legitimately in-progress submission.\n */\n #bindForm(form: HTMLFormElement | null): void {\n if (form === this.#boundForm) return;\n const previous = this.#boundForm;\n if (previous) {\n this.#cancelPending();\n previous.removeAttribute(\"data-auto-submit-pending\");\n previous.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.unobserve(previous);\n }\n this.#boundForm = form;\n if (!form) return;\n form.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.observe(form);\n }\n\n /** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */\n #resolveForm(): HTMLFormElement | null {\n if (this.hasFormTarget) return this.formTarget;\n return this.element instanceof HTMLFormElement ? this.element : null;\n }\n\n /** Returns the transient state hooks to their initial (absent) state. */\n #rewindForCache(): void {\n const inProgress = this.#pendingId !== 0 || this.#boundForm?.hasAttribute(\"aria-busy\") === true;\n this.#cancelPending();\n this.#boundForm?.removeAttribute(\"data-auto-submit-pending\");\n this.#boundForm?.removeAttribute(\"aria-busy\");\n if (inProgress) this.dispatch(\"reconcile\", { detail: {} });\n }\n\n /** Whether `type` is one of the whitespace-separated event types in `on`. */\n #triggers(type: string): boolean {\n return this.onValue.split(/\\s+/).filter(Boolean).includes(type);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/composition_tracker.ts","../../src/utils/safe_timeout.ts","../../src/controllers/auto_submit_controller.ts"],"names":[],"mappings":"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","file":"auto_submit_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","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless **debounced auto-submit** for forms (no dedicated APG pattern). Submits\n * the form a configurable delay after `input`/`change`, so Rails search/filter\n * forms refresh via Turbo without a submit button.\n *\n * Markup contract (identifier: `stimeo--auto-submit`):\n * <form data-controller=\"stimeo--auto-submit\"\n * data-stimeo--auto-submit-debounce-value=\"300\"\n * data-action=\"input->stimeo--auto-submit#submit\n * change->stimeo--auto-submit#submit\">\n * <input type=\"search\" name=\"q\">\n * </form>\n *\n * <!-- Or with the form as a target nested under the controller element: -->\n * <div data-controller=\"stimeo--auto-submit\">\n * <form data-stimeo--auto-submit-target=\"form\"> … </form>\n * </div>\n *\n * `submit` dispatches `{ trigger }`; `done` dispatches `{ message? }`.\n * `reconcile` dispatches `{}` when the Turbo cache rewind drops a submission\n * that was pending or in flight — `done` would claim a response arrived.\n *\n * @remarks\n * Behavior only — it owns *triggering* the submit (debounce + `requestSubmit`),\n * never the submit itself (Turbo / native form submission) or validation. It\n * **never moves focus** (WCAG 2.2 3.2.2 / 4.1.3): auto-submitting must not yank the\n * caret out of the field. While a result swap is silent for screen-reader users,\n * setting `announce` bridges the completion to the shared `stimeo--announcer`\n * as a safety net; apps can also listen for `stimeo--auto-submit:done`\n * and announce richer text themselves. `aria-busy` marks the in-flight window and\n * `data-auto-submit-pending` the debounce window, for consumer CSS; both hooks are\n * rewound just before Turbo caches the page so they never burn into a snapshot.\n * The `turbo:submit-end`/composition subscriptions follow the `form` target as it\n * is added, replaced, or removed at runtime; unbinding a form drops its pending\n * debounced submit, since that submit described a form that is going away. With\n * no resolvable form (no target and a non-`<form>` root) the controller is inert.\n * During IME composition it holds the submit until `compositionend` (the confirmed\n * conversion) so it does not fire on each intermediate keystroke. The debounce\n * timer and the `turbo:submit-end`/composition listeners are torn down on\n * `disconnect()`.\n */\nexport class AutoSubmitController extends Controller<HTMLElement> {\n static override targets = [\"form\"];\n static override values = {\n debounce: { type: Number, default: 300 },\n on: { type: String, default: \"input change\" },\n announce: { type: Boolean, default: false },\n message: { type: String, default: \"\" },\n };\n static actions = [\"submit\"] as const;\n static events = [\"submit\", \"done\", \"reconcile\"] as const;\n\n declare readonly formTarget: HTMLFormElement;\n declare readonly hasFormTarget: boolean;\n\n declare debounceValue: number;\n declare onValue: string;\n declare announceValue: boolean;\n declare messageValue: string;\n\n /** Debounce timer registry; one `clearAll()` in disconnect tears it down. */\n readonly #timers = new SafeTimeout();\n /** Id of the pending debounce timer, so a new keystroke can reset it. */\n #pendingId = 0;\n /** The form the listeners are attached to; target callbacks rebind it. */\n #boundForm: HTMLFormElement | null = null;\n /** Rewinds the transient state hooks just before Turbo snapshots the page. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** Clears `aria-busy` and emits completion once Turbo finishes the submit. */\n readonly #onSubmitEnd = (): void => {\n this.#boundForm?.removeAttribute(\"aria-busy\");\n const message = this.messageValue;\n this.dispatch(\"done\", { detail: { message: message || undefined } });\n // Bridge the silent result swap to the shared Announcer so SR users hear it.\n if (this.announceValue && message) {\n window.dispatchEvent(new CustomEvent(\"stimeo--announcer:announce\", { detail: { message } }));\n }\n };\n\n /** Owns delegated IME lifecycle state and submits confirmed input text. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n if (this.#triggers(\"input\")) this.#schedule((event.target as HTMLElement | null) ?? null);\n },\n });\n\n override connect(): void {\n this.#beforeCache.activate();\n this.#bindForm(this.#resolveForm());\n }\n\n override disconnect(): void {\n this.#beforeCache.deactivate();\n this.#bindForm(null);\n this.#composition.disconnect();\n this.#timers.clearAll();\n this.#pendingId = 0;\n }\n\n /** Follows a `form` target added (or swapped in) at runtime. */\n formTargetConnected(): void {\n this.#bindForm(this.#resolveForm());\n }\n\n /** Follows a `form` target removed (or swapped out) at runtime. */\n formTargetDisconnected(): void {\n this.#bindForm(this.#resolveForm());\n }\n\n /**\n * Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an\n * allowlist so a configured subset (e.g. only `change`) is honored even when both\n * are bound in markup. Coalesces rapid events into a single `requestSubmit`.\n */\n submit(event: Event): void {\n if (!this.#triggers(event.type)) return;\n // Ignore `input` events fired mid-IME-composition (e.g. typing kana before the\n // Japanese conversion is confirmed); the confirmed text submits on\n // `compositionend` and the browser's final post-composition `input`.\n if (event.type === \"input\" && this.#composition.isComposing(event as InputEvent)) return;\n this.#schedule((event.target as HTMLElement | null) ?? null);\n }\n\n /** Schedules (and coalesces) the debounced submit for the given trigger. */\n #schedule(trigger: HTMLElement | null): void {\n const form = this.#boundForm;\n if (!form) return;\n form.setAttribute(\"data-auto-submit-pending\", \"true\");\n this.#cancelPending();\n\n this.#pendingId = this.#timers.set(() => {\n this.#pendingId = 0;\n form.removeAttribute(\"data-auto-submit-pending\");\n this.dispatch(\"submit\", { detail: { trigger } });\n // `requestSubmit()` runs native constraint validation. If the form is\n // invalid the actual submit never happens — the browser blocks it (or, when\n // a `stimeo--form-validation` set `novalidate`, that controller cancels the\n // submit) — so no `turbo:submit-end` arrives to clear `aria-busy`. Only mark\n // the form busy when the submit will really proceed; still call\n // `requestSubmit()` either way so the validation surfaces to the user.\n if (form.checkValidity()) {\n form.setAttribute(\"aria-busy\", \"true\");\n }\n form.requestSubmit();\n }, this.debounceValue);\n }\n\n /** Cancels the pending debounced submit, if any (`clear` no-ops on unknown ids). */\n #cancelPending(): void {\n this.#timers.clear(this.#pendingId);\n this.#pendingId = 0;\n }\n\n /**\n * Replaces the subscribed form symmetrically. The outgoing form loses the\n * `turbo:submit-end`/composition listeners, its pending hook, and any pending\n * debounced submit (which described the outgoing form). An in-flight `aria-busy`\n * is left for `turbo:submit-end` or the pre-cache rewind — removing it here\n * would wipe a legitimately in-progress submission.\n */\n #bindForm(form: HTMLFormElement | null): void {\n if (form === this.#boundForm) return;\n const previous = this.#boundForm;\n if (previous) {\n this.#cancelPending();\n previous.removeAttribute(\"data-auto-submit-pending\");\n previous.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.unobserve(previous);\n }\n this.#boundForm = form;\n if (!form) return;\n form.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n this.#composition.observe(form);\n }\n\n /** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */\n #resolveForm(): HTMLFormElement | null {\n if (this.hasFormTarget) return this.formTarget;\n return this.element instanceof HTMLFormElement ? this.element : null;\n }\n\n /** Returns the transient state hooks to their initial (absent) state. */\n #rewindForCache(): void {\n const inProgress = this.#pendingId !== 0 || this.#boundForm?.hasAttribute(\"aria-busy\") === true;\n this.#cancelPending();\n this.#boundForm?.removeAttribute(\"data-auto-submit-pending\");\n this.#boundForm?.removeAttribute(\"aria-busy\");\n if (inProgress) this.dispatch(\"reconcile\", { detail: {} });\n }\n\n /** Whether `type` is one of the whitespace-separated event types in `on`. */\n #triggers(type: string): boolean {\n return this.onValue.split(/\\s+/).filter(Boolean).includes(type);\n }\n}\n"]}