stimeo-ui 0.13.0 → 0.15.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 (143) hide show
  1. package/CHANGELOG.md +73 -5
  2. package/dist/cable/index.d.ts +1 -1
  3. package/dist/cable/index.js +60 -14
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +1 -1
  6. package/dist/controllers/accordion_controller.js.map +1 -1
  7. package/dist/controllers/announcer_controller.js +88 -61
  8. package/dist/controllers/announcer_controller.js.map +1 -1
  9. package/dist/controllers/auto_submit_controller.js +40 -5
  10. package/dist/controllers/auto_submit_controller.js.map +1 -1
  11. package/dist/controllers/breadcrumb_controller.js +29 -3
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +20 -4
  14. package/dist/controllers/calendar_controller.js +51 -10
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.js +8 -6
  17. package/dist/controllers/carousel_controller.js.map +1 -1
  18. package/dist/controllers/checkbox_controller.js +27 -2
  19. package/dist/controllers/checkbox_controller.js.map +1 -1
  20. package/dist/controllers/clipboard_controller.d.ts +1 -1
  21. package/dist/controllers/clipboard_controller.js.map +1 -1
  22. package/dist/controllers/collapsible_controller.d.ts +1 -1
  23. package/dist/controllers/collapsible_controller.js.map +1 -1
  24. package/dist/controllers/conditional_fields_controller.js +36 -7
  25. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  26. package/dist/controllers/count_up_controller.js.map +1 -1
  27. package/dist/controllers/currency_input_controller.d.ts +4 -2
  28. package/dist/controllers/currency_input_controller.js +77 -25
  29. package/dist/controllers/currency_input_controller.js.map +1 -1
  30. package/dist/controllers/date_range_picker_controller.d.ts +10 -3
  31. package/dist/controllers/date_range_picker_controller.js +43 -10
  32. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  33. package/dist/controllers/dialog_controller.d.ts +2 -2
  34. package/dist/controllers/dialog_controller.js.map +1 -1
  35. package/dist/controllers/direct_upload_controller.js +21 -10
  36. package/dist/controllers/direct_upload_controller.js.map +1 -1
  37. package/dist/controllers/dirty_form_controller.js +32 -12
  38. package/dist/controllers/dirty_form_controller.js.map +1 -1
  39. package/dist/controllers/dismissible_controller.d.ts +1 -1
  40. package/dist/controllers/dismissible_controller.js.map +1 -1
  41. package/dist/controllers/dropdown_controller.d.ts +1 -1
  42. package/dist/controllers/dropdown_controller.js.map +1 -1
  43. package/dist/controllers/flash_controller.js +145 -43
  44. package/dist/controllers/flash_controller.js.map +1 -1
  45. package/dist/controllers/form_field_controller.js +7 -1
  46. package/dist/controllers/form_field_controller.js.map +1 -1
  47. package/dist/controllers/frame_loading_controller.js +29 -6
  48. package/dist/controllers/frame_loading_controller.js.map +1 -1
  49. package/dist/controllers/highlight_controller.js +63 -23
  50. package/dist/controllers/highlight_controller.js.map +1 -1
  51. package/dist/controllers/input_mask_controller.d.ts +3 -2
  52. package/dist/controllers/input_mask_controller.js +33 -12
  53. package/dist/controllers/input_mask_controller.js.map +1 -1
  54. package/dist/controllers/intersection_controller.js +10 -5
  55. package/dist/controllers/intersection_controller.js.map +1 -1
  56. package/dist/controllers/lazy_frame_controller.js +10 -5
  57. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  58. package/dist/controllers/local_time_controller.d.ts +3 -2
  59. package/dist/controllers/local_time_controller.js +43 -11
  60. package/dist/controllers/local_time_controller.js.map +1 -1
  61. package/dist/controllers/masonry_controller.d.ts +4 -4
  62. package/dist/controllers/masonry_controller.js +32 -14
  63. package/dist/controllers/masonry_controller.js.map +1 -1
  64. package/dist/controllers/menu_controller.js +10 -2
  65. package/dist/controllers/menu_controller.js.map +1 -1
  66. package/dist/controllers/menubar_controller.js +10 -2
  67. package/dist/controllers/menubar_controller.js.map +1 -1
  68. package/dist/controllers/navigation_menu_controller.js +8 -2
  69. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  70. package/dist/controllers/optimistic_controller.js +28 -4
  71. package/dist/controllers/optimistic_controller.js.map +1 -1
  72. package/dist/controllers/otp_controller.js +23 -10
  73. package/dist/controllers/otp_controller.js.map +1 -1
  74. package/dist/controllers/overflow_indicator_controller.js +31 -8
  75. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  76. package/dist/controllers/overflow_menu_controller.js +39 -4
  77. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  78. package/dist/controllers/password_reveal_controller.d.ts +1 -1
  79. package/dist/controllers/password_reveal_controller.js.map +1 -1
  80. package/dist/controllers/pointer_drag_controller.js +10 -3
  81. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  82. package/dist/controllers/portal_controller.js +21 -10
  83. package/dist/controllers/portal_controller.js.map +1 -1
  84. package/dist/controllers/radio_group_controller.js +11 -3
  85. package/dist/controllers/radio_group_controller.js.map +1 -1
  86. package/dist/controllers/read_more_controller.d.ts +1 -1
  87. package/dist/controllers/read_more_controller.js +31 -5
  88. package/dist/controllers/read_more_controller.js.map +1 -1
  89. package/dist/controllers/reading_progress_controller.js +65 -19
  90. package/dist/controllers/reading_progress_controller.js.map +1 -1
  91. package/dist/controllers/relative_time_controller.js +40 -9
  92. package/dist/controllers/relative_time_controller.js.map +1 -1
  93. package/dist/controllers/reset_before_cache_controller.js +23 -10
  94. package/dist/controllers/reset_before_cache_controller.js.map +1 -1
  95. package/dist/controllers/resizable_controller.js +7 -2
  96. package/dist/controllers/resizable_controller.js.map +1 -1
  97. package/dist/controllers/roving_controller.js +7 -3
  98. package/dist/controllers/roving_controller.js.map +1 -1
  99. package/dist/controllers/scroll_area_controller.js +70 -26
  100. package/dist/controllers/scroll_area_controller.js.map +1 -1
  101. package/dist/controllers/scroll_restore_controller.js +37 -16
  102. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  103. package/dist/controllers/scroll_visibility_controller.d.ts +1 -1
  104. package/dist/controllers/scroll_visibility_controller.js +71 -42
  105. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  106. package/dist/controllers/scrollspy_controller.d.ts +1 -1
  107. package/dist/controllers/scrollspy_controller.js +76 -32
  108. package/dist/controllers/scrollspy_controller.js.map +1 -1
  109. package/dist/controllers/smart_sticky_header_controller.js +72 -37
  110. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  111. package/dist/controllers/stick_to_bottom_controller.js +29 -3
  112. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  113. package/dist/controllers/sticky_observer_controller.js +10 -5
  114. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  115. package/dist/controllers/submit_once_controller.js +29 -6
  116. package/dist/controllers/submit_once_controller.js.map +1 -1
  117. package/dist/controllers/switch_controller.d.ts +1 -1
  118. package/dist/controllers/switch_controller.js.map +1 -1
  119. package/dist/controllers/tabs_controller.d.ts +1 -1
  120. package/dist/controllers/tabs_controller.js.map +1 -1
  121. package/dist/controllers/textarea_autosize_controller.js +29 -3
  122. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  123. package/dist/controllers/theme_controller.d.ts +2 -3
  124. package/dist/controllers/theme_controller.js +23 -13
  125. package/dist/controllers/theme_controller.js.map +1 -1
  126. package/dist/controllers/toast_controller.d.ts +3 -0
  127. package/dist/controllers/toast_controller.js +136 -73
  128. package/dist/controllers/toast_controller.js.map +1 -1
  129. package/dist/controllers/toggle_group_controller.js +11 -3
  130. package/dist/controllers/toggle_group_controller.js.map +1 -1
  131. package/dist/controllers/toolbar_controller.js +7 -3
  132. package/dist/controllers/toolbar_controller.js.map +1 -1
  133. package/dist/controllers/transition_controller.js.map +1 -1
  134. package/dist/index.js +629 -597
  135. package/dist/index.js.map +1 -1
  136. package/dist/inspector/cli.d.ts +38 -1
  137. package/dist/inspector/cli.js +80 -7
  138. package/dist/inspector/cli.js.map +1 -1
  139. package/dist/inspector/cli_bin.js +103 -7
  140. package/dist/inspector/cli_bin.js.map +1 -1
  141. package/dist/inspector/examples.json +5 -5
  142. package/dist/inspector/manifest.json +131 -3
  143. package/package.json +9 -11
