stimeo-ui 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +178 -0
- package/README.md +120 -0
- package/dist/cable/index.js +63 -15
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +8 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +43 -2
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +9 -8
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +85 -23
- package/dist/controllers/calendar_controller.js +323 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +54 -10
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +56 -11
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +74 -15
- package/dist/controllers/currency_input_controller.js +205 -58
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +34 -6
- package/dist/controllers/date_range_picker_controller.js +139 -27
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +14 -1
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +305 -46
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +16 -2
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +19 -2
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.js +0 -2
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +14 -1
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +84 -13
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +112 -41
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +69 -2
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +5 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +0 -1
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +6 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +11 -1
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +75 -5
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +16 -3
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +52 -15
- package/dist/controllers/toast_controller.js +337 -54
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +4588 -1535
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +525 -59
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/keyed_timers.ts","../../src/utils/reduced_motion.ts","../../src/controllers/highlight_controller.ts"],"names":[],"mappings":"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","file":"highlight_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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Keeps at most one pending timer per key, and releases them all together.\n *\n * A region, a row, a message or a speaker that is armed again while its earlier\n * timer is still pending must end up with exactly one timer deciding when its\n * state ends. Two timers on one key means the earlier deadline wins: it cuts the\n * new emphasis short, clears text written after it, or drops a speaker who is\n * still typing — and then reports an ending for a state that is already gone.\n * Arming through this registry cancels the key's earlier timer in the same step,\n * and no timer id ever reaches the caller.\n *\n * An entry lives exactly as long as its timer: it goes as the timer fires, and\n * the whole ledger goes on `clearAll()`. That is a correctness rule, not\n * tidiness — a platform may hand an id released by `clearTimeout` to the next\n * timer it creates, so an entry that outlived its timer would let a later\n * `clear()` on that key cancel whatever timer now holds the id, on some other\n * key entirely.\n *\n * The ledger is a plain `Map`, and the key may be anything. Weak keys would\n * retain nothing less: while a timer is pending the platform already holds the\n * key through the callback, and the entry is gone the moment the timer is.\n *\n * Scope is the one-per-key timer only. What the timer means, and the roster of\n * keys it is armed for, stay with the consumer, so a second kind of timer on the\n * same keys (a transition wait, an animation frame) is a second registry, and a\n * timer that belongs to no key stays on a plain `SafeTimeout`.\n *\n * @example\n * ```ts\n * readonly #expiry = new KeyedTimers<string>();\n *\n * #onSignal(name: string): void {\n * this.#expiry.set(name, () => this.#drop(name), this.#timeout);\n * }\n *\n * disconnect(): void {\n * this.#expiry.clearAll();\n * }\n * ```\n */\nexport class KeyedTimers<K> {\n readonly #timers = new SafeTimeout();\n\n /** The pending timer of each key; an entry lives exactly as long as its timer. */\n readonly #pending = new Map<K, number>();\n\n /**\n * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had\n * pending. The entry is dropped before the callback runs, so the callback sees\n * the key unarmed and may arm it again for the next round.\n */\n set(key: K, callback: () => void, delay: number): void {\n this.clear(key);\n const id = this.#timers.set(() => {\n this.#pending.delete(key);\n callback();\n }, delay);\n this.#pending.set(key, id);\n }\n\n /**\n * Cancels `key`'s pending timer, if it has one.\n *\n * A timer id is a positive integer, so `-1` stands for \"nothing pending\" and\n * the registry ignores an id it does not own — the unarmed case needs no\n * branch of its own, and no other key's timer can be reached from here.\n */\n clear(key: K): void {\n this.#timers.clear(this.#pending.get(key) ?? -1);\n this.#pending.delete(key);\n }\n\n /**\n * Cancels every pending timer and forgets every key. Call this from a\n * controller's `disconnect()` so no timer, and no entry, outlives the element.\n */\n clearAll(): void {\n this.#timers.clearAll();\n this.#pending.clear();\n }\n\n /** Whether `key` has a timer pending. */\n has(key: K): boolean {\n return this.#pending.has(key);\n }\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or 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","import { Controller } from \"@hotwired/stimulus\";\nimport { KeyedTimers } from \"../utils/keyed_timers\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/**\n * The connection whose removal timer currently owns an element's `data-highlight`\n * hook. Shared by every instance, so a row moved from one watched container into\n * another still has exactly one timer deciding when its emphasis ends.\n *\n * Only the keys are weak: an entry keeps its controller — and through it that\n * controller's element — reachable for as long as the row is in the DOM. So an entry\n * is dropped as soon as the timer that owns it is released or fires, and `connect()`\n * drops the claims the connection before it left behind.\n */\nconst hookOwners = new WeakMap<Element, HighlightController>();\n\n/**\n * Headless \"highlight on insert\" behavior: briefly flags a freshly inserted element\n * with `data-highlight` so CSS can flash / fade it in, then removes the flag after\n * `duration` ms (no dedicated APG pattern; a purely visual emphasis that honors the\n * WCAG \"animation from interactions\" practice via `prefers-reduced-motion`).\n *\n * Markup contract (identifier: `stimeo--highlight`):\n * <!-- self-highlight: put it on the inserted row itself -->\n * <li data-controller=\"stimeo--highlight\">New item</li>\n *\n * <!-- container mode: watch for added children and highlight each -->\n * <ul data-controller=\"stimeo--highlight\" data-stimeo--highlight-observe-value=\"true\"></ul>\n *\n * In the default (self) mode the controller element is highlighted once on\n * `connect()`. With `observe`, a `MutationObserver` instead highlights every element\n * child added to the container (e.g. Turbo Stream appends). Each highlight sets\n * `data-highlight=\"true\"`, dispatches `start`, and after `duration` removes it and\n * dispatches `end` — both carry `detail.element`.\n *\n * `start` and `end` dispatch `{ element }`.\n *\n * @remarks\n * Behavior only — it ships no color or animation, just the hook (pair with CSS, and\n * with Announcer / Flash for non-visual notice). Under `prefers-reduced-motion: reduce`\n * the emphasis is suppressed entirely (the element simply appears), so no hook or\n * event is emitted. A hook never outlives the connection that set it: the observer and\n * pending timers are torn down on `disconnect()` (Turbo navigation included), and\n * `connect()` clears any hook that arrived with the DOM — a restored `turbo:before-cache`\n * snapshot, an in-page move — because the timer that would have removed it is gone.\n * An element carries at most one emphasis at a time: highlighting it again — a reorder\n * inside one container, or a move into another watched one — takes the hook over and\n * releases the timer that held it, so `duration` is measured from the latest highlight\n * and `end` fires once per emphasis.\n */\nexport class HighlightController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1500 },\n observe: { type: Boolean, default: false },\n };\n static events = [\"start\", \"end\"] as const;\n\n declare durationValue: number;\n declare observeValue: boolean;\n\n /**\n * The removal timer this connection has outstanding for each element. Which\n * connection owns an element's hook is answered by the shared owner registry above.\n */\n readonly #removal = new KeyedTimers<HTMLElement>();\n #observer: MutationObserver | null = null;\n\n override connect(): void {\n // A hook that arrived with the DOM was written by an earlier connection, whose\n // removal timer is gone; this connection owns no timer for it either, so nothing\n // would ever take it off. Drop it before anything else — in self mode\n // `#highlight()` re-adds it with a fresh timer below, except under reduced\n // motion, where the element must carry no hook at all.\n this.#clearArrivedHook(this.element);\n if (this.observeValue) {\n // A container highlights its children, never itself, so the same reasoning\n // applies to every child present before the observer starts watching.\n for (const child of this.element.children) this.#clearArrivedHook(child);\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.element, { childList: true });\n }\n return;\n }\n this.#highlight(this.element);\n }\n\n override disconnect(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#removal.clearAll();\n }\n\n /** Drops a hook that arrived with the DOM, along with this connection's claim on it. */\n #clearArrivedHook(el: Element): void {\n // Another connection may still hold a live timer for this element, and that timer\n // has to stay reachable so the next highlight can release it. Only our own claim,\n // whose timer went down with the previous disconnect, is dropped here.\n if (hookOwners.get(el) === this) hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n }\n\n /** Highlights every element child added by a childList mutation. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) this.#highlight(node);\n }\n }\n }\n\n /** Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion). */\n #highlight(el: HTMLElement): void {\n // Reduced motion: suppress the emphasis so the element just appears — no hook,\n // no timer, no events, nothing to transition.\n if (prefersReducedMotion()) return;\n\n // A second highlight of the same element — a re-insert, which is the shape a\n // reorder takes — owns the hook from here on. Release the first timer, or it\n // ends the new emphasis early and then reports an `end` for a hook already gone.\n this.#releasePending(el);\n el.setAttribute(\"data-highlight\", \"true\");\n this.dispatch(\"start\", { target: el, detail: { element: el } });\n this.#removal.set(\n el,\n () => {\n hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n this.dispatch(\"end\", { target: el, detail: { element: el } });\n },\n this.durationValue,\n );\n hookOwners.set(el, this);\n }\n\n /**\n * Releases whichever removal timer holds `el`'s hook. The row may have been\n * highlighted inside a different watched container before it moved here, and that\n * container's timer is reachable only through the shared owner registry.\n */\n #releasePending(el: HTMLElement): void {\n const owner = hookOwners.get(el);\n if (owner !== undefined && owner !== this) owner.#cancelPending(el);\n this.#cancelPending(el);\n }\n\n /** Releases `el`'s pending removal timer, if it has one, and this connection's claim. */\n #cancelPending(el: HTMLElement): void {\n this.#removal.clear(el);\n hookOwners.delete(el);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/keyed_timers.ts","../../src/utils/reduced_motion.ts","../../src/utils/transient_hooks.ts","../../src/controllers/highlight_controller.ts"],"names":[],"mappings":"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","file":"highlight_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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Keeps at most one pending timer per key, and releases them all together.\n *\n * A region, a row, a message or a speaker that is armed again while its earlier\n * timer is still pending must end up with exactly one timer deciding when its\n * state ends. Two timers on one key means the earlier deadline wins: it cuts the\n * new emphasis short, clears text written after it, or drops a speaker who is\n * still typing — and then reports an ending for a state that is already gone.\n * Arming through this registry cancels the key's earlier timer in the same step,\n * and no timer id ever reaches the caller.\n *\n * An entry lives exactly as long as its timer: it goes as the timer fires, and\n * the whole ledger goes on `clearAll()`. That is a correctness rule, not\n * tidiness — a platform may hand an id released by `clearTimeout` to the next\n * timer it creates, so an entry that outlived its timer would let a later\n * `clear()` on that key cancel whatever timer now holds the id, on some other\n * key entirely.\n *\n * The ledger is a plain `Map`, and the key may be anything. Weak keys would\n * retain nothing less: while a timer is pending the platform already holds the\n * key through the callback, and the entry is gone the moment the timer is.\n *\n * Scope is the one-per-key timer only. What the timer means, and the roster of\n * keys it is armed for, stay with the consumer, so a second kind of timer on the\n * same keys (a transition wait, an animation frame) is a second registry, and a\n * timer that belongs to no key stays on a plain `SafeTimeout`.\n *\n * @example\n * ```ts\n * readonly #expiry = new KeyedTimers<string>();\n *\n * #onSignal(name: string): void {\n * this.#expiry.set(name, () => this.#drop(name), this.#timeout);\n * }\n *\n * disconnect(): void {\n * this.#expiry.clearAll();\n * }\n * ```\n */\nexport class KeyedTimers<K> {\n readonly #timers = new SafeTimeout();\n\n /** The pending timer of each key; an entry lives exactly as long as its timer. */\n readonly #pending = new Map<K, number>();\n\n /**\n * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had\n * pending. The entry is dropped before the callback runs, so the callback sees\n * the key unarmed and may arm it again for the next round.\n */\n set(key: K, callback: () => void, delay: number): void {\n this.clear(key);\n const id = this.#timers.set(() => {\n this.#pending.delete(key);\n callback();\n }, delay);\n this.#pending.set(key, id);\n }\n\n /**\n * Cancels `key`'s pending timer, if it has one.\n *\n * A timer id is a positive integer, so `-1` stands for \"nothing pending\" and\n * the registry ignores an id it does not own — the unarmed case needs no\n * branch of its own, and no other key's timer can be reached from here.\n */\n clear(key: K): void {\n this.#timers.clear(this.#pending.get(key) ?? -1);\n this.#pending.delete(key);\n }\n\n /**\n * Cancels every pending timer and forgets every key. Call this from a\n * controller's `disconnect()` so no timer, and no entry, outlives the element.\n */\n clearAll(): void {\n this.#timers.clearAll();\n this.#pending.clear();\n }\n\n /** Whether `key` has a timer pending. */\n has(key: K): boolean {\n return this.#pending.has(key);\n }\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers.\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or 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 * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: that event fires for a navigation only, so an in-page move — the\n * other half of the population — never reaches it, and a hook that arrived with\n * a moved element would survive. `connect()` covers both, because both end in a\n * connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n *\n * Every member comment inside this class body is inlined into each consumer\n * bundle (`tsup` builds with `splitting: false`), so rationale belongs in this\n * file's own block, which `dist` drops, and only the contract belongs inside.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { KeyedTimers } from \"../utils/keyed_timers\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-highlight\"] });\n\n/**\n * The connection whose removal timer currently owns an element's `data-highlight`\n * hook. Shared by every instance, so a row moved from one watched container into\n * another still has exactly one timer deciding when its emphasis ends.\n *\n * Only the keys are weak: an entry keeps its controller — and through it that\n * controller's element — reachable for as long as the row is in the DOM. So an entry\n * is dropped as soon as the timer that owns it is released or fires, and `connect()`\n * drops the claims the connection before it left behind.\n */\nconst hookOwners = new WeakMap<Element, HighlightController>();\n\n/**\n * Headless \"highlight on insert\" behavior: briefly flags a freshly inserted element\n * with `data-highlight` so CSS can flash / fade it in, then removes the flag after\n * `duration` ms (no dedicated APG pattern; a purely visual emphasis that honors the\n * WCAG \"animation from interactions\" practice via `prefers-reduced-motion`).\n *\n * Markup contract (identifier: `stimeo--highlight`):\n * <!-- self-highlight: put it on the inserted row itself -->\n * <li data-controller=\"stimeo--highlight\">New item</li>\n *\n * <!-- container mode: watch for added children and highlight each -->\n * <ul data-controller=\"stimeo--highlight\" data-stimeo--highlight-observe-value=\"true\"></ul>\n *\n * In the default (self) mode the controller element is highlighted once on\n * `connect()`. With `observe`, a `MutationObserver` instead highlights every element\n * child added to the container (e.g. Turbo Stream appends). Each highlight sets\n * `data-highlight=\"true\"`, dispatches `start`, and after `duration` removes it and\n * dispatches `end` — both carry `detail.element`.\n *\n * `start` and `end` dispatch `{ element }`.\n *\n * @remarks\n * Behavior only — it ships no color or animation, just the hook (pair with CSS, and\n * with Announcer / Flash for non-visual notice). Under `prefers-reduced-motion: reduce`\n * the emphasis is suppressed entirely (the element simply appears), so no hook or\n * event is emitted. A hook never outlives the connection that set it: the observer and\n * pending timers are torn down on `disconnect()` (Turbo navigation included), and\n * `connect()` clears any hook that arrived with the DOM — a restored `turbo:before-cache`\n * snapshot, an in-page move — because the timer that would have removed it is gone.\n * An element carries at most one emphasis at a time: highlighting it again — a reorder\n * inside one container, or a move into another watched one — takes the hook over and\n * releases the timer that held it, so `duration` is measured from the latest highlight\n * and `end` fires once per emphasis.\n */\nexport class HighlightController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1500 },\n observe: { type: Boolean, default: false },\n };\n static events = [\"start\", \"end\"] as const;\n\n declare durationValue: number;\n declare observeValue: boolean;\n\n /**\n * The removal timer this connection has outstanding for each element. Which\n * connection owns an element's hook is answered by the shared owner registry above.\n */\n readonly #removal = new KeyedTimers<HTMLElement>();\n #observer: MutationObserver | null = null;\n\n override connect(): void {\n // A hook that arrived with the DOM was written by an earlier connection, whose\n // removal timer is gone; this connection owns no timer for it either, so nothing\n // would ever take it off. Drop it before anything else — in self mode\n // `#highlight()` re-adds it with a fresh timer below, except under reduced\n // motion, where the element must carry no hook at all.\n this.#clearArrivedHook(this.element);\n if (this.observeValue) {\n // A container highlights its children, never itself, so the same reasoning\n // applies to every child present before the observer starts watching.\n for (const child of this.element.children) this.#clearArrivedHook(child);\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.element, { childList: true });\n }\n return;\n }\n this.#highlight(this.element);\n }\n\n override disconnect(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n this.#removal.clearAll();\n }\n\n /** Drops a hook that arrived with the DOM, along with this connection's claim on it. */\n #clearArrivedHook(el: Element): void {\n // Another connection may still hold a live timer for this element, and that timer\n // has to stay reachable so the next highlight can release it. Only our own claim,\n // whose timer went down with the previous disconnect, is dropped here.\n if (hookOwners.get(el) === this) hookOwners.delete(el);\n TRANSIENT.reset(el);\n }\n\n /** Highlights every element child added by a childList mutation. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) this.#highlight(node);\n }\n }\n }\n\n /**\n * Flags `el` with `data-highlight` and schedules its removal (unless reduced-motion).\n *\n * @stimeoRuntimeOnly `duration` is the length of the one emphasis this call arms.\n */\n #highlight(el: HTMLElement): void {\n // Reduced motion: suppress the emphasis so the element just appears — no hook,\n // no timer, no events, nothing to transition.\n if (prefersReducedMotion()) return;\n\n // A second highlight of the same element — a re-insert, which is the shape a\n // reorder takes — owns the hook from here on. Release the first timer, or it\n // ends the new emphasis early and then reports an `end` for a hook already gone.\n this.#releasePending(el);\n el.setAttribute(\"data-highlight\", \"true\");\n this.dispatch(\"start\", { target: el, detail: { element: el } });\n this.#removal.set(\n el,\n () => {\n hookOwners.delete(el);\n el.removeAttribute(\"data-highlight\");\n this.dispatch(\"end\", { target: el, detail: { element: el } });\n },\n this.durationValue,\n );\n hookOwners.set(el, this);\n }\n\n /**\n * Releases whichever removal timer holds `el`'s hook. The row may have been\n * highlighted inside a different watched container before it moved here, and that\n * container's timer is reachable only through the shared owner registry.\n */\n #releasePending(el: HTMLElement): void {\n const owner = hookOwners.get(el);\n if (owner !== undefined && owner !== this) owner.#cancelPending(el);\n this.#cancelPending(el);\n }\n\n /** Releases `el`'s pending removal timer, if it has one, and this connection's claim. */\n #cancelPending(el: HTMLElement): void {\n this.#removal.clear(el);\n hookOwners.delete(el);\n }\n}\n"]}
|
|
@@ -47,6 +47,13 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
47
47
|
* scroll-parent ancestor (or the window) closes the card, the usual convention for
|
|
48
48
|
* anchored popups. Covers keyboard/programmatic scroll and scrollbar-drag, which the
|
|
49
49
|
* pointer-leave close cannot. Off by default.
|
|
50
|
+
* - Each move of the open state is reported: `stimeo--hover-card:open` and
|
|
51
|
+
* `stimeo--hover-card:close` dispatch `{ reason: StateReason }`, after the
|
|
52
|
+
* state attributes are written. The reason is taken from the event that
|
|
53
|
+
* started the move and survives the delay, so a `mouseleave` that closes
|
|
54
|
+
* `closeDelay` later still reports `"pointer"`. Both are informational, so
|
|
55
|
+
* neither is cancelable. A call that leaves the state where it already was,
|
|
56
|
+
* the normalization in {@link connect}, and {@link disconnect} are all silent.
|
|
50
57
|
*/
|
|
51
58
|
declare class HoverCardController extends Controller<HTMLElement> {
|
|
52
59
|
#private;
|
|
@@ -66,6 +73,7 @@ declare class HoverCardController extends Controller<HTMLElement> {
|
|
|
66
73
|
};
|
|
67
74
|
};
|
|
68
75
|
static actions: readonly ["close", "open"];
|
|
76
|
+
static events: readonly ["close", "open"];
|
|
69
77
|
readonly triggerTarget: HTMLElement;
|
|
70
78
|
readonly cardTarget: HTMLElement;
|
|
71
79
|
readonly hasTriggerTarget: boolean;
|
|
@@ -78,14 +86,14 @@ declare class HoverCardController extends Controller<HTMLElement> {
|
|
|
78
86
|
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
79
87
|
disconnect(): void;
|
|
80
88
|
/** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
|
|
81
|
-
open(): void;
|
|
89
|
+
open(event?: Event): void;
|
|
82
90
|
/**
|
|
83
91
|
* Schedules the card to close after `closeDelay`. Cancels a pending open. The
|
|
84
92
|
* delayed callback re-checks whether focus has landed inside the controller
|
|
85
93
|
* (e.g. a link in the card) and, if so, aborts the close — covering keyboard
|
|
86
94
|
* traversal that the pointer-only hoverable bridge cannot.
|
|
87
95
|
*/
|
|
88
|
-
close(): void;
|
|
96
|
+
close(event?: Event): void;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
export { HoverCardController };
|
|
@@ -164,6 +164,16 @@ function isScrollable(element) {
|
|
|
164
164
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/utils/state_reason.ts
|
|
168
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
169
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
170
|
+
function stateReasonFor(event) {
|
|
171
|
+
if (!event) return "api";
|
|
172
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
173
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
174
|
+
return "user";
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
// src/controllers/hover_card_controller.ts
|
|
168
178
|
var HoverCardController = class extends Controller {
|
|
169
179
|
static targets = ["trigger", "card"];
|
|
@@ -173,6 +183,7 @@ var HoverCardController = class extends Controller {
|
|
|
173
183
|
closeOnScroll: { type: Boolean, default: false }
|
|
174
184
|
};
|
|
175
185
|
static actions = ["close", "open"];
|
|
186
|
+
static events = ["close", "open"];
|
|
176
187
|
/** Pending open/close timers, with their IDs reset on every lifecycle boundary. */
|
|
177
188
|
#timers = new SafeTimeout();
|
|
178
189
|
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
@@ -181,14 +192,18 @@ var HoverCardController = class extends Controller {
|
|
|
181
192
|
#pendingClose = null;
|
|
182
193
|
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
183
194
|
#stopScrollDismiss = null;
|
|
195
|
+
/** Whether state moves are reported: set once `connect()` settled the baseline. */
|
|
196
|
+
#reporting = false;
|
|
184
197
|
/** Starts closed and discards any stale pending state from a prior connection. */
|
|
185
198
|
connect() {
|
|
186
199
|
this.#cancelOpen();
|
|
187
200
|
this.#cancelClose();
|
|
188
|
-
this.#conceal();
|
|
201
|
+
this.#conceal("api");
|
|
202
|
+
this.#reporting = true;
|
|
189
203
|
}
|
|
190
204
|
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
191
205
|
disconnect() {
|
|
206
|
+
this.#reporting = false;
|
|
192
207
|
this.#cancelOpen();
|
|
193
208
|
this.#cancelClose();
|
|
194
209
|
this.#escapeLayer.deactivate();
|
|
@@ -196,16 +211,17 @@ var HoverCardController = class extends Controller {
|
|
|
196
211
|
this.#stopScrollDismiss = null;
|
|
197
212
|
}
|
|
198
213
|
/** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */
|
|
199
|
-
open() {
|
|
214
|
+
open(event) {
|
|
215
|
+
const reason = stateReasonFor(event);
|
|
200
216
|
this.#cancelClose();
|
|
201
217
|
if (this.#isOpen || this.#pendingOpen !== null) return;
|
|
202
218
|
if (this.openDelayValue <= 0) {
|
|
203
|
-
this.#reveal();
|
|
219
|
+
this.#reveal(reason);
|
|
204
220
|
return;
|
|
205
221
|
}
|
|
206
222
|
this.#pendingOpen = this.#timers.set(() => {
|
|
207
223
|
this.#pendingOpen = null;
|
|
208
|
-
this.#reveal();
|
|
224
|
+
this.#reveal(reason);
|
|
209
225
|
}, this.openDelayValue);
|
|
210
226
|
}
|
|
211
227
|
/**
|
|
@@ -214,28 +230,37 @@ var HoverCardController = class extends Controller {
|
|
|
214
230
|
* (e.g. a link in the card) and, if so, aborts the close — covering keyboard
|
|
215
231
|
* traversal that the pointer-only hoverable bridge cannot.
|
|
216
232
|
*/
|
|
217
|
-
close() {
|
|
233
|
+
close(event) {
|
|
234
|
+
const reason = stateReasonFor(event);
|
|
218
235
|
this.#cancelOpen();
|
|
219
236
|
if (!this.#isOpen || this.#pendingClose !== null) return;
|
|
220
237
|
this.#pendingClose = this.#timers.set(() => {
|
|
221
238
|
this.#pendingClose = null;
|
|
222
239
|
if (this.element.contains(document.activeElement)) return;
|
|
223
|
-
this.#conceal();
|
|
240
|
+
this.#conceal(reason);
|
|
224
241
|
}, this.closeDelayValue);
|
|
225
242
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Reveals the card, reflects state, reports a move, and joins the Escape stack.
|
|
245
|
+
*
|
|
246
|
+
* @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what
|
|
247
|
+
* is shown does not depend on it.
|
|
248
|
+
*/
|
|
249
|
+
#reveal(reason) {
|
|
228
250
|
if (!this.hasCardTarget) return;
|
|
251
|
+
const was = this.#isOpen;
|
|
229
252
|
this.cardTarget.hidden = false;
|
|
230
253
|
this.cardTarget.setAttribute("data-state", "open");
|
|
231
254
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "true");
|
|
232
|
-
this.#
|
|
255
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
256
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss("escape") });
|
|
233
257
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
234
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
258
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss("scroll"));
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
|
-
/** Hides the card, reflects state, and leaves the Escape stack
|
|
238
|
-
#conceal() {
|
|
261
|
+
/** Hides the card, reflects state, reports a move, and leaves the Escape stack. */
|
|
262
|
+
#conceal(reason) {
|
|
263
|
+
const was = this.#isOpen;
|
|
239
264
|
this.#escapeLayer.deactivate();
|
|
240
265
|
this.#stopScrollDismiss?.();
|
|
241
266
|
this.#stopScrollDismiss = null;
|
|
@@ -244,12 +269,13 @@ var HoverCardController = class extends Controller {
|
|
|
244
269
|
this.cardTarget.setAttribute("data-state", "closed");
|
|
245
270
|
}
|
|
246
271
|
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
272
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
247
273
|
}
|
|
248
274
|
/** Cancels pending timers and conceals immediately (shared Escape path). */
|
|
249
|
-
#dismiss() {
|
|
275
|
+
#dismiss(reason) {
|
|
250
276
|
this.#cancelOpen();
|
|
251
277
|
this.#cancelClose();
|
|
252
|
-
this.#conceal();
|
|
278
|
+
this.#conceal(reason);
|
|
253
279
|
}
|
|
254
280
|
/** Cancels any pending open timer. */
|
|
255
281
|
#cancelOpen() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/utils/scroll_dismiss.ts","../../src/controllers/hover_card_controller.ts"],"names":[],"mappings":"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","file":"hover_card_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\n\n/**\n * Headless, accessible **hover card** behavior.\n *\n * Markup contract (identifier: `stimeo--hover-card`):\n * <span data-controller=\"stimeo--hover-card\">\n * <a href=\"/users/jane\" data-stimeo--hover-card-target=\"trigger\"\n * aria-expanded=\"false\" aria-controls=\"hc\"\n * data-action=\"mouseenter->stimeo--hover-card#open\n * mouseleave->stimeo--hover-card#close\n * focusin->stimeo--hover-card#open\n * focusout->stimeo--hover-card#close\">@jane</a>\n * <div id=\"hc\" data-stimeo--hover-card-target=\"card\"\n * data-action=\"mouseenter->stimeo--hover-card#open\n * mouseleave->stimeo--hover-card#close\n * focusin->stimeo--hover-card#open\n * focusout->stimeo--hover-card#close\" hidden>…</div>\n * </span>\n *\n * There is no dedicated APG pattern; this follows the **Disclosure** convention\n * (`aria-expanded`) for a hover/focus-opened, non-modal popover that *may* hold\n * interactive content (unlike a tooltip). The card is **not** a `role=\"dialog\"`:\n * it is supplementary, so its content must also be reachable from the trigger\n * itself. For a short text hint use `stimeo--tooltip`; for a click-opened\n * action panel use `stimeo--popover`.\n *\n * @remarks\n * Behavior only — placement is the consumer's CSS (static) or the opt-in\n * `stimeo-ui/positioning` module (dynamic); this controller never imports it.\n *\n * Behavior provided:\n * - Open on `mouseenter`/`focusin`, close on `mouseleave`/`focusout`, each gated by\n * `openDelay`/`closeDelay` to prevent accidental flicker.\n * - **Hoverable bridge**: binding open/close on the card cancels a pending close\n * when the pointer crosses into it. Matching focus actions on the card cancel\n * the trigger's pending close while focus is inside, then schedule close once\n * focus leaves the whole controller.\n * - **Dismissible**: while open, the card joins the shared `EscapeLayer`\n * stack, so `Escape` closes it regardless of where focus sits (card, trigger,\n * or elsewhere). The resolver ignores an Escape already consumed by an inner\n * handler and lets the most recently shown layer own the press, so one\n * keypress closes exactly one layer.\n * - Open/closed flips the trigger's `aria-expanded`, the card's `hidden`, and a\n * `data-state` (`open`/`closed`). Focus is never stolen on open.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the card, the usual convention for\n * anchored popups. Covers keyboard/programmatic scroll and scrollbar-drag, which the\n * pointer-leave close cannot. Off by default.\n */\nexport class HoverCardController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"card\"];\n static override values = {\n openDelay: { type: Number, default: 300 },\n closeDelay: { type: Number, default: 200 },\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly cardTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasCardTarget: boolean;\n declare readonly openDelayValue: number;\n declare readonly closeDelayValue: number;\n declare readonly closeOnScrollValue: boolean;\n\n /** Pending open/close timers, with their IDs reset on every lifecycle boundary. */\n readonly #timers = new SafeTimeout();\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n #pendingOpen: number | null = null;\n #pendingClose: number | null = null;\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n\n /** Starts closed and discards any stale pending state from a prior connection. */\n override connect(): void {\n this.#cancelOpen();\n this.#cancelClose();\n this.#conceal();\n }\n\n /** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#cancelOpen();\n this.#cancelClose();\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */\n open(): void {\n this.#cancelClose();\n if (this.#isOpen || this.#pendingOpen !== null) return;\n if (this.openDelayValue <= 0) {\n this.#reveal();\n return;\n }\n this.#pendingOpen = this.#timers.set(() => {\n this.#pendingOpen = null;\n this.#reveal();\n }, this.openDelayValue);\n }\n\n /**\n * Schedules the card to close after `closeDelay`. Cancels a pending open. The\n * delayed callback re-checks whether focus has landed inside the controller\n * (e.g. a link in the card) and, if so, aborts the close — covering keyboard\n * traversal that the pointer-only hoverable bridge cannot.\n */\n close(): void {\n this.#cancelOpen();\n if (!this.#isOpen || this.#pendingClose !== null) return;\n this.#pendingClose = this.#timers.set(() => {\n this.#pendingClose = null;\n if (this.element.contains(document.activeElement)) return;\n this.#conceal();\n }, this.closeDelayValue);\n }\n\n /** Reveals the card, reflects state, and joins the Escape stack / scroll watcher. */\n #reveal(): void {\n if (!this.hasCardTarget) return;\n this.cardTarget.hidden = false;\n this.cardTarget.setAttribute(\"data-state\", \"open\");\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n // No claims predicate: hover-revealed content is dismissible regardless of\n // where focus sits (WCAG 2.2 SC 1.4.13), so it always claims while open.\n this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss() });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());\n }\n }\n\n /** Hides the card, reflects state, and leaves the Escape stack / scroll watcher. */\n #conceal(): void {\n // Release the layer and observers first, unconditionally: if the card\n // target was removed from the DOM while open, an early return would leak\n // the stack entry and the scroll-dismiss listeners.\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasCardTarget) {\n this.cardTarget.hidden = true;\n this.cardTarget.setAttribute(\"data-state\", \"closed\");\n }\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Cancels pending timers and conceals immediately (shared Escape path). */\n #dismiss(): void {\n this.#cancelOpen();\n this.#cancelClose();\n this.#conceal();\n }\n\n /** Cancels any pending open timer. */\n #cancelOpen(): void {\n if (this.#pendingOpen !== null) {\n this.#timers.clear(this.#pendingOpen);\n this.#pendingOpen = null;\n }\n }\n\n /** Cancels any pending close timer. */\n #cancelClose(): void {\n if (this.#pendingClose !== null) {\n this.#timers.clear(this.#pendingClose);\n this.#pendingClose = null;\n }\n }\n\n /** Whether the card is currently visible. */\n get #isOpen(): boolean {\n return this.hasCardTarget && !this.cardTarget.hidden;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/utils/scroll_dismiss.ts","../../src/utils/state_reason.ts","../../src/controllers/hover_card_controller.ts"],"names":[],"mappings":"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","file":"hover_card_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","/**\n * Why a component's public open/closed state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible **hover card** behavior.\n *\n * Markup contract (identifier: `stimeo--hover-card`):\n * <span data-controller=\"stimeo--hover-card\">\n * <a href=\"/users/jane\" data-stimeo--hover-card-target=\"trigger\"\n * aria-expanded=\"false\" aria-controls=\"hc\"\n * data-action=\"mouseenter->stimeo--hover-card#open\n * mouseleave->stimeo--hover-card#close\n * focusin->stimeo--hover-card#open\n * focusout->stimeo--hover-card#close\">@jane</a>\n * <div id=\"hc\" data-stimeo--hover-card-target=\"card\"\n * data-action=\"mouseenter->stimeo--hover-card#open\n * mouseleave->stimeo--hover-card#close\n * focusin->stimeo--hover-card#open\n * focusout->stimeo--hover-card#close\" hidden>…</div>\n * </span>\n *\n * There is no dedicated APG pattern; this follows the **Disclosure** convention\n * (`aria-expanded`) for a hover/focus-opened, non-modal popover that *may* hold\n * interactive content (unlike a tooltip). The card is **not** a `role=\"dialog\"`:\n * it is supplementary, so its content must also be reachable from the trigger\n * itself. For a short text hint use `stimeo--tooltip`; for a click-opened\n * action panel use `stimeo--popover`.\n *\n * @remarks\n * Behavior only — placement is the consumer's CSS (static) or the opt-in\n * `stimeo-ui/positioning` module (dynamic); this controller never imports it.\n *\n * Behavior provided:\n * - Open on `mouseenter`/`focusin`, close on `mouseleave`/`focusout`, each gated by\n * `openDelay`/`closeDelay` to prevent accidental flicker.\n * - **Hoverable bridge**: binding open/close on the card cancels a pending close\n * when the pointer crosses into it. Matching focus actions on the card cancel\n * the trigger's pending close while focus is inside, then schedule close once\n * focus leaves the whole controller.\n * - **Dismissible**: while open, the card joins the shared `EscapeLayer`\n * stack, so `Escape` closes it regardless of where focus sits (card, trigger,\n * or elsewhere). The resolver ignores an Escape already consumed by an inner\n * handler and lets the most recently shown layer own the press, so one\n * keypress closes exactly one layer.\n * - Open/closed flips the trigger's `aria-expanded`, the card's `hidden`, and a\n * `data-state` (`open`/`closed`). Focus is never stolen on open.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the card, the usual convention for\n * anchored popups. Covers keyboard/programmatic scroll and scrollbar-drag, which the\n * pointer-leave close cannot. Off by default.\n * - Each move of the open state is reported: `stimeo--hover-card:open` and\n * `stimeo--hover-card:close` dispatch `{ reason: StateReason }`, after the\n * state attributes are written. The reason is taken from the event that\n * started the move and survives the delay, so a `mouseleave` that closes\n * `closeDelay` later still reports `\"pointer\"`. Both are informational, so\n * neither is cancelable. A call that leaves the state where it already was,\n * the normalization in {@link connect}, and {@link disconnect} are all silent.\n */\nexport class HoverCardController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"card\"];\n static override values = {\n openDelay: { type: Number, default: 300 },\n closeDelay: { type: Number, default: 200 },\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly cardTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasCardTarget: boolean;\n declare readonly openDelayValue: number;\n declare readonly closeDelayValue: number;\n declare readonly closeOnScrollValue: boolean;\n\n /** Pending open/close timers, with their IDs reset on every lifecycle boundary. */\n readonly #timers = new SafeTimeout();\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n #pendingOpen: number | null = null;\n #pendingClose: number | null = null;\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** Starts closed and discards any stale pending state from a prior connection. */\n override connect(): void {\n this.#cancelOpen();\n this.#cancelClose();\n this.#conceal(\"api\");\n this.#reporting = true;\n }\n\n /** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#reporting = false;\n this.#cancelOpen();\n this.#cancelClose();\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Opens the card, after `openDelay` ms (or immediately at 0). Cancels a pending close. */\n open(event?: Event): void {\n const reason = stateReasonFor(event);\n this.#cancelClose();\n if (this.#isOpen || this.#pendingOpen !== null) return;\n if (this.openDelayValue <= 0) {\n this.#reveal(reason);\n return;\n }\n this.#pendingOpen = this.#timers.set(() => {\n this.#pendingOpen = null;\n this.#reveal(reason);\n }, this.openDelayValue);\n }\n\n /**\n * Schedules the card to close after `closeDelay`. Cancels a pending open. The\n * delayed callback re-checks whether focus has landed inside the controller\n * (e.g. a link in the card) and, if so, aborts the close — covering keyboard\n * traversal that the pointer-only hoverable bridge cannot.\n */\n close(event?: Event): void {\n const reason = stateReasonFor(event);\n this.#cancelOpen();\n if (!this.#isOpen || this.#pendingClose !== null) return;\n this.#pendingClose = this.#timers.set(() => {\n this.#pendingClose = null;\n if (this.element.contains(document.activeElement)) return;\n this.#conceal(reason);\n }, this.closeDelayValue);\n }\n\n /**\n * Reveals the card, reflects state, reports a move, and joins the Escape stack.\n *\n * @stimeoRuntimeOnly `closeOnScroll` decides whether this reveal wires the scroll dismissal; what\n * is shown does not depend on it.\n */\n #reveal(reason: StateReason): void {\n if (!this.hasCardTarget) return;\n const was = this.#isOpen;\n this.cardTarget.hidden = false;\n this.cardTarget.setAttribute(\"data-state\", \"open\");\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n // No claims predicate: hover-revealed content is dismissible regardless of\n // where focus sits (WCAG 2.2 SC 1.4.13), so it always claims while open.\n this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss(\"escape\") });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss(\"scroll\"));\n }\n }\n\n /** Hides the card, reflects state, reports a move, and leaves the Escape stack. */\n #conceal(reason: StateReason): void {\n // Release the layer and observers first, unconditionally: if the card\n // target was removed from the DOM while open, an early return would leak\n // the stack entry and the scroll-dismiss listeners.\n const was = this.#isOpen;\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasCardTarget) {\n this.cardTarget.hidden = true;\n this.cardTarget.setAttribute(\"data-state\", \"closed\");\n }\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /** Cancels pending timers and conceals immediately (shared Escape path). */\n #dismiss(reason: StateReason): void {\n this.#cancelOpen();\n this.#cancelClose();\n this.#conceal(reason);\n }\n\n /** Cancels any pending open timer. */\n #cancelOpen(): void {\n if (this.#pendingOpen !== null) {\n this.#timers.clear(this.#pendingOpen);\n this.#pendingOpen = null;\n }\n }\n\n /** Cancels any pending close timer. */\n #cancelClose(): void {\n if (this.#pendingClose !== null) {\n this.#timers.clear(this.#pendingClose);\n this.#pendingClose = null;\n }\n }\n\n /** Whether the card is currently visible. */\n get #isOpen(): boolean {\n return this.hasCardTarget && !this.cardTarget.hidden;\n }\n}\n"]}
|
|
@@ -11,12 +11,18 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
11
11
|
* data-stimeo--idle-timeout-value="900000"
|
|
12
12
|
* data-stimeo--idle-prompt-before-value="60000"
|
|
13
13
|
* data-action="stimeo--idle:prompt->session#warn
|
|
14
|
-
* stimeo--idle:idle->session#logout"
|
|
14
|
+
* stimeo--idle:idle->session#logout">
|
|
15
|
+
* <p data-stimeo--idle-target="prompt" hidden>Your session expires in a minute.</p>
|
|
16
|
+
* <p data-stimeo--idle-target="idle" hidden>You have been signed out.</p>
|
|
17
|
+
* </body>
|
|
15
18
|
*
|
|
16
19
|
* Activity events (`events`, passive) are watched on `document` with capture so
|
|
17
20
|
* non-bubbling ones like `scroll` are seen anywhere; returning to a hidden tab
|
|
18
|
-
* (`visibilitychange` → visible) counts as activity too.
|
|
19
|
-
*
|
|
21
|
+
* (`visibilitychange` → visible) counts as activity too. The controller element carries
|
|
22
|
+
* `data-prompt` while the warning stands and `data-idle` once the timeout is reached,
|
|
23
|
+
* and the optional `prompt` / `idle` targets are the regions those phases reveal:
|
|
24
|
+
* declared where the page has something to show, shown while their phase holds and
|
|
25
|
+
* hidden otherwise, whatever visibility the markup was authored with.
|
|
20
26
|
*
|
|
21
27
|
* `prompt` dispatches `{ remaining }`; `idle` and `active` dispatch `{}`.
|
|
22
28
|
*
|
|
@@ -43,12 +49,19 @@ declare class IdleController extends Controller<HTMLElement> {
|
|
|
43
49
|
default: string;
|
|
44
50
|
};
|
|
45
51
|
};
|
|
52
|
+
static targets: string[];
|
|
46
53
|
static events: readonly ["prompt", "idle", "active"];
|
|
47
54
|
timeoutValue: number;
|
|
48
55
|
promptBeforeValue: number;
|
|
49
56
|
eventsValue: string;
|
|
57
|
+
readonly promptTargets: HTMLElement[];
|
|
58
|
+
readonly idleTargets: HTMLElement[];
|
|
50
59
|
connect(): void;
|
|
51
60
|
disconnect(): void;
|
|
61
|
+
/** Applies the current phase to a warning region inserted or replaced at runtime. */
|
|
62
|
+
promptTargetConnected(): void;
|
|
63
|
+
/** Applies the current phase to an idle region inserted or replaced at runtime. */
|
|
64
|
+
idleTargetConnected(): void;
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
export { IdleController };
|
|
@@ -55,6 +55,47 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
// src/utils/state_regions.ts
|
|
59
|
+
var StateRegions = class {
|
|
60
|
+
#whenTrue;
|
|
61
|
+
#whenFalse;
|
|
62
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.#whenTrue = options.whenTrue;
|
|
65
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
66
|
+
}
|
|
67
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
68
|
+
reflect(host, isTrue) {
|
|
69
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
70
|
+
if (!this.#whenFalse) {
|
|
71
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
75
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
76
|
+
for (const region of shown) this.#give(region);
|
|
77
|
+
for (const region of hiddenSide) this.#give(region);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
81
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
82
|
+
}
|
|
83
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
84
|
+
#write(region, hidden) {
|
|
85
|
+
if (hidden) this.#taken.add(region);
|
|
86
|
+
else this.#taken.delete(region);
|
|
87
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
88
|
+
}
|
|
89
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
90
|
+
#give(region) {
|
|
91
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
92
|
+
}
|
|
93
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
94
|
+
#inside(host, regions) {
|
|
95
|
+
return regions.filter((region) => host.contains(region));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
58
99
|
// src/utils/string_list.ts
|
|
59
100
|
function parseStringList(raw, fallback = []) {
|
|
60
101
|
const text = raw.trim();
|
|
@@ -87,7 +128,12 @@ var IdleController = class extends Controller {
|
|
|
87
128
|
// runs, so one malformed attribute would stop the detector connecting.
|
|
88
129
|
events: { type: String, default: "" }
|
|
89
130
|
};
|
|
131
|
+
static targets = ["prompt", "idle"];
|
|
90
132
|
static events = ["prompt", "idle", "active"];
|
|
133
|
+
/** The regions of the warning window, revealed alongside `data-prompt`. */
|
|
134
|
+
#promptRegions = new StateRegions({ whenTrue: () => this.promptTargets });
|
|
135
|
+
/** The regions of the elapsed timeout, revealed alongside `data-idle`. */
|
|
136
|
+
#idleRegions = new StateRegions({ whenTrue: () => this.idleTargets });
|
|
91
137
|
#timeouts = new SafeTimeout();
|
|
92
138
|
#idle = false;
|
|
93
139
|
#prompted = false;
|
|
@@ -104,7 +150,7 @@ var IdleController = class extends Controller {
|
|
|
104
150
|
if (this.#idle || this.#prompted) {
|
|
105
151
|
this.#idle = false;
|
|
106
152
|
this.#prompted = false;
|
|
107
|
-
this
|
|
153
|
+
this.#reflect();
|
|
108
154
|
this.dispatch("active", { detail: {} });
|
|
109
155
|
this.#arm();
|
|
110
156
|
}
|
|
@@ -115,7 +161,7 @@ var IdleController = class extends Controller {
|
|
|
115
161
|
connect() {
|
|
116
162
|
this.#idle = false;
|
|
117
163
|
this.#prompted = false;
|
|
118
|
-
this
|
|
164
|
+
this.#reflect();
|
|
119
165
|
this.#boundEvents = parseStringList(this.eventsValue, DEFAULT_ACTIVITY_EVENTS);
|
|
120
166
|
for (const type of this.#boundEvents) {
|
|
121
167
|
document.addEventListener(type, this.#onActivity, { passive: true, capture: true });
|
|
@@ -131,7 +177,11 @@ var IdleController = class extends Controller {
|
|
|
131
177
|
document.removeEventListener("visibilitychange", this.#onVisibility);
|
|
132
178
|
this.#timeouts.clearAll();
|
|
133
179
|
}
|
|
134
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Schedules the prompt and idle checks from the current activity baseline.
|
|
182
|
+
*
|
|
183
|
+
* @stimeoRuntimeOnly `timeout` and `promptBefore` time the checks this call arms.
|
|
184
|
+
*/
|
|
135
185
|
#arm() {
|
|
136
186
|
this.#timeouts.clearAll();
|
|
137
187
|
this.#lastActivity = Date.now();
|
|
@@ -145,6 +195,9 @@ var IdleController = class extends Controller {
|
|
|
145
195
|
* Idle-timer callback: go idle only if there has genuinely been no activity for
|
|
146
196
|
* `timeout`; otherwise reschedule for the remaining time. This lets activity events
|
|
147
197
|
* stay O(1) (a timestamp write) while the deadline still tracks the last activity.
|
|
198
|
+
*
|
|
199
|
+
* @stimeoRuntimeOnly `timeout` sets the deadline this one check compares against; the phase it
|
|
200
|
+
* shows follows the elapsed time.
|
|
148
201
|
*/
|
|
149
202
|
#checkIdle() {
|
|
150
203
|
const remaining = this.timeoutValue - (Date.now() - this.#lastActivity);
|
|
@@ -154,19 +207,51 @@ var IdleController = class extends Controller {
|
|
|
154
207
|
}
|
|
155
208
|
this.#idle = true;
|
|
156
209
|
this.#prompted = false;
|
|
157
|
-
this
|
|
210
|
+
this.#reflect();
|
|
158
211
|
this.dispatch("idle", { detail: {} });
|
|
159
212
|
}
|
|
160
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Prompt-timer callback: warn at `promptBefore` before the idle deadline. A window
|
|
215
|
+
* narrowed while the cycle runs can push the warning onto the deadline or past it,
|
|
216
|
+
* and the phase it belongs to is the one before idle, so a lapsed cycle keeps the
|
|
217
|
+
* phase it reached. The next window opens with the cycle that activity arms.
|
|
218
|
+
*
|
|
219
|
+
* @stimeoRuntimeOnly `timeout` and `promptBefore` set the deadlines this one check compares
|
|
220
|
+
* against; the phase it shows follows the elapsed time.
|
|
221
|
+
*/
|
|
161
222
|
#checkPrompt() {
|
|
223
|
+
if (this.#idle) return;
|
|
162
224
|
const remaining = this.timeoutValue - this.promptBeforeValue - (Date.now() - this.#lastActivity);
|
|
163
225
|
if (remaining > 0) {
|
|
164
226
|
this.#timeouts.set(() => this.#checkPrompt(), remaining);
|
|
165
227
|
return;
|
|
166
228
|
}
|
|
167
229
|
this.#prompted = true;
|
|
230
|
+
this.#reflect();
|
|
168
231
|
this.dispatch("prompt", { detail: { remaining: this.promptBeforeValue } });
|
|
169
232
|
}
|
|
233
|
+
/** Applies the current phase to a warning region inserted or replaced at runtime. */
|
|
234
|
+
promptTargetConnected() {
|
|
235
|
+
this.#promptRegions.reflect(this.element, this.#prompted);
|
|
236
|
+
}
|
|
237
|
+
/** Applies the current phase to an idle region inserted or replaced at runtime. */
|
|
238
|
+
idleTargetConnected() {
|
|
239
|
+
this.#idleRegions.reflect(this.element, this.#idle);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Writes the phase to the element and to the declared regions. Both hooks and both
|
|
243
|
+
* regions are a pure function of the two flags, so every place that moves a flag ends
|
|
244
|
+
* here and the markup can never disagree with the phase the detector is in.
|
|
245
|
+
*/
|
|
246
|
+
#reflect() {
|
|
247
|
+
const { element } = this;
|
|
248
|
+
if (this.#prompted) element.setAttribute("data-prompt", "true");
|
|
249
|
+
else element.removeAttribute("data-prompt");
|
|
250
|
+
if (this.#idle) element.setAttribute("data-idle", "true");
|
|
251
|
+
else element.removeAttribute("data-idle");
|
|
252
|
+
this.#promptRegions.reflect(element, this.#prompted);
|
|
253
|
+
this.#idleRegions.reflect(element, this.#idle);
|
|
254
|
+
}
|
|
170
255
|
};
|
|
171
256
|
|
|
172
257
|
export { IdleController };
|