stimeo-ui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/dist/controllers/accordion_controller.d.ts +3 -2
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +64 -0
- package/dist/controllers/alert_dialog_controller.js +318 -0
- package/dist/controllers/alert_dialog_controller.js.map +1 -0
- package/dist/controllers/carousel_controller.d.ts +100 -0
- package/dist/controllers/carousel_controller.js +272 -0
- package/dist/controllers/carousel_controller.js.map +1 -0
- package/dist/controllers/clipboard_controller.d.ts +72 -0
- package/dist/controllers/clipboard_controller.js +144 -0
- package/dist/controllers/clipboard_controller.js.map +1 -0
- package/dist/controllers/collapsible_controller.d.ts +70 -0
- package/dist/controllers/collapsible_controller.js +327 -0
- package/dist/controllers/collapsible_controller.js.map +1 -0
- package/dist/controllers/color_picker_controller.d.ts +76 -0
- package/dist/controllers/color_picker_controller.js +213 -0
- package/dist/controllers/color_picker_controller.js.map +1 -0
- package/dist/controllers/count_up_controller.js +8 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +72 -0
- package/dist/controllers/currency_input_controller.js +147 -0
- package/dist/controllers/currency_input_controller.js.map +1 -0
- package/dist/controllers/data_grid_controller.d.ts +71 -0
- package/dist/controllers/data_grid_controller.js +168 -0
- package/dist/controllers/data_grid_controller.js.map +1 -0
- package/dist/controllers/date_range_picker_controller.d.ts +87 -0
- package/dist/controllers/date_range_picker_controller.js +417 -0
- package/dist/controllers/date_range_picker_controller.js.map +1 -0
- package/dist/controllers/dismissible_controller.d.ts +57 -0
- package/dist/controllers/dismissible_controller.js +117 -0
- package/dist/controllers/dismissible_controller.js.map +1 -0
- package/dist/controllers/drawer_controller.d.ts +96 -0
- package/dist/controllers/drawer_controller.js +630 -0
- package/dist/controllers/drawer_controller.js.map +1 -0
- package/dist/controllers/editable_controller.d.ts +73 -0
- package/dist/controllers/editable_controller.js +168 -0
- package/dist/controllers/editable_controller.js.map +1 -0
- package/dist/controllers/file_dropzone_controller.d.ts +92 -0
- package/dist/controllers/file_dropzone_controller.js +165 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -0
- package/dist/controllers/filter_controller.d.ts +60 -0
- package/dist/controllers/filter_controller.js +86 -0
- package/dist/controllers/filter_controller.js.map +1 -0
- package/dist/controllers/flash_controller.js +36 -5
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +6 -4
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +41 -18
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +33 -11
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +54 -0
- package/dist/controllers/masonry_controller.js +142 -0
- package/dist/controllers/masonry_controller.js.map +1 -0
- package/dist/controllers/menubar_controller.d.ts +62 -0
- package/dist/controllers/menubar_controller.js +433 -0
- package/dist/controllers/menubar_controller.js.map +1 -0
- package/dist/controllers/multi_select_controller.d.ts +115 -0
- package/dist/controllers/multi_select_controller.js +472 -0
- package/dist/controllers/multi_select_controller.js.map +1 -0
- package/dist/controllers/navigation_menu_controller.d.ts +99 -0
- package/dist/controllers/navigation_menu_controller.js +384 -0
- package/dist/controllers/navigation_menu_controller.js.map +1 -0
- package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
- package/dist/controllers/overflow_indicator_controller.js +178 -27
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +49 -0
- package/dist/controllers/password_reveal_controller.js +117 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -0
- package/dist/controllers/range_slider_controller.d.ts +87 -0
- package/dist/controllers/range_slider_controller.js +166 -0
- package/dist/controllers/range_slider_controller.js.map +1 -0
- package/dist/controllers/read_more_controller.d.ts +52 -0
- package/dist/controllers/read_more_controller.js +194 -0
- package/dist/controllers/read_more_controller.js.map +1 -0
- package/dist/controllers/scroll_area_controller.js +15 -2
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +54 -0
- package/dist/controllers/scroll_restore_controller.js +93 -0
- package/dist/controllers/scroll_restore_controller.js.map +1 -0
- package/dist/controllers/scroll_visibility_controller.js +8 -4
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js +33 -11
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -0
- package/dist/controllers/separator_controller.js +87 -0
- package/dist/controllers/separator_controller.js.map +1 -0
- package/dist/controllers/sidebar_controller.d.ts +107 -0
- package/dist/controllers/sidebar_controller.js +761 -0
- package/dist/controllers/sidebar_controller.js.map +1 -0
- package/dist/controllers/stepper_controller.d.ts +4 -0
- package/dist/controllers/stepper_controller.js +28 -12
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.js +8 -4
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +17 -4
- package/dist/controllers/sticky_observer_controller.js +88 -20
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +3 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +91 -0
- package/dist/controllers/tags_input_controller.js +275 -0
- package/dist/controllers/tags_input_controller.js.map +1 -0
- package/dist/controllers/theme_controller.js +20 -10
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +67 -0
- package/dist/controllers/time_picker_controller.js +212 -0
- package/dist/controllers/time_picker_controller.js.map +1 -0
- package/dist/controllers/toast_controller.d.ts +1 -9
- package/dist/controllers/toast_controller.js +36 -9
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +14 -8
- package/dist/controllers/transition_controller.js +153 -38
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +54 -0
- package/dist/controllers/tree_view_controller.js +275 -0
- package/dist/controllers/tree_view_controller.js.map +1 -0
- package/dist/index.d.ts +27 -1818
- package/dist/index.js +811 -295
- package/dist/index.js.map +1 -1
- package/dist/inspector/examples.json +2 -2
- package/dist/inspector/manifest.json +15 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/toast_controller.ts"],"names":[],"mappings":"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","file":"toast_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\nconst DELEGATED_EVENTS = [\"click\", \"focusin\", \"focusout\", \"keydown\", \"mouseover\", \"mouseout\"];\n\n/**\n * Headless, highly accessible toast notification behavior.\n *\n * Markup contract (identifier: `stimeo--toast`):\n * <div data-controller=\"stimeo--toast\"\n * data-stimeo--toast-duration-value=\"5000\"\n * data-stimeo--toast-max-value=\"3\">\n * <!-- Attribute-only trigger: no hand-written JS required. -->\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n * <!-- The live region is a descendant; the controller element only needs\n * to enclose both the trigger(s) and the list/template targets. -->\n * <div role=\"region\" aria-label=\"Notifications\">\n * <ol data-stimeo--toast-target=\"list\"></ol>\n * <template data-stimeo--toast-target=\"template\">\n * <li data-stimeo--toast-target=\"item\" tabindex=\"0\">\n * <span role=\"status\" data-toast-slot=\"body\"></span>\n * <button type=\"button\" data-toast-dismiss>Dismiss</button>\n * </li>\n * </template>\n * </div>\n * </div>\n *\n * Implements WAI-ARIA live region status/alert announcements, limits simultaneous\n * elements, and pauses dismiss timeouts on hover or focus to comply with WCAG 2.2.1.\n *\n * @remarks\n * Behavior only. The controller handles state updates via `data-state` and lifecycle events\n * while leaving visual styling completely to the client's CSS transitions.\n */\nexport class ToastController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"item\"];\n static override values = {\n duration: { type: Number, default: 0 },\n max: { type: Number, default: 3 },\n };\n static actions = [\"dismiss\", \"onKeydown\", \"pause\", \"resume\", \"show\"] as const;\n static events = [\"dismiss\", \"show\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n\n declare durationValue: number;\n declare maxValue: number;\n\n /**\n * Registry for every auto-dismiss and transition-finalize timer the controller\n * schedules. {@link SafeTimeout} owns *registration and teardown only*; the\n * pause/resume remaining-time accounting stays in `#activeTimeouts` so the\n * per-widget WCAG 2.2.1 semantics are not flattened into the helper.\n */\n #timers = new SafeTimeout();\n\n /**\n * Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).\n * Tracked so {@link disconnect} can cancel any that have not fired, preventing a\n * detached element from being mutated after it leaves the DOM (Turbo).\n */\n #rafHandles = new Map<HTMLElement, number>();\n\n /** Track active timeouts mapped by each toast element for safe cancellation. */\n #activeTimeouts = new Map<HTMLElement, { id: number; startedAt: number; remaining: number }>();\n\n /** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */\n #pauseReasons = new Map<HTMLElement, Set<string>>();\n\n /** The stable list that owns delegated listeners for dynamically added items. */\n #delegatedList: HTMLElement | null = null;\n\n override connect(): void {\n this.#connectDelegatedEvents();\n this.enforceMaxLimit();\n for (const item of this.itemTargets) {\n if (!this.#activeTimeouts.has(item) && item.dataset.state !== \"leaving\") {\n this.#startTimer(item);\n }\n }\n }\n\n override disconnect(): void {\n // SafeTimeout owns every auto-dismiss + finalize timer; one call tears them\n // all down so none fires against the detached controller.\n this.#disconnectDelegatedEvents();\n this.#timers.clearAll();\n for (const handle of this.#rafHandles.values()) {\n window.cancelAnimationFrame(handle);\n }\n this.#rafHandles.clear();\n this.#activeTimeouts.clear();\n this.#pauseReasons.clear();\n }\n\n /** Rebinds delegated interaction when Turbo replaces the list target in place. */\n listTargetConnected(element: HTMLElement): void {\n if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);\n }\n\n /** Releases delegation only when the removed target is its current owner. */\n listTargetDisconnected(element: HTMLElement): void {\n if (this.#delegatedList === element) this.#disconnectDelegatedEvents();\n }\n\n durationValueChanged(): void {\n for (const item of this.itemTargets) {\n if (item.dataset.state === \"leaving\") continue;\n\n const pauseReasons = this.#pauseReasons.get(item);\n this.#clearTimer(item);\n if (this.durationValue <= 0) {\n item.removeAttribute(\"data-paused\");\n } else if (pauseReasons && pauseReasons.size > 0) {\n this.#pauseReasons.set(item, pauseReasons);\n this.#activeTimeouts.set(item, {\n id: 0,\n startedAt: 0,\n remaining: this.durationValue,\n });\n item.setAttribute(\"data-paused\", \"true\");\n } else {\n this.#startTimer(item);\n }\n }\n }\n\n maxValueChanged(): void {\n this.enforceMaxLimit();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when a new item target\n * enters the DOM. Perfectly handles dynamic client-side injections and server-side\n * Turbo Stream appends alike.\n */\n itemTargetConnected(element: HTMLElement): void {\n this.enforceMaxLimit();\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#startTimer(element);\n element.setAttribute(\"data-state\", \"entering\");\n this.#cancelAnimation(element);\n const handle = window.requestAnimationFrame(() => {\n this.#rafHandles.delete(element);\n if (element.parentNode !== this.listTarget || element.dataset.state === \"leaving\") return;\n element.setAttribute(\"data-state\", \"visible\");\n });\n this.#rafHandles.set(element, handle);\n }\n\n /** Clears any active timer when a toast is removed from the DOM. */\n itemTargetDisconnected(element: HTMLElement): void {\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n }\n\n /**\n * Shows a new toast. Accepts its content from either a Stimulus action param\n * (attribute-only trigger) or a programmatic `show` CustomEvent `detail`\n * (remote / Turbo trigger); the action param wins when both are present.\n *\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n *\n * element.dispatchEvent(new CustomEvent(\"show\", { detail: { body, type } }))\n *\n * Clones the template slot, interpolates the body text, and appends to the list.\n */\n show(event: Event): void {\n if (!this.hasTemplateTarget || !this.hasListTarget) return;\n\n const body = this.#readField(event, \"body\");\n if (!body) return;\n\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const item = clone.querySelector(\"[data-stimeo--toast-target='item']\") as HTMLElement | null;\n if (!item) return;\n\n const bodySlot = item.querySelector<HTMLElement>(\"[data-toast-slot='body']\");\n if (!bodySlot) return;\n bodySlot.textContent = body;\n\n // Validate the live-region role at runtime; untrusted params/detail could\n // otherwise write an invalid ARIA role. Anything but \"alert\" stays polite.\n bodySlot.setAttribute(\"role\", this.#readField(event, \"type\") === \"alert\" ? \"alert\" : \"status\");\n\n this.listTarget.appendChild(item);\n this.dispatch(\"show\", { detail: { item } });\n }\n\n /**\n * Reads a string field from a Stimulus action param or a CustomEvent `detail`,\n * preferring the action param. Returns null unless a non-empty string is found,\n * so untrusted runtime payloads cannot inject non-string values.\n */\n #readField(event: Event, key: \"body\" | \"type\"): string | null {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParams = params?.[key];\n if (typeof fromParams === \"string\" && fromParams.length > 0) return fromParams;\n\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail && typeof detail === \"object\" && key in detail) {\n const value = (detail as Record<string, unknown>)[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Dismisses the toast that contained the trigger. */\n dismiss(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n this.#removeWithTransition(item, \"user\");\n }\n\n /** Dismisses the focused toast when Escape is pressed. */\n onKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n // Layered-Escape rule 1: leave a press an inner handler already owned.\n // A press during IME composition (a text field inside the toast) cancels\n // the conversion, never the toast.\n if (event.defaultPrevented || event.isComposing) return;\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n event.preventDefault();\n this.#removeWithTransition(item, \"user\");\n }\n }\n\n /**\n * Pauses the auto-dismiss timer on mouse entry or keyboard focus.\n *\n * Hover and focus are tracked as independent reasons: the timer is only\n * snapshotted on the first active reason, and only resumed once *every*\n * reason has been released (see {@link resume}). This keeps a toast paused\n * while it is still hovered *or* focused, per WCAG 2.2.1.\n */\n pause(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n\n const reasons = this.#pauseReasonsFor(item);\n const wasActive = reasons.size > 0;\n reasons.add(this.#pauseReason(event));\n\n // Only snapshot the remaining time on the first reason; subsequent reasons\n // must not recompute elapsed against the already-cleared timer.\n if (wasActive || timeout.id === 0) return;\n\n this.#timers.clear(timeout.id);\n const elapsed = Date.now() - timeout.startedAt;\n const remaining = Math.max(0, timeout.remaining - elapsed);\n\n if (remaining <= 0) {\n this.#removeWithTransition(item, \"timeout\");\n return;\n }\n\n this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });\n item.setAttribute(\"data-paused\", \"true\");\n }\n\n /** Resumes the auto-dismiss timer once both hover and focus have been released. */\n resume(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const reasons = this.#pauseReasonsFor(item);\n reasons.delete(this.#pauseReason(event));\n // Still paused by the other reason (e.g. mouse left but focus remains).\n if (reasons.size > 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n if (timeout.id !== 0 || timeout.remaining <= 0) return;\n\n item.removeAttribute(\"data-paused\");\n this.#startTimer(item, timeout.remaining);\n }\n\n /** Resolves the toast item element a pause/resume event targets. */\n #itemFromEvent(event: Event): HTMLElement | null {\n const target = event.target instanceof Element ? event.target : event.currentTarget;\n if (!(target instanceof Element) || !this.hasListTarget) return null;\n const item = target.closest<HTMLElement>(\"[data-stimeo--toast-target='item']\");\n return item && this.listTarget.contains(item) ? item : null;\n }\n\n /** Classifies a pause/resume event as a hover or focus reason. */\n #pauseReason(event: Event): \"focus\" | \"hover\" {\n return event.type === \"focusin\" || event.type === \"focusout\" ? \"focus\" : \"hover\";\n }\n\n /** Lazily creates and returns the active pause-reason set for an item. */\n #pauseReasonsFor(item: HTMLElement): Set<string> {\n let reasons = this.#pauseReasons.get(item);\n if (!reasons) {\n reasons = new Set<string>();\n this.#pauseReasons.set(item, reasons);\n }\n return reasons;\n }\n\n #startTimer(element: HTMLElement, duration = this.durationValue): void {\n if (\n duration <= 0 ||\n element.dataset.state === \"leaving\" ||\n element.parentNode !== this.listTarget\n ) {\n return;\n }\n\n this.#clearTimer(element);\n const id = this.#timers.set(() => {\n this.#removeWithTransition(element, \"timeout\");\n }, duration);\n\n this.#activeTimeouts.set(element, { id, startedAt: Date.now(), remaining: duration });\n }\n\n #clearTimer(element: HTMLElement): void {\n const timeout = this.#activeTimeouts.get(element);\n if (timeout) {\n if (timeout.id) this.#timers.clear(timeout.id);\n this.#activeTimeouts.delete(element);\n }\n this.#pauseReasons.delete(element);\n }\n\n #removeWithTransition(element: HTMLElement, reason: \"timeout\" | \"user\"): void {\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n element.setAttribute(\"data-state\", \"leaving\");\n\n const finalize = () => {\n if (element.parentNode !== this.listTarget) return;\n this.listTarget.removeChild(element);\n this.dispatch(\"dismiss\", { detail: { item: element, reason } });\n };\n\n const transitions = window.getComputedStyle(element).transitionDuration;\n const duration = cssTimeToMs(transitions);\n if (duration > 0) {\n this.#timers.set(finalize, duration);\n } else {\n finalize();\n }\n }\n\n /**\n * Removes the oldest toasts when the list exceeds `maxValue`.\n *\n * Public (not `#private`) as a deterministic test seam: enforcement normally\n * runs from `itemTargetConnected`, a Stimulus callback that fires via a\n * MutationObserver — which happy-dom does not reliably deliver — so the unit\n * tests invoke it directly. It is therefore listed in the contract guard's\n * NON_ACTION_ALLOWLIST (it is not a user-wired action).\n */\n enforceMaxLimit(): void {\n const currentItems = this.itemTargets;\n const max = Math.max(0, this.maxValue);\n if (currentItems.length > max) {\n const excessCount = currentItems.length - max;\n for (let i = 0; i < excessCount; i++) {\n const oldest = currentItems[i];\n if (oldest) this.#removeWithTransition(oldest, \"timeout\");\n }\n }\n }\n\n /** Wires stable-container delegation so newly appended items work immediately. */\n #connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null): void {\n this.#disconnectDelegatedEvents();\n if (!list) return;\n\n this.#delegatedList = list;\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.addEventListener(type, this.#onListEvent);\n }\n }\n\n /** Releases every delegated listener from the exact list that owns it. */\n #disconnectDelegatedEvents(): void {\n if (!this.#delegatedList) return;\n\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.removeEventListener(type, this.#onListEvent);\n }\n this.#delegatedList = null;\n }\n\n /** Routes every delegated item interaction from the stable list. */\n readonly #onListEvent = (event: Event): void => {\n if (event.type === \"click\") {\n const target = event.target instanceof Element ? event.target : null;\n const trigger = target?.closest<HTMLElement>(\"[data-toast-dismiss]\");\n if (trigger && this.#delegatedList?.contains(trigger)) this.dismiss(event);\n } else if (event.type === \"keydown\") {\n this.onKeydown(event as KeyboardEvent);\n } else if (this.#crossesItemBoundary(event)) {\n if (event.type === \"focusin\" || event.type === \"mouseover\") this.pause(event);\n else this.resume(event);\n }\n };\n\n /** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */\n #crossesItemBoundary(event: Event): boolean {\n const item = this.#itemFromEvent(event);\n if (!item) return false;\n const related = \"relatedTarget\" in event ? event.relatedTarget : null;\n return !(related instanceof Node && item.contains(related));\n }\n\n /** Cancels the pending entering-to-visible frame owned by one item. */\n #cancelAnimation(element: HTMLElement): void {\n const handle = this.#rafHandles.get(element);\n if (handle === undefined) return;\n window.cancelAnimationFrame(handle);\n this.#rafHandles.delete(element);\n }\n}\n\n/**\n * Converts the first value of a CSS `transition-duration` string to milliseconds.\n * Browsers normalize computed `<time>` values to seconds (e.g. `0.2s`), but parse\n * `ms` defensively so a consumer's `ms` transition is not delayed 1000x.\n *\n * Pure (no `this`); exported so it can be unit-tested directly.\n */\nexport function cssTimeToMs(value: string): number {\n const first = value.split(\",\")[0]?.trim() ?? \"\";\n const amount = Number.parseFloat(first);\n if (Number.isNaN(amount)) return 0;\n return first.endsWith(\"ms\") ? amount : amount * 1000;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/toast_controller.ts"],"names":[],"mappings":"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","file":"toast_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\nconst DELEGATED_EVENTS = [\"click\", \"focusin\", \"focusout\", \"keydown\", \"mouseover\", \"mouseout\"];\n\n/**\n * Headless, highly accessible toast notification behavior.\n *\n * Markup contract (identifier: `stimeo--toast`):\n * <div data-controller=\"stimeo--toast\"\n * data-stimeo--toast-duration-value=\"5000\"\n * data-stimeo--toast-max-value=\"3\">\n * <!-- Attribute-only trigger: no hand-written JS required. -->\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n * <!-- The live region is a descendant; the controller element only needs\n * to enclose both the trigger(s) and the list/template targets. -->\n * <div role=\"region\" aria-label=\"Notifications\">\n * <ol data-stimeo--toast-target=\"list\"></ol>\n * <template data-stimeo--toast-target=\"template\">\n * <li data-stimeo--toast-target=\"item\" tabindex=\"0\">\n * <span role=\"status\" data-toast-slot=\"body\"></span>\n * <button type=\"button\" data-toast-dismiss>Dismiss</button>\n * </li>\n * </template>\n * </div>\n * </div>\n *\n * Implements WAI-ARIA live region status/alert announcements, limits simultaneous\n * elements, and pauses dismiss timeouts on hover or focus to comply with WCAG 2.2.1.\n *\n * @remarks\n * Behavior only. The controller handles state updates via `data-state` and lifecycle events\n * while leaving visual styling completely to the client's CSS transitions.\n */\nexport class ToastController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"item\"];\n static override values = {\n duration: { type: Number, default: 0 },\n max: { type: Number, default: 3 },\n };\n static actions = [\"dismiss\", \"onKeydown\", \"pause\", \"resume\", \"show\"] as const;\n static events = [\"dismiss\", \"show\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n\n declare durationValue: number;\n declare maxValue: number;\n\n /**\n * Registry for every auto-dismiss and transition-finalize timer the controller\n * schedules. {@link SafeTimeout} owns *registration and teardown only*; the\n * pause/resume remaining-time accounting stays in `#activeTimeouts` so the\n * per-widget WCAG 2.2.1 semantics are not flattened into the helper.\n */\n #timers = new SafeTimeout();\n\n /**\n * Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).\n * Tracked so {@link disconnect} can cancel any that have not fired, preventing a\n * detached element from being mutated after it leaves the DOM (Turbo).\n */\n #rafHandles = new Map<HTMLElement, number>();\n\n /** Track active timeouts mapped by each toast element for safe cancellation. */\n #activeTimeouts = new Map<HTMLElement, { id: number; startedAt: number; remaining: number }>();\n\n /** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */\n #pauseReasons = new Map<HTMLElement, Set<string>>();\n\n /** The stable list that owns delegated listeners for dynamically added items. */\n #delegatedList: HTMLElement | null = null;\n\n override connect(): void {\n this.#connectDelegatedEvents();\n this.enforceMaxLimit();\n for (const item of this.itemTargets) {\n if (!this.#activeTimeouts.has(item) && item.dataset.state !== \"leaving\") {\n this.#startTimer(item);\n }\n }\n }\n\n override disconnect(): void {\n // SafeTimeout owns every auto-dismiss + finalize timer; one call tears them\n // all down so none fires against the detached controller.\n this.#disconnectDelegatedEvents();\n this.#timers.clearAll();\n for (const handle of this.#rafHandles.values()) {\n window.cancelAnimationFrame(handle);\n }\n this.#rafHandles.clear();\n this.#activeTimeouts.clear();\n this.#pauseReasons.clear();\n }\n\n /** Rebinds delegated interaction when Turbo replaces the list target in place. */\n listTargetConnected(element: HTMLElement): void {\n if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);\n }\n\n /** Releases delegation only when the removed target is its current owner. */\n listTargetDisconnected(element: HTMLElement): void {\n if (this.#delegatedList === element) this.#disconnectDelegatedEvents();\n }\n\n durationValueChanged(): void {\n for (const item of this.itemTargets) {\n if (item.dataset.state === \"leaving\") continue;\n\n const pauseReasons = this.#pauseReasons.get(item);\n this.#clearTimer(item);\n if (this.durationValue <= 0) {\n item.removeAttribute(\"data-paused\");\n } else if (pauseReasons && pauseReasons.size > 0) {\n this.#pauseReasons.set(item, pauseReasons);\n this.#activeTimeouts.set(item, {\n id: 0,\n startedAt: 0,\n remaining: this.durationValue,\n });\n item.setAttribute(\"data-paused\", \"true\");\n } else {\n this.#startTimer(item);\n }\n }\n }\n\n maxValueChanged(): void {\n this.enforceMaxLimit();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when a new item target\n * enters the DOM. Perfectly handles dynamic client-side injections and server-side\n * Turbo Stream appends alike.\n */\n itemTargetConnected(element: HTMLElement): void {\n this.enforceMaxLimit();\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#startTimer(element);\n element.setAttribute(\"data-state\", \"entering\");\n this.#cancelAnimation(element);\n const handle = window.requestAnimationFrame(() => {\n this.#rafHandles.delete(element);\n if (element.parentNode !== this.listTarget || element.dataset.state === \"leaving\") return;\n element.setAttribute(\"data-state\", \"visible\");\n });\n this.#rafHandles.set(element, handle);\n }\n\n /** Clears any active timer when a toast is removed from the DOM. */\n itemTargetDisconnected(element: HTMLElement): void {\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n }\n\n /**\n * Shows a new toast. Accepts its content from either a Stimulus action param\n * (attribute-only trigger) or a programmatic `show` CustomEvent `detail`\n * (remote / Turbo trigger); the action param wins when both are present.\n *\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n *\n * element.dispatchEvent(new CustomEvent(\"show\", { detail: { body, type } }))\n *\n * Clones the template slot, interpolates the body text, and appends to the list.\n */\n show(event: Event): void {\n if (!this.hasTemplateTarget || !this.hasListTarget) return;\n\n const body = this.#readField(event, \"body\");\n if (!body) return;\n\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const item = clone.querySelector(\"[data-stimeo--toast-target='item']\") as HTMLElement | null;\n if (!item) return;\n\n const bodySlot = item.querySelector<HTMLElement>(\"[data-toast-slot='body']\");\n if (!bodySlot) return;\n bodySlot.textContent = body;\n\n // Validate the live-region role at runtime; untrusted params/detail could\n // otherwise write an invalid ARIA role. Anything but \"alert\" stays polite.\n bodySlot.setAttribute(\"role\", this.#readField(event, \"type\") === \"alert\" ? \"alert\" : \"status\");\n\n this.listTarget.appendChild(item);\n this.dispatch(\"show\", { detail: { item } });\n }\n\n /**\n * Reads a string field from a Stimulus action param or a CustomEvent `detail`,\n * preferring the action param. Returns null unless a non-empty string is found,\n * so untrusted runtime payloads cannot inject non-string values.\n */\n #readField(event: Event, key: \"body\" | \"type\"): string | null {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParams = params?.[key];\n if (typeof fromParams === \"string\" && fromParams.length > 0) return fromParams;\n\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail && typeof detail === \"object\" && key in detail) {\n const value = (detail as Record<string, unknown>)[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Dismisses the toast that contained the trigger. */\n dismiss(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n this.#removeWithTransition(item, \"user\");\n }\n\n /** Dismisses the focused toast when Escape is pressed. */\n onKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n // Layered-Escape rule 1: leave a press an inner handler already owned.\n // A press during IME composition (a text field inside the toast) cancels\n // the conversion, never the toast.\n if (event.defaultPrevented || event.isComposing) return;\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n event.preventDefault();\n this.#removeWithTransition(item, \"user\");\n }\n }\n\n /**\n * Pauses the auto-dismiss timer on mouse entry or keyboard focus.\n *\n * Hover and focus are tracked as independent reasons: the timer is only\n * snapshotted on the first active reason, and only resumed once *every*\n * reason has been released (see {@link resume}). This keeps a toast paused\n * while it is still hovered *or* focused, per WCAG 2.2.1.\n */\n pause(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n\n const reasons = this.#pauseReasonsFor(item);\n const wasActive = reasons.size > 0;\n reasons.add(this.#pauseReason(event));\n\n // Only snapshot the remaining time on the first reason; subsequent reasons\n // must not recompute elapsed against the already-cleared timer.\n if (wasActive || timeout.id === 0) return;\n\n this.#timers.clear(timeout.id);\n const elapsed = Date.now() - timeout.startedAt;\n const remaining = Math.max(0, timeout.remaining - elapsed);\n\n if (remaining <= 0) {\n this.#removeWithTransition(item, \"timeout\");\n return;\n }\n\n this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });\n item.setAttribute(\"data-paused\", \"true\");\n }\n\n /** Resumes the auto-dismiss timer once both hover and focus have been released. */\n resume(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const reasons = this.#pauseReasonsFor(item);\n reasons.delete(this.#pauseReason(event));\n // Still paused by the other reason (e.g. mouse left but focus remains).\n if (reasons.size > 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n if (timeout.id !== 0 || timeout.remaining <= 0) return;\n\n item.removeAttribute(\"data-paused\");\n this.#startTimer(item, timeout.remaining);\n }\n\n /** Resolves the toast item element a pause/resume event targets. */\n #itemFromEvent(event: Event): HTMLElement | null {\n const target = event.target instanceof Element ? event.target : event.currentTarget;\n if (!(target instanceof Element) || !this.hasListTarget) return null;\n const item = target.closest<HTMLElement>(\"[data-stimeo--toast-target='item']\");\n return item && this.listTarget.contains(item) ? item : null;\n }\n\n /** Classifies a pause/resume event as a hover or focus reason. */\n #pauseReason(event: Event): \"focus\" | \"hover\" {\n return event.type === \"focusin\" || event.type === \"focusout\" ? \"focus\" : \"hover\";\n }\n\n /** Lazily creates and returns the active pause-reason set for an item. */\n #pauseReasonsFor(item: HTMLElement): Set<string> {\n let reasons = this.#pauseReasons.get(item);\n if (!reasons) {\n reasons = new Set<string>();\n this.#pauseReasons.set(item, reasons);\n }\n return reasons;\n }\n\n #startTimer(element: HTMLElement, duration = this.durationValue): void {\n if (\n duration <= 0 ||\n element.dataset.state === \"leaving\" ||\n element.parentNode !== this.listTarget\n ) {\n return;\n }\n\n this.#clearTimer(element);\n const id = this.#timers.set(() => {\n this.#removeWithTransition(element, \"timeout\");\n }, duration);\n\n this.#activeTimeouts.set(element, { id, startedAt: Date.now(), remaining: duration });\n }\n\n #clearTimer(element: HTMLElement): void {\n const timeout = this.#activeTimeouts.get(element);\n if (timeout) {\n if (timeout.id) this.#timers.clear(timeout.id);\n this.#activeTimeouts.delete(element);\n }\n this.#pauseReasons.delete(element);\n }\n\n #removeWithTransition(element: HTMLElement, reason: \"timeout\" | \"user\"): void {\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n element.setAttribute(\"data-state\", \"leaving\");\n\n const finalize = () => {\n if (element.parentNode !== this.listTarget) return;\n this.listTarget.removeChild(element);\n this.dispatch(\"dismiss\", { detail: { item: element, reason } });\n };\n\n const duration = maxTransitionTotalMs(window.getComputedStyle(element));\n if (duration > 0) {\n this.#timers.set(finalize, duration);\n } else {\n finalize();\n }\n }\n\n /**\n * Removes the oldest toasts when the list exceeds `maxValue`.\n *\n * Public (not `#private`) as a deterministic test seam: enforcement normally\n * runs from `itemTargetConnected`, a Stimulus callback that fires via a\n * MutationObserver — which happy-dom does not reliably deliver — so the unit\n * tests invoke it directly. It is therefore listed in the contract guard's\n * NON_ACTION_ALLOWLIST (it is not a user-wired action).\n */\n enforceMaxLimit(): void {\n const currentItems = this.itemTargets;\n const max = Math.max(0, this.maxValue);\n if (currentItems.length > max) {\n const excessCount = currentItems.length - max;\n for (let i = 0; i < excessCount; i++) {\n const oldest = currentItems[i];\n if (oldest) this.#removeWithTransition(oldest, \"timeout\");\n }\n }\n }\n\n /** Wires stable-container delegation so newly appended items work immediately. */\n #connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null): void {\n this.#disconnectDelegatedEvents();\n if (!list) return;\n\n this.#delegatedList = list;\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.addEventListener(type, this.#onListEvent);\n }\n }\n\n /** Releases every delegated listener from the exact list that owns it. */\n #disconnectDelegatedEvents(): void {\n if (!this.#delegatedList) return;\n\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.removeEventListener(type, this.#onListEvent);\n }\n this.#delegatedList = null;\n }\n\n /** Routes every delegated item interaction from the stable list. */\n readonly #onListEvent = (event: Event): void => {\n if (event.type === \"click\") {\n const target = event.target instanceof Element ? event.target : null;\n const trigger = target?.closest<HTMLElement>(\"[data-toast-dismiss]\");\n if (trigger && this.#delegatedList?.contains(trigger)) this.dismiss(event);\n } else if (event.type === \"keydown\") {\n this.onKeydown(event as KeyboardEvent);\n } else if (this.#crossesItemBoundary(event)) {\n if (event.type === \"focusin\" || event.type === \"mouseover\") this.pause(event);\n else this.resume(event);\n }\n };\n\n /** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */\n #crossesItemBoundary(event: Event): boolean {\n const item = this.#itemFromEvent(event);\n if (!item) return false;\n const related = \"relatedTarget\" in event ? event.relatedTarget : null;\n return !(related instanceof Node && item.contains(related));\n }\n\n /** Cancels the pending entering-to-visible frame owned by one item. */\n #cancelAnimation(element: HTMLElement): void {\n const handle = this.#rafHandles.get(element);\n if (handle === undefined) return;\n window.cancelAnimationFrame(handle);\n this.#rafHandles.delete(element);\n }\n}\n"]}
|
|
@@ -17,20 +17,26 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
17
17
|
* data-stimeo--transition-leave-to-value="opacity-0" hidden>…</div>
|
|
18
18
|
*
|
|
19
19
|
* `enter()` unhides the element, applies `enter` + `enterFrom`, then on the next frame
|
|
20
|
-
* swaps `enterFrom` → `enterTo` so the CSS transition runs, and
|
|
21
|
-
*
|
|
20
|
+
* swaps `enterFrom` → `enterTo` so the CSS transition runs, and settles to `entered`
|
|
21
|
+
* once the transition completes. `leave()` mirrors it and re-applies `hidden`.
|
|
22
22
|
* `toggle()` reverses the current direction. The element carries `data-transition-state`
|
|
23
23
|
* (`entering` / `entered` / `leaving` / `left`) and `entered` / `left` events fire on
|
|
24
24
|
* completion.
|
|
25
25
|
*
|
|
26
26
|
* @remarks
|
|
27
27
|
* Behavior only — the animation itself is the consumer's CSS; this controls *when* the
|
|
28
|
-
* stage classes are applied.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* stage classes are applied. Completion is owned by the shared
|
|
29
|
+
* {@link TransitionCompletion}: every declared transition property must settle
|
|
30
|
+
* (`transitionend` / `transitioncancel`, pseudo-element events excluded) with a
|
|
31
|
+
* `max(duration + delay) + 50ms` bounded fallback, and a computed 0ms transition
|
|
32
|
+
* settles synchronously at the staging frame. A positive `timeout` Value replaces the
|
|
33
|
+
* fallback verbatim and keeps the wait armed even for a computed 0ms transition.
|
|
34
|
+
* Under `prefers-reduced-motion: reduce` it switches instantly (no staging). An
|
|
35
|
+
* interrupting call cancels the in-flight transition and starts the new one. State
|
|
36
|
+
* lives solely in `hidden` / `data-transition-state`, and `connect()` reconciles to a
|
|
37
|
+
* stable state (stripping any half-applied stage classes from a Turbo cache); the
|
|
38
|
+
* terminal-event listeners, rAF, and fallback timer are released on `disconnect()`
|
|
39
|
+
* (Turbo navigation included).
|
|
34
40
|
*/
|
|
35
41
|
declare class TransitionController extends Controller<HTMLElement> {
|
|
36
42
|
#private;
|
|
@@ -2,6 +2,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/transition_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/reduced_motion.ts
|
|
6
|
+
function prefersReducedMotion() {
|
|
7
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
// src/utils/safe_timeout.ts
|
|
6
11
|
var TimerRegistry = class {
|
|
7
12
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -55,14 +60,149 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
60
|
}
|
|
56
61
|
};
|
|
57
62
|
|
|
63
|
+
// src/utils/transition_completion.ts
|
|
64
|
+
function timeMs(value) {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
const amount = Number.parseFloat(trimmed);
|
|
67
|
+
if (!Number.isFinite(amount)) return 0;
|
|
68
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
69
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function cssList(value) {
|
|
73
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
function transitionTimings(style) {
|
|
76
|
+
const properties = cssList(style.transitionProperty);
|
|
77
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
78
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
79
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
80
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
81
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
82
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
83
|
+
property,
|
|
84
|
+
totalMs: Math.max(
|
|
85
|
+
0,
|
|
86
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
87
|
+
)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function maxTotalMs(timings) {
|
|
91
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
92
|
+
}
|
|
93
|
+
var TransitionCompletion = class {
|
|
94
|
+
#timers = new SafeTimeout();
|
|
95
|
+
#element = null;
|
|
96
|
+
#complete = null;
|
|
97
|
+
#pendingProperties = null;
|
|
98
|
+
#deadline = 0;
|
|
99
|
+
/**
|
|
100
|
+
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
101
|
+
* transition (including when `getComputedStyle` is unavailable).
|
|
102
|
+
*
|
|
103
|
+
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
104
|
+
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
105
|
+
*/
|
|
106
|
+
wait(element, complete, options = {}) {
|
|
107
|
+
this.cancel();
|
|
108
|
+
const requested = options.timeoutMs ?? 0;
|
|
109
|
+
const override = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
110
|
+
const timings = typeof window.getComputedStyle === "function" ? transitionTimings(window.getComputedStyle(element)) : [];
|
|
111
|
+
const maximum = maxTotalMs(timings);
|
|
112
|
+
if (maximum <= 0 && override <= 0) {
|
|
113
|
+
complete();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#element = element;
|
|
117
|
+
this.#complete = complete;
|
|
118
|
+
this.#deadline = Date.now() + maximum;
|
|
119
|
+
const activeProperties = this.#activeTransitionProperties(element);
|
|
120
|
+
this.#pendingProperties = activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);
|
|
121
|
+
element.addEventListener("transitionend", this.#onTerminal);
|
|
122
|
+
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
123
|
+
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
124
|
+
}
|
|
125
|
+
/** Cancels the pending wait without invoking its completion callback. */
|
|
126
|
+
cancel() {
|
|
127
|
+
this.#complete = null;
|
|
128
|
+
this.#teardown();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Handles terminal events from the observed element only.
|
|
132
|
+
*
|
|
133
|
+
* For explicit property lists, every declared positive-time property must
|
|
134
|
+
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
135
|
+
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
136
|
+
*/
|
|
137
|
+
#onTerminal = (event) => {
|
|
138
|
+
if (event.target !== this.#element) return;
|
|
139
|
+
const transitionEvent = event;
|
|
140
|
+
if (transitionEvent.pseudoElement) return;
|
|
141
|
+
if (this.#pendingProperties) {
|
|
142
|
+
const propertyName = transitionEvent.propertyName;
|
|
143
|
+
const active = this.#activeTransitionProperties(this.#element);
|
|
144
|
+
if (active.has(propertyName)) return;
|
|
145
|
+
if (!this.#pendingProperties.delete(propertyName)) return;
|
|
146
|
+
if (this.#pendingProperties.size > 0) return;
|
|
147
|
+
if (active.size > 0) {
|
|
148
|
+
this.#pendingProperties = active;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.#finish();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (Date.now() >= this.#deadline) this.#finish();
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Returns active CSS transition properties expanded to the names reported by
|
|
158
|
+
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
159
|
+
*/
|
|
160
|
+
#activeTransitionProperties(element) {
|
|
161
|
+
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
162
|
+
try {
|
|
163
|
+
const properties = element.getAnimations().flatMap((animation) => {
|
|
164
|
+
if (animation.playState === "idle" || animation.playState === "finished") return [];
|
|
165
|
+
const effect = animation.effect;
|
|
166
|
+
if (effect?.pseudoElement) return [];
|
|
167
|
+
const target = effect?.target;
|
|
168
|
+
if (target && target !== element) return [];
|
|
169
|
+
const property = animation.transitionProperty;
|
|
170
|
+
return typeof property === "string" && property.length > 0 ? [property] : [];
|
|
171
|
+
});
|
|
172
|
+
return new Set(properties);
|
|
173
|
+
} catch {
|
|
174
|
+
return /* @__PURE__ */ new Set();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
178
|
+
#explicitPendingProperties(timings) {
|
|
179
|
+
if (timings.some(({ property }) => property === "all")) return null;
|
|
180
|
+
const pending = new Set(
|
|
181
|
+
timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property)
|
|
182
|
+
);
|
|
183
|
+
return pending.size > 0 ? pending : null;
|
|
184
|
+
}
|
|
185
|
+
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
186
|
+
#finish() {
|
|
187
|
+
const complete = this.#complete;
|
|
188
|
+
if (!complete) return;
|
|
189
|
+
this.#complete = null;
|
|
190
|
+
this.#teardown();
|
|
191
|
+
complete();
|
|
192
|
+
}
|
|
193
|
+
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
194
|
+
#teardown() {
|
|
195
|
+
this.#timers.clearAll();
|
|
196
|
+
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
197
|
+
this.#element?.removeEventListener("transitioncancel", this.#onTerminal);
|
|
198
|
+
this.#element = null;
|
|
199
|
+
this.#pendingProperties = null;
|
|
200
|
+
this.#deadline = 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
58
204
|
// src/controllers/transition_controller.ts
|
|
59
205
|
var tokensOf = (value) => value.split(/\s+/).filter(Boolean);
|
|
60
|
-
var firstTimeMs = (value) => {
|
|
61
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
62
|
-
const amount = Number.parseFloat(first);
|
|
63
|
-
if (Number.isNaN(amount)) return 0;
|
|
64
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
65
|
-
};
|
|
66
206
|
var TransitionController = class extends Controller {
|
|
67
207
|
static values = {
|
|
68
208
|
enter: { type: String, default: "" },
|
|
@@ -75,9 +215,9 @@ var TransitionController = class extends Controller {
|
|
|
75
215
|
};
|
|
76
216
|
static actions = ["enter", "leave", "toggle"];
|
|
77
217
|
static events = ["entered", "left"];
|
|
78
|
-
|
|
218
|
+
/** Owns the cancellable completion wait (terminal events + bounded fallback). */
|
|
219
|
+
#transition = new TransitionCompletion();
|
|
79
220
|
#rafId = null;
|
|
80
|
-
#endListener = null;
|
|
81
221
|
connect() {
|
|
82
222
|
this.#strip();
|
|
83
223
|
this.element.setAttribute("data-transition-state", this.element.hidden ? "left" : "entered");
|
|
@@ -104,7 +244,7 @@ var TransitionController = class extends Controller {
|
|
|
104
244
|
const isEnter = kind === "enter";
|
|
105
245
|
if (isEnter) this.element.hidden = false;
|
|
106
246
|
this.element.setAttribute("data-transition-state", isEnter ? "entering" : "leaving");
|
|
107
|
-
if (
|
|
247
|
+
if (prefersReducedMotion()) {
|
|
108
248
|
this.#finish(kind);
|
|
109
249
|
return;
|
|
110
250
|
}
|
|
@@ -116,12 +256,13 @@ var TransitionController = class extends Controller {
|
|
|
116
256
|
this.#rafId = null;
|
|
117
257
|
this.#remove(from);
|
|
118
258
|
this.#add(to);
|
|
119
|
-
this.#
|
|
259
|
+
this.#transition.wait(this.element, () => this.#finish(kind), {
|
|
260
|
+
timeoutMs: this.timeoutValue
|
|
261
|
+
});
|
|
120
262
|
});
|
|
121
263
|
}
|
|
122
264
|
/** Settles the element into the completed state, clearing the stage classes. */
|
|
123
265
|
#finish(kind) {
|
|
124
|
-
this.#cleanupEnd();
|
|
125
266
|
this.#strip();
|
|
126
267
|
if (kind === "enter") {
|
|
127
268
|
this.element.setAttribute("data-transition-state", "entered");
|
|
@@ -132,29 +273,13 @@ var TransitionController = class extends Controller {
|
|
|
132
273
|
this.dispatch("left", { detail: {} });
|
|
133
274
|
}
|
|
134
275
|
}
|
|
135
|
-
/** Resolves on the element's own `transitionend`, with a safety timeout fallback. */
|
|
136
|
-
#awaitEnd(done) {
|
|
137
|
-
this.#endListener = (event) => {
|
|
138
|
-
if (event.target === this.element) done();
|
|
139
|
-
};
|
|
140
|
-
this.element.addEventListener("transitionend", this.#endListener);
|
|
141
|
-
const ms = this.timeoutValue > 0 ? this.timeoutValue : this.#duration();
|
|
142
|
-
this.#timers.set(done, ms);
|
|
143
|
-
}
|
|
144
|
-
#cleanupEnd() {
|
|
145
|
-
if (this.#endListener) {
|
|
146
|
-
this.element.removeEventListener("transitionend", this.#endListener);
|
|
147
|
-
this.#endListener = null;
|
|
148
|
-
}
|
|
149
|
-
this.#timers.clearAll();
|
|
150
|
-
}
|
|
151
276
|
/** Cancels any in-flight transition (interruption / teardown). */
|
|
152
277
|
#cancel() {
|
|
153
278
|
if (this.#rafId !== null) {
|
|
154
279
|
this.#cancelRaf(this.#rafId);
|
|
155
280
|
this.#rafId = null;
|
|
156
281
|
}
|
|
157
|
-
this.#
|
|
282
|
+
this.#transition.cancel();
|
|
158
283
|
this.#strip();
|
|
159
284
|
}
|
|
160
285
|
#add(...lists) {
|
|
@@ -176,13 +301,6 @@ var TransitionController = class extends Controller {
|
|
|
176
301
|
this.leaveToValue
|
|
177
302
|
);
|
|
178
303
|
}
|
|
179
|
-
/** Auto-computed safety duration (transition time + delay, with a small buffer). */
|
|
180
|
-
#duration() {
|
|
181
|
-
if (typeof window.getComputedStyle !== "function") return 0;
|
|
182
|
-
const style = window.getComputedStyle(this.element);
|
|
183
|
-
const total = firstTimeMs(style.transitionDuration) + firstTimeMs(style.transitionDelay);
|
|
184
|
-
return total > 0 ? total + 50 : 0;
|
|
185
|
-
}
|
|
186
304
|
#raf(callback) {
|
|
187
305
|
if (typeof window.requestAnimationFrame === "function") {
|
|
188
306
|
return window.requestAnimationFrame(() => callback());
|
|
@@ -193,9 +311,6 @@ var TransitionController = class extends Controller {
|
|
|
193
311
|
if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(id);
|
|
194
312
|
else window.clearTimeout(id);
|
|
195
313
|
}
|
|
196
|
-
#prefersReducedMotion() {
|
|
197
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
198
|
-
}
|
|
199
314
|
};
|
|
200
315
|
|
|
201
316
|
export { TransitionController };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/transition_controller.ts"],"names":[],"mappings":"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","file":"transition_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Splits a space-separated class-list value into individual, non-empty tokens. */\nconst tokensOf = (value: string): string[] => value.split(/\\s+/).filter(Boolean);\n\n/** First `<time>` of a computed `transition-duration` / `-delay` value, in ms. */\nconst firstTimeMs = (value: string): number => {\n const first = value.split(\",\")[0]?.trim() ?? \"\";\n const amount = Number.parseFloat(first);\n if (Number.isNaN(amount)) return 0;\n return first.endsWith(\"ms\") ? amount : amount * 1000;\n};\n\n/**\n * Headless **enter/leave transition base**: stages CSS classes for showing and hiding\n * an element (the shared substrate other widgets can lean on instead of hand-rolling\n * it). Counterpart to Headless UI `Transition` / Alpine `x-transition` (no APG pattern;\n * honors WCAG 2.2 **2.3.3** via `prefers-reduced-motion` and keeps the visual state in\n * sync with `hidden`).\n *\n * Markup contract (identifier: `stimeo--transition`):\n * <div data-controller=\"stimeo--transition\"\n * data-stimeo--transition-enter-value=\"ease-out duration-200\"\n * data-stimeo--transition-enter-from-value=\"opacity-0\"\n * data-stimeo--transition-enter-to-value=\"opacity-100\"\n * data-stimeo--transition-leave-value=\"ease-in duration-150\"\n * data-stimeo--transition-leave-from-value=\"opacity-100\"\n * data-stimeo--transition-leave-to-value=\"opacity-0\" hidden>…</div>\n *\n * `enter()` unhides the element, applies `enter` + `enterFrom`, then on the next frame\n * swaps `enterFrom` → `enterTo` so the CSS transition runs, and on `transitionend` (or\n * a safety timeout) settles to `entered`. `leave()` mirrors it and re-applies `hidden`.\n * `toggle()` reverses the current direction. The element carries `data-transition-state`\n * (`entering` / `entered` / `leaving` / `left`) and `entered` / `left` events fire on\n * completion.\n *\n * @remarks\n * Behavior only — the animation itself is the consumer's CSS; this controls *when* the\n * stage classes are applied. Under `prefers-reduced-motion: reduce` it switches\n * instantly (no staging). An interrupting call cancels the in-flight transition and\n * starts the new one. State lives solely in `hidden` / `data-transition-state`, and\n * `connect()` reconciles to a stable state (stripping any half-applied stage classes\n * from a Turbo cache); the `transitionend` listener, rAF, and safety timer are released\n * on `disconnect()` (Turbo navigation included).\n */\nexport class TransitionController extends Controller<HTMLElement> {\n static override values = {\n enter: { type: String, default: \"\" },\n enterFrom: { type: String, default: \"\" },\n enterTo: { type: String, default: \"\" },\n leave: { type: String, default: \"\" },\n leaveFrom: { type: String, default: \"\" },\n leaveTo: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n };\n static actions = [\"enter\", \"leave\", \"toggle\"] as const;\n static events = [\"entered\", \"left\"] as const;\n\n declare enterValue: string;\n declare enterFromValue: string;\n declare enterToValue: string;\n declare leaveValue: string;\n declare leaveFromValue: string;\n declare leaveToValue: string;\n declare timeoutValue: number;\n\n readonly #timers = new SafeTimeout();\n #rafId: number | null = null;\n #endListener: ((event: Event) => void) | null = null;\n\n override connect(): void {\n // Drop any half-applied stage classes a cache may have captured, then settle the\n // state hook to match the element's current visibility.\n this.#strip();\n this.element.setAttribute(\"data-transition-state\", this.element.hidden ? \"left\" : \"entered\");\n }\n\n override disconnect(): void {\n this.#cancel();\n }\n\n /** Shows the element with the enter transition. */\n enter(): void {\n this.#run(\"enter\");\n }\n\n /** Hides the element with the leave transition. */\n leave(): void {\n this.#run(\"leave\");\n }\n\n /** Reverses the current direction (enter when hidden/leaving, else leave). */\n toggle(): void {\n const state = this.element.getAttribute(\"data-transition-state\");\n if (state === \"entered\" || state === \"entering\") this.leave();\n else this.enter();\n }\n\n #run(kind: \"enter\" | \"leave\"): void {\n this.#cancel();\n const isEnter = kind === \"enter\";\n if (isEnter) this.element.hidden = false;\n this.element.setAttribute(\"data-transition-state\", isEnter ? \"entering\" : \"leaving\");\n\n if (this.#prefersReducedMotion()) {\n this.#finish(kind);\n return;\n }\n\n const base = isEnter ? this.enterValue : this.leaveValue;\n const from = isEnter ? this.enterFromValue : this.leaveFromValue;\n const to = isEnter ? this.enterToValue : this.leaveToValue;\n\n this.#add(base, from);\n this.#rafId = this.#raf(() => {\n this.#rafId = null;\n this.#remove(from);\n this.#add(to);\n this.#awaitEnd(() => this.#finish(kind));\n });\n }\n\n /** Settles the element into the completed state, clearing the stage classes. */\n #finish(kind: \"enter\" | \"leave\"): void {\n this.#cleanupEnd();\n this.#strip();\n if (kind === \"enter\") {\n this.element.setAttribute(\"data-transition-state\", \"entered\");\n this.dispatch(\"entered\", { detail: {} });\n } else {\n this.element.hidden = true;\n this.element.setAttribute(\"data-transition-state\", \"left\");\n this.dispatch(\"left\", { detail: {} });\n }\n }\n\n /** Resolves on the element's own `transitionend`, with a safety timeout fallback. */\n #awaitEnd(done: () => void): void {\n this.#endListener = (event: Event): void => {\n if (event.target === this.element) done();\n };\n this.element.addEventListener(\"transitionend\", this.#endListener);\n const ms = this.timeoutValue > 0 ? this.timeoutValue : this.#duration();\n this.#timers.set(done, ms);\n }\n\n #cleanupEnd(): void {\n if (this.#endListener) {\n this.element.removeEventListener(\"transitionend\", this.#endListener);\n this.#endListener = null;\n }\n this.#timers.clearAll();\n }\n\n /** Cancels any in-flight transition (interruption / teardown). */\n #cancel(): void {\n if (this.#rafId !== null) {\n this.#cancelRaf(this.#rafId);\n this.#rafId = null;\n }\n this.#cleanupEnd();\n this.#strip();\n }\n\n #add(...lists: string[]): void {\n const tokens = lists.flatMap(tokensOf);\n if (tokens.length > 0) this.element.classList.add(...tokens);\n }\n\n #remove(...lists: string[]): void {\n const tokens = lists.flatMap(tokensOf);\n if (tokens.length > 0) this.element.classList.remove(...tokens);\n }\n\n /** Removes every stage class so no half-applied state lingers. */\n #strip(): void {\n this.#remove(\n this.enterValue,\n this.enterFromValue,\n this.enterToValue,\n this.leaveValue,\n this.leaveFromValue,\n this.leaveToValue,\n );\n }\n\n /** Auto-computed safety duration (transition time + delay, with a small buffer). */\n #duration(): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n const style = window.getComputedStyle(this.element);\n const total = firstTimeMs(style.transitionDuration) + firstTimeMs(style.transitionDelay);\n return total > 0 ? total + 50 : 0;\n }\n\n #raf(callback: () => void): number {\n if (typeof window.requestAnimationFrame === \"function\") {\n return window.requestAnimationFrame(() => callback());\n }\n return window.setTimeout(callback, 0);\n }\n\n #cancelRaf(id: number): void {\n if (typeof window.cancelAnimationFrame === \"function\") window.cancelAnimationFrame(id);\n else window.clearTimeout(id);\n }\n\n #prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/transition_controller.ts"],"names":[],"mappings":";;;;;AAoBO,SAAS,oBAAA,GAAgC;AAC9C,EAAA,OACE,OAAO,MAAA,CAAO,UAAA,KAAe,cAC7B,MAAA,CAAO,UAAA,CAAW,kCAAkC,CAAA,CAAE,OAAA;AAE1D;;;ACDA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACxFA,SAAS,OAAO,KAAA,EAAuB;AACrC,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA;AACxC,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,GAAG,OAAO,CAAA;AACrC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,MAAA;AACnC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,SAAU,MAAA,GAAS,GAAA;AAC3C,EAAA,OAAO,CAAA;AACT;AAGA,SAAS,QAAQ,KAAA,EAAyB;AACxC,EAAA,OAAO,KAAA,CACJ,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,CAAA,CACzB,MAAA,CAAO,OAAO,CAAA;AACnB;AASA,SAAS,kBAAkB,KAAA,EAA4C;AACrE,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA;AACnD,EAAA,MAAM,YAAY,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA,CAAE,IAAI,MAAM,CAAA;AAC9D,EAAA,MAAM,SAAS,OAAA,CAAQ,KAAA,CAAM,eAAe,CAAA,CAAE,IAAI,MAAM,CAAA;AACxD,EAAA,MAAM,sBACJ,UAAA,CAAW,MAAA,GAAS,IAChB,UAAA,GACA,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,KAAK,GAAA,CAAI,SAAA,CAAU,QAAQ,MAAA,CAAO,MAAA,EAAQ,CAAC,CAAA,EAAE,EAAG,MAAM,KAAK,CAAA;AACtF,EAAA,MAAM,qBAAqB,SAAA,CAAU,MAAA,GAAS,CAAA,GAAI,SAAA,GAAY,CAAC,CAAC,CAAA;AAChE,EAAA,MAAM,kBAAkB,MAAA,CAAO,MAAA,GAAS,CAAA,GAAI,MAAA,GAAS,CAAC,CAAC,CAAA;AAEvD,EAAA,OAAO,mBAAA,CACJ,MAAA,CAAO,CAAC,QAAA,KAAa,QAAA,KAAa,MAAM,CAAA,CACxC,GAAA,CAAI,CAAC,QAAA,EAAU,KAAA,MAAW;AAAA,IACzB,QAAA;AAAA,IACA,SAAS,IAAA,CAAK,GAAA;AAAA,MACZ,CAAA;AAAA,MAAA,CACC,kBAAA,CAAmB,KAAA,GAAQ,kBAAA,CAAmB,MAAM,CAAA,IAAK,MACvD,eAAA,CAAgB,KAAA,GAAQ,eAAA,CAAgB,MAAM,CAAA,IAAK,CAAA;AAAA;AACxD,GACF,CAAE,CAAA;AACN;AAGA,SAAS,WAAW,OAAA,EAAqC;AACvD,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,EAAE,OAAA,EAAQ,KAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA,EAAG,CAAC,CAAA;AACvE;AAqCO,IAAM,uBAAN,MAA2B;AAAA,EACvB,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA,EACnC,QAAA,GAA+B,IAAA;AAAA,EAC/B,SAAA,GAAiC,IAAA;AAAA,EACjC,kBAAA,GAAyC,IAAA;AAAA,EACzC,SAAA,GAAY,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASZ,IAAA,CAAK,OAAA,EAAsB,QAAA,EAAsB,OAAA,GAAiC,EAAC,EAAS;AAC1F,IAAA,IAAA,CAAK,MAAA,EAAO;AAEZ,IAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,CAAA;AACvC,IAAA,MAAM,WAAW,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA,IAAK,SAAA,GAAY,IAAI,SAAA,GAAY,CAAA;AAG3E,IAAA,MAAM,OAAA,GACJ,OAAO,MAAA,CAAO,gBAAA,KAAqB,UAAA,GAC/B,iBAAA,CAAkB,MAAA,CAAO,gBAAA,CAAiB,OAAO,CAAC,CAAA,GAClD,EAAC;AACP,IAAA,MAAM,OAAA,GAAU,WAAW,OAAO,CAAA;AAClC,IAAA,IAAI,OAAA,IAAW,CAAA,IAAK,QAAA,IAAY,CAAA,EAAG;AACjC,MAAA,QAAA,EAAS;AACT,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAChB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AACjB,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,GAAA,EAAI,GAAI,OAAA;AAC9B,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,2BAAA,CAA4B,OAAO,CAAA;AACjE,IAAA,IAAA,CAAK,qBACH,gBAAA,CAAiB,IAAA,GAAO,IAAI,gBAAA,GAAmB,IAAA,CAAK,2BAA2B,OAAO,CAAA;AACxF,IAAA,OAAA,CAAQ,gBAAA,CAAiB,eAAA,EAAiB,IAAA,CAAK,WAAW,CAAA;AAC1D,IAAA,OAAA,CAAQ,gBAAA,CAAiB,kBAAA,EAAoB,IAAA,CAAK,WAAW,CAAA;AAC7D,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,MAAM,IAAA,CAAK,OAAA,IAAW,QAAA,GAAW,CAAA,GAAI,QAAA,GAAW,OAAA,GAAU,EAAE,CAAA;AAAA,EAC/E;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASS,WAAA,GAAc,CAAC,KAAA,KAAuB;AAC7C,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,QAAA,EAAU;AACpC,IAAA,MAAM,eAAA,GAAkB,KAAA;AAGxB,IAAA,IAAI,gBAAgB,aAAA,EAAe;AAEnC,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,MAAA,MAAM,eAAe,eAAA,CAAgB,YAAA;AACrC,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,2BAAA,CAA4B,IAAA,CAAK,QAAQ,CAAA;AAK7D,MAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC9B,MAAA,IAAI,CAAC,IAAA,CAAK,kBAAA,CAAmB,MAAA,CAAO,YAAY,CAAA,EAAG;AACnD,MAAA,IAAI,IAAA,CAAK,kBAAA,CAAmB,IAAA,GAAO,CAAA,EAAG;AAKtC,MAAA,IAAI,MAAA,CAAO,OAAO,CAAA,EAAG;AACnB,QAAA,IAAA,CAAK,kBAAA,GAAqB,MAAA;AAC1B,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,OAAA,EAAQ;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAK,GAAA,EAAI,IAAK,IAAA,CAAK,SAAA,OAAgB,OAAA,EAAQ;AAAA,EACjD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,4BAA4B,OAAA,EAA0C;AACpE,IAAA,IAAI,CAAC,WAAW,OAAO,OAAA,CAAQ,kBAAkB,UAAA,EAAY,2BAAW,GAAA,EAAI;AAE5E,IAAA,IAAI;AACF,MAAA,MAAM,aAAa,OAAA,CAAQ,aAAA,EAAc,CAAE,OAAA,CAAQ,CAAC,SAAA,KAAc;AAChE,QAAA,IAAI,UAAU,SAAA,KAAc,MAAA,IAAU,UAAU,SAAA,KAAc,UAAA,SAAmB,EAAC;AAClF,QAAA,MAAM,SAAS,SAAA,CAAU,MAAA;AAIzB,QAAA,IAAI,MAAA,EAAQ,aAAA,EAAe,OAAO,EAAC;AACnC,QAAA,MAAM,SAAS,MAAA,EAAQ,MAAA;AACvB,QAAA,IAAI,MAAA,IAAU,MAAA,KAAW,OAAA,EAAS,OAAO,EAAC;AAC1C,QAAA,MAAM,WAAY,SAAA,CAAqC,kBAAA;AACvD,QAAA,OAAO,OAAO,aAAa,QAAA,IAAY,QAAA,CAAS,SAAS,CAAA,GAAI,CAAC,QAAQ,CAAA,GAAI,EAAC;AAAA,MAC7E,CAAC,CAAA;AACD,MAAA,OAAO,IAAI,IAAI,UAAU,CAAA;AAAA,IAC3B,CAAA,CAAA,MAAQ;AAGN,MAAA,2BAAW,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA,EAGA,2BAA2B,OAAA,EAAiD;AAC1E,IAAA,IAAI,OAAA,CAAQ,KAAK,CAAC,EAAE,UAAS,KAAM,QAAA,KAAa,KAAK,CAAA,EAAG,OAAO,IAAA;AAC/D,IAAA,MAAM,UAAU,IAAI,GAAA;AAAA,MAClB,OAAA,CAAQ,MAAA,CAAO,CAAC,EAAE,SAAQ,KAAM,OAAA,GAAU,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,EAAE,QAAA,OAAe,QAAQ;AAAA,KAC7E;AAIA,IAAA,OAAO,OAAA,CAAQ,IAAA,GAAO,CAAA,GAAI,OAAA,GAAU,IAAA;AAAA,EACtC;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,MAAM,WAAW,IAAA,CAAK,SAAA;AACtB,IAAA,IAAI,CAAC,QAAA,EAAU;AACf,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAA,CAAK,SAAA,EAAU;AACf,IAAA,QAAA,EAAS;AAAA,EACX;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,QAAA,EAAU,mBAAA,CAAoB,eAAA,EAAiB,IAAA,CAAK,WAAW,CAAA;AACpE,IAAA,IAAA,CAAK,QAAA,EAAU,mBAAA,CAAoB,kBAAA,EAAoB,IAAA,CAAK,WAAW,CAAA;AACvE,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,IAAA,IAAA,CAAK,SAAA,GAAY,CAAA;AAAA,EACnB;AACF,CAAA;;;AClPA,IAAM,QAAA,GAAW,CAAC,KAAA,KAA4B,KAAA,CAAM,MAAM,KAAK,CAAA,CAAE,OAAO,OAAO,CAAA;AAwCxE,IAAM,oBAAA,GAAN,cAAmC,UAAA,CAAwB;AAAA,EAChE,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACnC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACvC,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACrC,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACnC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACvC,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACrC,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GACtC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,OAAA,EAAS,SAAS,QAAQ,CAAA;AAAA,EAC5C,OAAO,MAAA,GAAS,CAAC,SAAA,EAAW,MAAM,CAAA;AAAA;AAAA,EAWzB,WAAA,GAAc,IAAI,oBAAA,EAAqB;AAAA,EAChD,MAAA,GAAwB,IAAA;AAAA,EAEf,OAAA,GAAgB;AAGvB,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,IAAA,CAAK,QAAQ,YAAA,CAAa,uBAAA,EAAyB,KAAK,OAAA,CAAQ,MAAA,GAAS,SAAS,SAAS,CAAA;AAAA,EAC7F;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AAAA,EACnB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AAAA,EACnB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,uBAAuB,CAAA;AAC/D,IAAA,IAAI,KAAA,KAAU,SAAA,IAAa,KAAA,KAAU,UAAA,OAAiB,KAAA,EAAM;AAAA,cAClD,KAAA,EAAM;AAAA,EAClB;AAAA,EAEA,KAAK,IAAA,EAA+B;AAClC,IAAA,IAAA,CAAK,OAAA,EAAQ;AACb,IAAA,MAAM,UAAU,IAAA,KAAS,OAAA;AACzB,IAAA,IAAI,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,KAAA;AACnC,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,uBAAA,EAAyB,OAAA,GAAU,aAAa,SAAS,CAAA;AAEnF,IAAA,IAAI,sBAAqB,EAAG;AAC1B,MAAA,IAAA,CAAK,QAAQ,IAAI,CAAA;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,OAAA,GAAU,IAAA,CAAK,UAAA,GAAa,IAAA,CAAK,UAAA;AAC9C,IAAA,MAAM,IAAA,GAAO,OAAA,GAAU,IAAA,CAAK,cAAA,GAAiB,IAAA,CAAK,cAAA;AAClD,IAAA,MAAM,EAAA,GAAK,OAAA,GAAU,IAAA,CAAK,YAAA,GAAe,IAAA,CAAK,YAAA;AAE9C,IAAA,IAAA,CAAK,IAAA,CAAK,MAAM,IAAI,CAAA;AACpB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,MAAM;AAC5B,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,MAAA,IAAA,CAAK,QAAQ,IAAI,CAAA;AACjB,MAAA,IAAA,CAAK,KAAK,EAAE,CAAA;AAGZ,MAAA,IAAA,CAAK,WAAA,CAAY,KAAK,IAAA,CAAK,OAAA,EAAS,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAA,EAAG;AAAA,QAC5D,WAAW,IAAA,CAAK;AAAA,OACjB,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,QAAQ,IAAA,EAA+B;AACrC,IAAA,IAAA,CAAK,MAAA,EAAO;AACZ,IAAA,IAAI,SAAS,OAAA,EAAS;AACpB,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,uBAAA,EAAyB,SAAS,CAAA;AAC5D,MAAA,IAAA,CAAK,SAAS,SAAA,EAAW,EAAE,MAAA,EAAQ,IAAI,CAAA;AAAA,IACzC,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,QAAQ,MAAA,GAAS,IAAA;AACtB,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,uBAAA,EAAyB,MAAM,CAAA;AACzD,MAAA,IAAA,CAAK,SAAS,MAAA,EAAQ,EAAE,MAAA,EAAQ,IAAI,CAAA;AAAA,IACtC;AAAA,EACF;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,IAAA,CAAK,WAAW,IAAA,EAAM;AACxB,MAAA,IAAA,CAAK,UAAA,CAAW,KAAK,MAAM,CAAA;AAC3B,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,YAAY,MAAA,EAAO;AACxB,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA,EAEA,QAAQ,KAAA,EAAuB;AAC7B,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA;AACrC,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG,IAAA,CAAK,QAAQ,SAAA,CAAU,GAAA,CAAI,GAAG,MAAM,CAAA;AAAA,EAC7D;AAAA,EAEA,WAAW,KAAA,EAAuB;AAChC,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA;AACrC,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG,IAAA,CAAK,QAAQ,SAAA,CAAU,MAAA,CAAO,GAAG,MAAM,CAAA;AAAA,EAChE;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA;AAAA,MACH,IAAA,CAAK,UAAA;AAAA,MACL,IAAA,CAAK,cAAA;AAAA,MACL,IAAA,CAAK,YAAA;AAAA,MACL,IAAA,CAAK,UAAA;AAAA,MACL,IAAA,CAAK,cAAA;AAAA,MACL,IAAA,CAAK;AAAA,KACP;AAAA,EACF;AAAA,EAEA,KAAK,QAAA,EAA8B;AACjC,IAAA,IAAI,OAAO,MAAA,CAAO,qBAAA,KAA0B,UAAA,EAAY;AACtD,MAAA,OAAO,MAAA,CAAO,qBAAA,CAAsB,MAAM,QAAA,EAAU,CAAA;AAAA,IACtD;AACA,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,CAAC,CAAA;AAAA,EACtC;AAAA,EAEA,WAAW,EAAA,EAAkB;AAC3B,IAAA,IAAI,OAAO,MAAA,CAAO,oBAAA,KAAyB,UAAA,EAAY,MAAA,CAAO,qBAAqB,EAAE,CAAA;AAAA,SAChF,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EAC7B;AACF","file":"transition_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * Each of those controllers used to duplicate the same guarded `matchMedia`\n * read; this one-liner keeps the media query string and the environment guard\n * single-sourced. The preference is intentionally re-read on every call — the\n * controllers check it at each animation/scroll start (WCAG 2.2 **2.3.3**), so\n * flipping the OS setting takes effect immediately without any listener or\n * cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { TransitionCompletion } from \"../utils/transition_completion\";\n\n/** Splits a space-separated class-list value into individual, non-empty tokens. */\nconst tokensOf = (value: string): string[] => value.split(/\\s+/).filter(Boolean);\n\n/**\n * Headless **enter/leave transition base**: stages CSS classes for showing and hiding\n * an element (the shared substrate other widgets can lean on instead of hand-rolling\n * it). Counterpart to Headless UI `Transition` / Alpine `x-transition` (no APG pattern;\n * honors WCAG 2.2 **2.3.3** via `prefers-reduced-motion` and keeps the visual state in\n * sync with `hidden`).\n *\n * Markup contract (identifier: `stimeo--transition`):\n * <div data-controller=\"stimeo--transition\"\n * data-stimeo--transition-enter-value=\"ease-out duration-200\"\n * data-stimeo--transition-enter-from-value=\"opacity-0\"\n * data-stimeo--transition-enter-to-value=\"opacity-100\"\n * data-stimeo--transition-leave-value=\"ease-in duration-150\"\n * data-stimeo--transition-leave-from-value=\"opacity-100\"\n * data-stimeo--transition-leave-to-value=\"opacity-0\" hidden>…</div>\n *\n * `enter()` unhides the element, applies `enter` + `enterFrom`, then on the next frame\n * swaps `enterFrom` → `enterTo` so the CSS transition runs, and settles to `entered`\n * once the transition completes. `leave()` mirrors it and re-applies `hidden`.\n * `toggle()` reverses the current direction. The element carries `data-transition-state`\n * (`entering` / `entered` / `leaving` / `left`) and `entered` / `left` events fire on\n * completion.\n *\n * @remarks\n * Behavior only — the animation itself is the consumer's CSS; this controls *when* the\n * stage classes are applied. Completion is owned by the shared\n * {@link TransitionCompletion}: every declared transition property must settle\n * (`transitionend` / `transitioncancel`, pseudo-element events excluded) with a\n * `max(duration + delay) + 50ms` bounded fallback, and a computed 0ms transition\n * settles synchronously at the staging frame. A positive `timeout` Value replaces the\n * fallback verbatim and keeps the wait armed even for a computed 0ms transition.\n * Under `prefers-reduced-motion: reduce` it switches instantly (no staging). An\n * interrupting call cancels the in-flight transition and starts the new one. State\n * lives solely in `hidden` / `data-transition-state`, and `connect()` reconciles to a\n * stable state (stripping any half-applied stage classes from a Turbo cache); the\n * terminal-event listeners, rAF, and fallback timer are released on `disconnect()`\n * (Turbo navigation included).\n */\nexport class TransitionController extends Controller<HTMLElement> {\n static override values = {\n enter: { type: String, default: \"\" },\n enterFrom: { type: String, default: \"\" },\n enterTo: { type: String, default: \"\" },\n leave: { type: String, default: \"\" },\n leaveFrom: { type: String, default: \"\" },\n leaveTo: { type: String, default: \"\" },\n timeout: { type: Number, default: 0 },\n };\n static actions = [\"enter\", \"leave\", \"toggle\"] as const;\n static events = [\"entered\", \"left\"] as const;\n\n declare enterValue: string;\n declare enterFromValue: string;\n declare enterToValue: string;\n declare leaveValue: string;\n declare leaveFromValue: string;\n declare leaveToValue: string;\n declare timeoutValue: number;\n\n /** Owns the cancellable completion wait (terminal events + bounded fallback). */\n readonly #transition = new TransitionCompletion();\n #rafId: number | null = null;\n\n override connect(): void {\n // Drop any half-applied stage classes a cache may have captured, then settle the\n // state hook to match the element's current visibility.\n this.#strip();\n this.element.setAttribute(\"data-transition-state\", this.element.hidden ? \"left\" : \"entered\");\n }\n\n override disconnect(): void {\n this.#cancel();\n }\n\n /** Shows the element with the enter transition. */\n enter(): void {\n this.#run(\"enter\");\n }\n\n /** Hides the element with the leave transition. */\n leave(): void {\n this.#run(\"leave\");\n }\n\n /** Reverses the current direction (enter when hidden/leaving, else leave). */\n toggle(): void {\n const state = this.element.getAttribute(\"data-transition-state\");\n if (state === \"entered\" || state === \"entering\") this.leave();\n else this.enter();\n }\n\n #run(kind: \"enter\" | \"leave\"): void {\n this.#cancel();\n const isEnter = kind === \"enter\";\n if (isEnter) this.element.hidden = false;\n this.element.setAttribute(\"data-transition-state\", isEnter ? \"entering\" : \"leaving\");\n\n if (prefersReducedMotion()) {\n this.#finish(kind);\n return;\n }\n\n const base = isEnter ? this.enterValue : this.leaveValue;\n const from = isEnter ? this.enterFromValue : this.leaveFromValue;\n const to = isEnter ? this.enterToValue : this.leaveToValue;\n\n this.#add(base, from);\n this.#rafId = this.#raf(() => {\n this.#rafId = null;\n this.#remove(from);\n this.#add(to);\n // The consumer's `timeout` Value (positive) replaces the auto-computed\n // fallback so an author-declared budget always wins over computed styles.\n this.#transition.wait(this.element, () => this.#finish(kind), {\n timeoutMs: this.timeoutValue,\n });\n });\n }\n\n /** Settles the element into the completed state, clearing the stage classes. */\n #finish(kind: \"enter\" | \"leave\"): void {\n this.#strip();\n if (kind === \"enter\") {\n this.element.setAttribute(\"data-transition-state\", \"entered\");\n this.dispatch(\"entered\", { detail: {} });\n } else {\n this.element.hidden = true;\n this.element.setAttribute(\"data-transition-state\", \"left\");\n this.dispatch(\"left\", { detail: {} });\n }\n }\n\n /** Cancels any in-flight transition (interruption / teardown). */\n #cancel(): void {\n if (this.#rafId !== null) {\n this.#cancelRaf(this.#rafId);\n this.#rafId = null;\n }\n this.#transition.cancel();\n this.#strip();\n }\n\n #add(...lists: string[]): void {\n const tokens = lists.flatMap(tokensOf);\n if (tokens.length > 0) this.element.classList.add(...tokens);\n }\n\n #remove(...lists: string[]): void {\n const tokens = lists.flatMap(tokensOf);\n if (tokens.length > 0) this.element.classList.remove(...tokens);\n }\n\n /** Removes every stage class so no half-applied state lingers. */\n #strip(): void {\n this.#remove(\n this.enterValue,\n this.enterFromValue,\n this.enterToValue,\n this.leaveValue,\n this.leaveFromValue,\n this.leaveToValue,\n );\n }\n\n #raf(callback: () => void): number {\n if (typeof window.requestAnimationFrame === \"function\") {\n return window.requestAnimationFrame(() => callback());\n }\n return window.setTimeout(callback, 0);\n }\n\n #cancelRaf(id: number): void {\n if (typeof window.cancelAnimationFrame === \"function\") window.cancelAnimationFrame(id);\n else window.clearTimeout(id);\n }\n}\n"]}
|