@@ -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.\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/composition_tracker.ts","../../src/utils/listener_set.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 * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\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 * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, so `dispose()` belongs exactly where\n * the `removeEventListener` block stood. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\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 #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { announce } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { ListenerSet } from \"../utils/listener_set\";\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 /** Listeners that live with the bound form rather than with the connection. */\n readonly #formListeners = new ListenerSet();\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) announce(message);\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 this.#formListeners.dispose();\n this.#composition.unobserve(previous);\n }\n this.#boundForm = form;\n if (!form) return;\n this.#formListeners.add(form, \"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"]}
@@ -63,10 +63,15 @@ var LayoutObserver = class {
63
63
  #resizeObserverFactory;
64
64
  #resizeObserver = null;
65
65
  #observingViewport = false;
66
+ #loadContainer = null;
66
67
  /** Stable bound handler so add/removeEventListener target the same reference. */
67
68
  #handleViewportResize = () => {
68
69
  this.#callback();
69
70
  };
71
+ /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
72
+ #handleDescendantLoad = () => {
73
+ this.#callback();
74
+ };
70
75
  constructor(callback, options = {}) {
71
76
  this.#callback = callback;
72
77
  this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
@@ -102,14 +107,35 @@ var LayoutObserver = class {
102
107
  window.removeEventListener("resize", this.#handleViewportResize);
103
108
  }
104
109
  /**
105
- * Releases every observation: disconnects the {@link ResizeObserver} and
106
- * removes the viewport listener. Safe to call multiple times. Call this from a
107
- * controller's `disconnect()`.
110
+ * Starts reporting a `load` from anywhere inside `container` — an image or a
111
+ * frame settling changes the box it sits in, and it measures as zero high until
112
+ * then. `load` does not bubble, so the subscription is a capture-phase listener
113
+ * on the container itself and nothing the caller spells.
114
+ *
115
+ * **One container at a time.** A further call moves the observation, so a widget
116
+ * whose content element is swapped at runtime releases the element it let go by
117
+ * naming the new one — there is no second place for the release to drift from.
118
+ */
119
+ observeDescendantLoads(container) {
120
+ this.unobserveDescendantLoads();
121
+ this.#loadContainer = container;
122
+ container.addEventListener("load", this.#handleDescendantLoad, true);
123
+ }
124
+ /** Stops reporting descendant loads without affecting element or viewport observation. */
125
+ unobserveDescendantLoads() {
126
+ this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
127
+ this.#loadContainer = null;
128
+ }
129
+ /**
130
+ * Releases every observation: disconnects the {@link ResizeObserver} and removes
131
+ * the viewport and descendant-load listeners. Safe to call multiple times. Call
132
+ * this from a controller's `disconnect()`.
108
133
  */
109
134
  disconnect() {
110
135
  this.#resizeObserver?.disconnect();
111
136
  this.#resizeObserver = null;
112
137
  this.unobserveViewport();
138
+ this.unobserveDescendantLoads();
113
139
  }
114
140
  };
115
141
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\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 contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: `#measureOverflow` clears it on every measurement to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when the disclosure set gains or loses a member. The set is a\n * precondition for collapsing at all, so it needs the same watching the list\n * and items get: without these callbacks a swap that breaks or completes the\n * set would only take effect at the next resize or list mutation, which may\n * never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and `#render` only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/blur_deferral.ts","../../src/utils/layout_observer.ts","../../src/controllers/breadcrumb_controller.ts"],"names":[],"mappings":"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","file":"breadcrumb_controller.js","sourcesContent":["/**\n * Shared bookkeeping for updates that must wait until an element loses focus.\n *\n * A control that becomes redundant while the user is standing on it cannot be\n * removed from the page immediately: hiding a focused element (or turning it\n * into a native `disabled` one) drops focus to `<body>`, stranding a keyboard\n * user mid-widget. The fix every affected controller reached for is the same —\n * keep the element reachable, hold the destructive update, listen for `blur`,\n * and apply it then.\n *\n * {@link BlurDeferral} owns *only* the registry part of that: which elements are\n * currently holding an update back, attaching and detaching the `blur` listener,\n * and guaranteeing teardown. It is deliberately **policy-free** — what the\n * interim state looks like (`aria-disabled`, an ownership marker, nothing at\n * all), and what the deferred update actually is, stay in the controller, which\n * receives the element back through its release callback.\n *\n * Three contracts are worth stating up front, because each one is a hazard the\n * consumers would otherwise hit:\n *\n * - **The release callback fires only on a real `blur`.** `release()` /\n * `releaseAll()` *cancel* a deferral; they do not complete it. Conflating the\n * two makes `overflow-indicator` disable a button that just left the boundary.\n * - **`elements` is a snapshot.** Iterating the live key set while releasing\n * inside the loop is the failure this registry exists to prevent.\n * - **The entry is detached before the callback runs**, so a controller that\n * decides the update is still unsafe can simply defer again.\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 contracts belong on the members.\n *\n * @example\n * ```ts\n * readonly #deferred = new BlurDeferral<HTMLElement>((trigger) => {\n * if (this.#connected) this.#evaluate();\n * });\n *\n * #hide(trigger: HTMLElement) {\n * if (document.activeElement === trigger) {\n * this.#deferred.deferOnly(trigger); // stays visible until blur\n * return;\n * }\n * this.#deferred.releaseAll();\n * trigger.hidden = true;\n * }\n *\n * disconnect() {\n * this.#deferred.releaseAll();\n * }\n * ```\n */\nexport class BlurDeferral<T extends HTMLElement = HTMLElement> {\n /** Elements currently holding an update back, mapped to their `blur` listener. */\n readonly #pending = new Map<T, () => void>();\n /** Called after a pending element blurs and has been detached. */\n readonly #onRelease: (element: T) => void;\n\n /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */\n constructor(onRelease: (element: T) => void) {\n this.#onRelease = onRelease;\n }\n\n /** Number of elements currently holding an update back. */\n get size(): number {\n return this.#pending.size;\n }\n\n /** Snapshot of the pending elements, safe to iterate while releasing them. */\n get elements(): T[] {\n return [...this.#pending.keys()];\n }\n\n /** Whether `element` is currently holding an update back. */\n has(element: T): boolean {\n return this.#pending.has(element);\n }\n\n /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */\n defer(element: T): void {\n if (this.#pending.has(element)) return;\n const onBlur = (): void => {\n this.#detach(element);\n this.#onRelease(element);\n };\n this.#pending.set(element, onBlur);\n element.addEventListener(\"blur\", onBlur);\n }\n\n /** Defers `element` as the only pending entry, cancelling any others. */\n deferOnly(element: T): void {\n for (const pending of this.elements) {\n if (pending !== element) this.#detach(pending);\n }\n this.defer(element);\n }\n\n /** Cancels `element`'s deferral without completing it; no-ops when not pending. */\n release(element: T): void {\n this.#detach(element);\n }\n\n /** Cancels every deferral without completing any of them. */\n releaseAll(): void {\n for (const element of this.elements) this.#detach(element);\n }\n\n /** Removes the `blur` listener for `element` and forgets it. */\n #detach(element: T): void {\n const onBlur = this.#pending.get(element);\n if (onBlur) element.removeEventListener(\"blur\", onBlur);\n this.#pending.delete(element);\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BlurDeferral } from \"../utils/blur_deferral\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Sub-pixel tolerance (px) applied to the overflow comparison.\n *\n * Fractional layout metrics (zoom, `border-box` rounding, fractional font\n * metrics) routinely make `scrollWidth` exceed `clientWidth` by a fraction of a\n * pixel on a trail that visually fits, which would collapse it for no reason.\n */\nconst OVERFLOW_EPSILON = 1;\n\n/**\n * Headless, accessible responsive breadcrumb behavior.\n *\n * Markup contract (identifier: `stimeo--breadcrumb`):\n * <nav data-controller=\"stimeo--breadcrumb\" aria-label=\"Breadcrumb\">\n * <ol data-stimeo--breadcrumb-target=\"list\">\n * <li><a href=\"/\">Home</a></li>\n * <li data-stimeo--breadcrumb-target=\"ellipsis\" hidden>\n * <button type=\"button\" aria-expanded=\"false\" aria-controls=\"bc-a bc-b\"\n * aria-label=\"Show full path\"\n * data-stimeo--breadcrumb-target=\"trigger\"\n * data-action=\"click->stimeo--breadcrumb#toggle\">…</button>\n * </li>\n * <li id=\"bc-a\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a\">Section A</a></li>\n * <li id=\"bc-b\" data-stimeo--breadcrumb-target=\"collapsible\"><a href=\"/a/b\">Sub B</a></li>\n * <li><a href=\"/a/b/c\" aria-current=\"page\">Item C</a></li>\n * </ol>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Breadcrumb** pattern. The base structure (`nav`\n * + `ol` + `aria-current=\"page\"`) lives in the markup; this controller adds the\n * responsive behavior: when the trail overflows its container it collapses the\n * author-marked middle items behind a disclosure (`…`) button, which expands\n * them back on demand.\n *\n * `toggle` dispatches `{ expanded: boolean }`.\n *\n * @remarks\n * Behavior only — the consumer owns separators (CSS) and the look. The items to\n * collapse are the author-marked `collapsible` targets (the source of truth),\n * placed between the leading and trailing items that must always stay visible.\n *\n * Behavior provided:\n * - Detects overflow via a shared layout observer (element + viewport resize) and a\n * `MutationObserver` on the list (content edits, Turbo Stream / morph item\n * swaps); the public `update` action re-measures on demand.\n * - While overflowing and not expanded, hides the `collapsible` items and shows\n * the `ellipsis` item; when it fits, shows everything, hides the ellipsis and\n * resets the expanded state (so a later collapse starts collapsed).\n * - The disclosure `trigger` toggles `aria-expanded` and the collapsed items,\n * dispatching `stimeo--breadcrumb:toggle`. `toggle` is a no-op (no state\n * change, no event) while the trail fits, so a hidden trigger can never be\n * left reporting `aria-expanded=\"true\"`.\n *\n * Layout requirements the behavior depends on:\n * - **The list must not wrap** (`white-space: nowrap` / `flex-wrap: nowrap`). A\n * wrapping list grows in height instead of scroll width, so\n * `scrollWidth === clientWidth` always holds and overflow is never detected.\n * - **The list width must follow its container** (no shrink-to-fit / intrinsic\n * width). Overflow is re-measured synchronously inside the resize\n * notification, so a list that widens to its content would never report\n * overflow. There is deliberately no debounce: the pass is one write → one\n * forced read → one write on a single element.\n * - Consumers must not neutralize `[hidden]` (e.g. `display: inline-flex` on the\n * items wins over the UA's `[hidden] { display: none }`); collapsing is\n * expressed exclusively through the `hidden` attribute.\n *\n * Ownership of `hidden`: this controller owns the attribute on an element **for as\n * long as that element is a `collapsible` target and the controller is connected**.\n * Outside that window it does not write it:\n * - lose the marker while connected → the element becomes an always-visible item\n * and the controller clears the `hidden` it owned;\n * - controller `disconnect()` → the collapsed state is left in the DOM on purpose,\n * because that DOM is what Turbo caches and restores.\n *\n * A consumer that also wants to own `hidden` on the same element (for a reason of\n * its own) is not supported: `#measureOverflow` clears it on every measurement to\n * read the expanded width.\n *\n * Focus safety: hiding the element the user is standing on would drop focus to\n * `<body>`. When a collapse would hide the focused item, focus moves to the\n * disclosure `trigger` first; when the ellipsis would be hidden while it holds\n * focus, the hide waits for `blur`.\n *\n * **Fail-safe when the disclosure is incomplete.** Collapsing needs all of\n * `list`, one `collapsible`, `ellipsis`, and `trigger`; miss any and the trail\n * degrades to a plain APG breadcrumb (every item visible, ellipsis hidden,\n * `aria-expanded=\"false\"`, `toggle` a silent no-op). Hiding the middle items\n * with no control that can reveal them puts the path out of reach for good — a\n * too-wide trail is the cheaper failure. Watched at runtime, so a swap that\n * breaks the set releases the items and one that completes it starts collapsing.\n */\nexport class BreadcrumbController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"collapsible\", \"ellipsis\", \"trigger\"];\n static actions = [\"toggle\", \"update\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly collapsibleTargets: HTMLElement[];\n declare readonly ellipsisTarget: HTMLElement;\n declare readonly triggerTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasEllipsisTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Guards target callbacks that can fire outside the connected window. */\n #connected = false;\n /** Whether the trail currently overflows its container. */\n #overflowing = false;\n /** Whether the user has expanded the collapsed items via the disclosure. */\n #expanded = false;\n /** The list element currently observed (resize + mutations), if any. */\n #observedList: HTMLElement | null = null;\n #listMutations: MutationObserver | null = null;\n readonly #layout = new LayoutObserver(() => this.update());\n\n readonly #onListMutation = (): void => {\n this.update();\n };\n\n /** Holds the ellipsis hide back while it contains focus; re-renders on blur. */\n readonly #deferredHide = new BlurDeferral(() => {\n this.#render();\n });\n\n /** Starts observing for overflow and renders the initial state. */\n override connect(): void {\n this.#connected = true;\n // The DOM is the source of truth on reconnect (Turbo cache restore / morph):\n // the trigger's `aria-expanded` is where the expanded state lives, so a trail\n // the user opened survives a back-navigation instead of silently re-collapsing.\n this.#expanded = this.#initialExpanded();\n this.#layout.observeViewport();\n this.#syncListObservation();\n this.update();\n }\n\n /** Releases the resize/mutation observation (Turbo navigation included). */\n override disconnect(): void {\n this.#connected = false;\n this.#deferredHide.releaseAll();\n this.#stopObservingList();\n this.#layout.disconnect();\n }\n\n listTargetConnected(): void {\n this.#resync();\n }\n\n listTargetDisconnected(): void {\n this.#resync();\n }\n\n collapsibleTargetConnected(): void {\n this.#resync();\n }\n\n /**\n * Re-measures when the disclosure set gains or loses a member. The set is a\n * precondition for collapsing at all, so it needs the same watching the list\n * and items get: without these callbacks a swap that breaks or completes the\n * set would only take effect at the next resize or list mutation, which may\n * never come.\n */\n ellipsisTargetConnected(): void {\n this.#resync();\n }\n\n ellipsisTargetDisconnected(): void {\n this.#resync();\n }\n\n triggerTargetConnected(): void {\n this.#resync();\n }\n\n triggerTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Hands `hidden` back when an element stops being a `collapsible` target.\n *\n * The marker is the source of truth for what may be collapsed, so an element\n * that loses it while the controller is live is an always-visible item again —\n * and `#render` only walks the *current* targets, so nothing else would ever\n * clear the `hidden` this controller put there.\n *\n * The `#connected` guard is load-bearing, not defensive. Stimulus fires this\n * callback for **every** target during teardown as well (`Context.disconnect()`\n * stops the target observer after `disconnect()` has run), and there the\n * collapsed DOM must survive untouched so Turbo's cache restores the trail in\n * the state the user left it (see the ownership note in {@link BreadcrumbController}).\n *\n * @param element - the element that just left the `collapsible` target set\n */\n collapsibleTargetDisconnected(element: HTMLElement): void {\n if (this.#connected) element.hidden = false;\n this.#resync();\n }\n\n /**\n * Expands or re-collapses the trail and dispatches `toggle`.\n *\n * No-op while the trail fits: there is nothing collapsed to reveal, and the\n * (hidden) trigger would otherwise keep a stale `aria-expanded=\"true\"` that\n * surfaces pre-expanded the next time the trail overflows. No event is\n * dispatched in that case.\n */\n toggle(): void {\n if (!this.#connected || !this.#overflowing) return;\n this.#expanded = !this.#expanded;\n this.#render();\n this.dispatch(\"toggle\", { detail: { expanded: this.#expanded } });\n }\n\n /**\n * Re-measures overflow and re-renders. Wired to resizes and list mutations;\n * exposed as an action so consumers can re-measure after a change the\n * observers cannot see (e.g. a web font swap that only alters text width).\n */\n update(): void {\n if (!this.#connected) return;\n this.#overflowing = this.#measureOverflow();\n if (!this.#overflowing) this.#expanded = false;\n this.#render();\n }\n\n /** Re-attaches the list observation (targets may have been swapped) and re-measures. */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncListObservation();\n this.update();\n }\n\n /** Reads the expanded state back from its DOM home (the trigger's `aria-expanded`). */\n #initialExpanded(): boolean {\n return this.hasTriggerTarget && this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /**\n * Points the resize + mutation observation at the current `list` target.\n *\n * The list is re-resolved rather than captured at connect so a target swapped\n * at runtime (Turbo Stream replacement of the `<ol>`) is observed too. Only\n * `childList` / `subtree` / `characterData` are watched — deliberately **not**\n * `attributes`, which the controller's own `hidden` writes would re-trigger.\n */\n #syncListObservation(): void {\n const next = this.hasListTarget ? this.listTarget : null;\n if (next === this.#observedList) return;\n\n this.#stopObservingList();\n if (!next) return;\n\n this.#observedList = next;\n this.#layout.observe(next);\n if (typeof MutationObserver !== \"undefined\") {\n this.#listMutations = new MutationObserver(this.#onListMutation);\n this.#listMutations.observe(next, { childList: true, subtree: true, characterData: true });\n }\n }\n\n #stopObservingList(): void {\n if (this.#observedList) this.#layout.unobserve(this.#observedList);\n this.#observedList = null;\n this.#listMutations?.disconnect();\n this.#listMutations = null;\n }\n\n /**\n * Measures overflow against the **fully expanded** layout so hiding items does\n * not make the condition oscillate. Reveals every item, then compares the list's\n * own scroll width to its own client width; `#render` immediately re-applies\n * the correct hidden state afterward.\n *\n * Both widths are read from the list element itself (not the host `nav`) so the\n * check is independent of any padding/border on the host — comparing against the\n * host's `clientWidth` would over-report the available space by its padding and\n * miss real overflow in a padded container.\n *\n * With no `collapsible` items there is nothing to collapse, so the trail is\n * never reported as overflowing (which also keeps the disclosure out of the\n * tab order — a button controlling nothing).\n *\n * The full disclosure set is a precondition: collapsing without an `ellipsis`\n * *and* a `trigger` would hide items behind a control that does not exist, so\n * an incomplete set reports \"not overflowing\" and `#render` degrades the trail\n * to a plain breadcrumb. Losing layout is recoverable; losing the path is not.\n */\n #measureOverflow(): boolean {\n if (!this.hasListTarget || this.collapsibleTargets.length === 0) return false;\n if (!this.hasEllipsisTarget || !this.hasTriggerTarget) return false;\n for (const item of this.collapsibleTargets) item.hidden = false;\n // No `hasEllipsisTarget` check: the line above already returned without one.\n this.ellipsisTarget.hidden = true;\n return this.listTarget.scrollWidth > this.listTarget.clientWidth + OVERFLOW_EPSILON;\n }\n\n /**\n * Applies the collapsed/expanded state to the items, ellipsis, and trigger.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const collapsed = this.#overflowing && !this.#expanded;\n const showEllipsis = this.#overflowing && this.collapsibleTargets.length > 0;\n\n // Decided *before* the writes below: once a focused item is hidden the browser\n // drops focus to <body> and the information is gone.\n const rescueFocus =\n collapsed &&\n this.hasTriggerTarget &&\n this.collapsibleTargets.some((i) => this.#holdsFocus(i));\n\n for (const item of this.collapsibleTargets) item.hidden = collapsed;\n if (this.hasEllipsisTarget) this.#applyEllipsisVisibility(showEllipsis);\n if (this.hasTriggerTarget) {\n // Never report expanded while the trail fits: nothing is collapsed to reveal.\n const expanded = this.#overflowing && this.#expanded;\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n if (rescueFocus) this.triggerTarget.focus();\n }\n\n /**\n * Shows or hides the ellipsis item, deferring a hide that would blur the user.\n *\n * Hiding the element that currently holds focus drops focus to `<body>`, so the\n * hide waits for that element's `blur` and is re-applied then (the shared\n * `BlurDeferral` registry).\n */\n #applyEllipsisVisibility(show: boolean): void {\n const ellipsis = this.ellipsisTarget;\n if (show) {\n this.#deferredHide.releaseAll();\n ellipsis.hidden = false;\n return;\n }\n\n const active = document.activeElement;\n if (active instanceof HTMLElement && ellipsis.contains(active)) {\n ellipsis.hidden = false;\n this.#deferredHide.deferOnly(active);\n return;\n }\n\n this.#deferredHide.releaseAll();\n ellipsis.hidden = true;\n }\n\n /** Whether `element` contains (or is) the currently focused element. */\n #holdsFocus(element: HTMLElement): boolean {\n const active = document.activeElement;\n return active instanceof HTMLElement && element.contains(active);\n }\n}\n"]}
@@ -7,9 +7,9 @@ import { Controller } from '@hotwired/stimulus';
7
7
  * <div data-controller="stimeo--calendar"
8
8
  * data-stimeo--calendar-month-value="2026-05"
9
9
  * data-stimeo--calendar-selected-value="2026-05-31">
10
- * <button data-action="stimeo--calendar#prev">Previous</button>
10
+ * <button data-action="click->stimeo--calendar#prev">Previous</button>
11
11
  * <span data-stimeo--calendar-target="label">May 2026</span>
12
- * <button data-action="stimeo--calendar#next">Next</button>
12
+ * <button data-action="click->stimeo--calendar#next">Next</button>
13
13
  * <table role="grid">
14
14
  * <tbody data-stimeo--calendar-target="grid"
15
15
  * data-action="keydown->stimeo--calendar#onKeydown
@@ -23,8 +23,10 @@ import { Controller } from '@hotwired/stimulus';
23
23
  * </div>
24
24
  *
25
25
  * Implements the WAI-ARIA APG **Date Picker Dialog** grid navigation pattern:
26
- * - Locale-aware month/year labels using native `Intl.DateTimeFormat`, read from
27
- * `<html lang>`; a malformed language tag falls back to English so the grid still paints.
26
+ * - Locale-aware month/year labels using native `Intl.DateTimeFormat`, taken from
27
+ * the `locale` Value, else the nearest `lang` up the ancestor chain, else the
28
+ * runtime default; a malformed language tag falls back to English so the grid
29
+ * still paints.
28
30
  * - Roving tabindex focus tracking (exactly one focusable day at any time).
29
31
  * - Full grid keyboard controls (arrows, PageUp/Down, Home/End, Shift+PageUp/Down).
30
32
  * - Automatic month wrapping and date clamping for missing dates (e.g. leap years, 31st to 30th).
@@ -59,6 +61,10 @@ declare class CalendarController extends Controller<HTMLElement> {
59
61
  type: NumberConstructor;
60
62
  default: number;
61
63
  };
64
+ locale: {
65
+ type: StringConstructor;
66
+ default: string;
67
+ };
62
68
  };
63
69
  static actions: readonly ["next", "onKeydown", "prev", "selectByClick"];
64
70
  static events: readonly ["monthchange", "select"];
@@ -72,6 +78,7 @@ declare class CalendarController extends Controller<HTMLElement> {
72
78
  minValue: string;
73
79
  maxValue: string;
74
80
  weekStartValue: number;
81
+ localeValue: string;
75
82
  /** The date currently receiving focus in the grid (local time). */
76
83
  focusedDate: Date;
77
84
  connect(): void;
@@ -82,6 +89,15 @@ declare class CalendarController extends Controller<HTMLElement> {
82
89
  * Forces a re-render of the date grid and updates labels.
83
90
  */
84
91
  monthValueChanged(): void;
92
+ /**
93
+ * Repaints the label when application code changes `locale` at runtime.
94
+ *
95
+ * Stimulus runs this for the initial Value too, before `connect()`. The paint
96
+ * it triggers is idempotent — the month comes from `month` (or, while that is
97
+ * still empty, from `focusedDate`), and `monthchange` only reports a move away
98
+ * from a month already announced, so the first paint never reports one.
99
+ */
100
+ localeValueChanged(): void;
85
101
  /**
86
102
  * Stimulus lifecycle callback triggered automatically when the selectedValue changes.
87
103
  * Re-renders grid cells to update `aria-selected` indicators.
@@ -18,6 +18,35 @@ function isReservedArrowChord(event, allow = []) {
18
18
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
19
  }
20
20
 
21
+ // src/utils/intl_format.ts
22
+ var formatters = /* @__PURE__ */ new WeakMap();
23
+ function intlFormatter(factory, locale, options, fallbackLocale) {
24
+ const cache = cacheFor(factory);
25
+ const key = cacheKey(locale, options, fallbackLocale);
26
+ if (cache.has(key)) return cache.get(key);
27
+ const formatter = construct(factory, locale, options, fallbackLocale);
28
+ cache.set(key, formatter);
29
+ return formatter;
30
+ }
31
+ function cacheFor(factory) {
32
+ const existing = formatters.get(factory);
33
+ if (existing) return existing;
34
+ const created = /* @__PURE__ */ new Map();
35
+ formatters.set(factory, created);
36
+ return created;
37
+ }
38
+ function cacheKey(locale, options, fallbackLocale) {
39
+ return JSON.stringify([locale, options, fallbackLocale]);
40
+ }
41
+ function construct(factory, locale, options, fallbackLocale) {
42
+ try {
43
+ return new factory(locale, options);
44
+ } catch (error) {
45
+ if (!(error instanceof RangeError)) throw error;
46
+ return new factory(fallbackLocale, options);
47
+ }
48
+ }
49
+
21
50
  // src/utils/dates.ts
22
51
  function toISODateString(date) {
23
52
  const year = date.getFullYear();
@@ -51,14 +80,14 @@ function parseISOMonthString(monthStr) {
51
80
  function toISOMonthString(date) {
52
81
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
53
82
  }
83
+ var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
54
84
  function monthLabelFormatter(locale) {
55
- const options = { month: "long", year: "numeric" };
56
- try {
57
- return new Intl.DateTimeFormat(locale, options);
58
- } catch (error) {
59
- if (!(error instanceof RangeError)) throw error;
60
- return new Intl.DateTimeFormat("en", options);
61
- }
85
+ return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
86
+ }
87
+
88
+ // src/utils/locale.ts
89
+ function resolveLocale(element, declared = "") {
90
+ return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
62
91
  }
63
92
 
64
93
  // src/utils/safe_timeout.ts
@@ -123,8 +152,9 @@ var CalendarController = class extends Controller {
123
152
  selected: { type: String, default: "" },
124
153
  min: { type: String, default: "" },
125
154
  max: { type: String, default: "" },
126
- weekStart: { type: Number, default: 0 }
155
+ weekStart: { type: Number, default: 0 },
127
156
  // 0 = Sunday, 1 = Monday, etc.
157
+ locale: { type: String, default: "" }
128
158
  };
129
159
  static actions = ["next", "onKeydown", "prev", "selectByClick"];
130
160
  static events = ["monthchange", "select"];
@@ -166,6 +196,17 @@ var CalendarController = class extends Controller {
166
196
  this.#syncFocusedDateWithMonth();
167
197
  this.render();
168
198
  }
199
+ /**
200
+ * Repaints the label when application code changes `locale` at runtime.
201
+ *
202
+ * Stimulus runs this for the initial Value too, before `connect()`. The paint
203
+ * it triggers is idempotent — the month comes from `month` (or, while that is
204
+ * still empty, from `focusedDate`), and `monthchange` only reports a move away
205
+ * from a month already announced, so the first paint never reports one.
206
+ */
207
+ localeValueChanged() {
208
+ this.render();
209
+ }
169
210
  /**
170
211
  * Stimulus lifecycle callback triggered automatically when the selectedValue changes.
171
212
  * Re-renders grid cells to update `aria-selected` indicators.
@@ -296,8 +337,8 @@ var CalendarController = class extends Controller {
296
337
  const year = monthStart.getFullYear();
297
338
  const month = monthStart.getMonth() + 1;
298
339
  if (this.hasLabelTarget) {
299
- const lang = document.documentElement.lang || "en";
300
- this.labelTarget.textContent = monthLabelFormatter(lang).format(monthStart);
340
+ const locale = resolveLocale(this.element, this.localeValue);
341
+ this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
301
342
  }
302
343
  const days = this.#calculateGridDays(year, month);
303
344
  const dayElements = this.dayTargets;