stimeo-ui 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/controllers/accordion_controller.d.ts +3 -2
  3. package/dist/controllers/accordion_controller.js.map +1 -1
  4. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  5. package/dist/controllers/alert_dialog_controller.js +318 -0
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  7. package/dist/controllers/carousel_controller.d.ts +100 -0
  8. package/dist/controllers/carousel_controller.js +272 -0
  9. package/dist/controllers/carousel_controller.js.map +1 -0
  10. package/dist/controllers/clipboard_controller.d.ts +72 -0
  11. package/dist/controllers/clipboard_controller.js +144 -0
  12. package/dist/controllers/clipboard_controller.js.map +1 -0
  13. package/dist/controllers/collapsible_controller.d.ts +70 -0
  14. package/dist/controllers/collapsible_controller.js +327 -0
  15. package/dist/controllers/collapsible_controller.js.map +1 -0
  16. package/dist/controllers/color_picker_controller.d.ts +76 -0
  17. package/dist/controllers/color_picker_controller.js +213 -0
  18. package/dist/controllers/color_picker_controller.js.map +1 -0
  19. package/dist/controllers/count_up_controller.js +8 -1
  20. package/dist/controllers/count_up_controller.js.map +1 -1
  21. package/dist/controllers/currency_input_controller.d.ts +72 -0
  22. package/dist/controllers/currency_input_controller.js +147 -0
  23. package/dist/controllers/currency_input_controller.js.map +1 -0
  24. package/dist/controllers/data_grid_controller.d.ts +71 -0
  25. package/dist/controllers/data_grid_controller.js +168 -0
  26. package/dist/controllers/data_grid_controller.js.map +1 -0
  27. package/dist/controllers/date_range_picker_controller.d.ts +87 -0
  28. package/dist/controllers/date_range_picker_controller.js +417 -0
  29. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  30. package/dist/controllers/dismissible_controller.d.ts +57 -0
  31. package/dist/controllers/dismissible_controller.js +117 -0
  32. package/dist/controllers/dismissible_controller.js.map +1 -0
  33. package/dist/controllers/drawer_controller.d.ts +96 -0
  34. package/dist/controllers/drawer_controller.js +630 -0
  35. package/dist/controllers/drawer_controller.js.map +1 -0
  36. package/dist/controllers/editable_controller.d.ts +73 -0
  37. package/dist/controllers/editable_controller.js +168 -0
  38. package/dist/controllers/editable_controller.js.map +1 -0
  39. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  40. package/dist/controllers/file_dropzone_controller.js +165 -0
  41. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  42. package/dist/controllers/filter_controller.d.ts +60 -0
  43. package/dist/controllers/filter_controller.js +86 -0
  44. package/dist/controllers/filter_controller.js.map +1 -0
  45. package/dist/controllers/flash_controller.js +36 -5
  46. package/dist/controllers/flash_controller.js.map +1 -1
  47. package/dist/controllers/highlight_controller.js +6 -4
  48. package/dist/controllers/highlight_controller.js.map +1 -1
  49. package/dist/controllers/intersection_controller.js +41 -18
  50. package/dist/controllers/intersection_controller.js.map +1 -1
  51. package/dist/controllers/lazy_frame_controller.js +33 -11
  52. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  53. package/dist/controllers/masonry_controller.d.ts +54 -0
  54. package/dist/controllers/masonry_controller.js +142 -0
  55. package/dist/controllers/masonry_controller.js.map +1 -0
  56. package/dist/controllers/menubar_controller.d.ts +62 -0
  57. package/dist/controllers/menubar_controller.js +433 -0
  58. package/dist/controllers/menubar_controller.js.map +1 -0
  59. package/dist/controllers/multi_select_controller.d.ts +115 -0
  60. package/dist/controllers/multi_select_controller.js +472 -0
  61. package/dist/controllers/multi_select_controller.js.map +1 -0
  62. package/dist/controllers/navigation_menu_controller.d.ts +99 -0
  63. package/dist/controllers/navigation_menu_controller.js +384 -0
  64. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  65. package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
  66. package/dist/controllers/overflow_indicator_controller.js +178 -27
  67. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  68. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  69. package/dist/controllers/password_reveal_controller.js +117 -0
  70. package/dist/controllers/password_reveal_controller.js.map +1 -0
  71. package/dist/controllers/range_slider_controller.d.ts +87 -0
  72. package/dist/controllers/range_slider_controller.js +166 -0
  73. package/dist/controllers/range_slider_controller.js.map +1 -0
  74. package/dist/controllers/read_more_controller.d.ts +52 -0
  75. package/dist/controllers/read_more_controller.js +194 -0
  76. package/dist/controllers/read_more_controller.js.map +1 -0
  77. package/dist/controllers/scroll_area_controller.js +15 -2
  78. package/dist/controllers/scroll_area_controller.js.map +1 -1
  79. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  80. package/dist/controllers/scroll_restore_controller.js +93 -0
  81. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  82. package/dist/controllers/scroll_visibility_controller.js +8 -4
  83. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  84. package/dist/controllers/scrollspy_controller.js +33 -11
  85. package/dist/controllers/scrollspy_controller.js.map +1 -1
  86. package/dist/controllers/separator_controller.d.ts +54 -0
  87. package/dist/controllers/separator_controller.js +87 -0
  88. package/dist/controllers/separator_controller.js.map +1 -0
  89. package/dist/controllers/sidebar_controller.d.ts +107 -0
  90. package/dist/controllers/sidebar_controller.js +761 -0
  91. package/dist/controllers/sidebar_controller.js.map +1 -0
  92. package/dist/controllers/stepper_controller.d.ts +4 -0
  93. package/dist/controllers/stepper_controller.js +28 -12
  94. package/dist/controllers/stepper_controller.js.map +1 -1
  95. package/dist/controllers/stick_to_bottom_controller.js +8 -4
  96. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  97. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  98. package/dist/controllers/sticky_observer_controller.js +88 -20
  99. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  100. package/dist/controllers/tabs_controller.d.ts +3 -2
  101. package/dist/controllers/tabs_controller.js.map +1 -1
  102. package/dist/controllers/tags_input_controller.d.ts +91 -0
  103. package/dist/controllers/tags_input_controller.js +275 -0
  104. package/dist/controllers/tags_input_controller.js.map +1 -0
  105. package/dist/controllers/theme_controller.js +20 -10
  106. package/dist/controllers/theme_controller.js.map +1 -1
  107. package/dist/controllers/time_picker_controller.d.ts +67 -0
  108. package/dist/controllers/time_picker_controller.js +212 -0
  109. package/dist/controllers/time_picker_controller.js.map +1 -0
  110. package/dist/controllers/toast_controller.d.ts +1 -9
  111. package/dist/controllers/toast_controller.js +36 -9
  112. package/dist/controllers/toast_controller.js.map +1 -1
  113. package/dist/controllers/transition_controller.d.ts +14 -8
  114. package/dist/controllers/transition_controller.js +153 -38
  115. package/dist/controllers/transition_controller.js.map +1 -1
  116. package/dist/controllers/tree_view_controller.d.ts +54 -0
  117. package/dist/controllers/tree_view_controller.js +275 -0
  118. package/dist/controllers/tree_view_controller.js.map +1 -0
  119. package/dist/index.d.ts +27 -1818
  120. package/dist/index.js +811 -295
  121. package/dist/index.js.map +1 -1
  122. package/dist/inspector/examples.json +2 -2
  123. package/dist/inspector/manifest.json +15 -3
  124. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/controllers/navigation_menu_controller.ts"],"names":[],"mappings":"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","file":"navigation_menu_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-first half of the\n * shared layered-Escape contract. A keydown that is part of an IME composition\n * (`event.isComposing`) cancels the composition, never a layer, and is ignored\n * here for every layer 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 * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible **navigation menu** behavior (disclosure navigation).\n *\n * Markup contract (identifier: `stimeo--navigation-menu`):\n * <nav data-controller=\"stimeo--navigation-menu\" aria-label=\"Main\">\n * <ul>\n * <li>\n * <button data-stimeo--navigation-menu-target=\"trigger\"\n * aria-expanded=\"false\" aria-controls=\"nav-products\"\n * data-action=\"click->stimeo--navigation-menu#toggle\n * keydown->stimeo--navigation-menu#onTriggerKeydown\">\n * Products\n * </button>\n * <div id=\"nav-products\" data-stimeo--navigation-menu-target=\"panel\" hidden>\n * <a href=\"/a\">Product A</a><a href=\"/b\">Product B</a>\n * </div>\n * </li>\n * </ul>\n * </nav>\n *\n * Implements the WAI-ARIA APG **Disclosure** navigation pattern: each top-level\n * button toggles its sub-panel (`aria-expanded` + `hidden` synced), only one panel\n * is open at a time, and the panel content is a plain set of links (not a\n * `role=\"menu\"`). Focus is **not** trapped — `Tab` moves through the links\n * naturally. `Escape` closes the open panel and returns focus to its trigger.\n * While a panel is open the nav is a layer on the shared {@link EscapeLayer}\n * stack; it claims a press only while focus is inside the nav or fell to the\n * body (a click on non-focusable panel content), so one keypress closes exactly\n * one layer (the shared layered-Escape contract). An outside click, or focus\n * leaving the nav for a known external destination, closes it. `ArrowLeft`/\n * `ArrowRight` move focus between triggers without rewriting `tabindex` (they\n * keep their natural Tab order). Hover open/close is opt-in via `openOnHover`.\n *\n * By default the hover region is each trigger and its panel. An optional\n * `hoverArea` target widens it: mark a wrapper that contains a trigger (e.g. the\n * `<li>` holding a top-level *link*, its disclosure button, and its panel — the\n * APG \"Disclosure Navigation with Top-Level Links\" arrangement) and hovering\n * anywhere over that wrapper opens the contained trigger's panel. A trigger or\n * panel inside a `hoverArea` defers to the wrapper (its own edges stop scheduling\n * open/close), so pointer movement within the area never flickers the panel.\n * Keep a trigger's panel inside its `hoverArea`: a panel left outside simply\n * falls back to the default two-region behavior — moving straight across a\n * shared edge never schedules a close (the leave handler checks\n * `relatedTarget`), and `hoverDelay` bridges the pointer's travel across an\n * actual gap. Targets added or removed while connected (e.g. a Turbo Stream\n * append) re-wire the hover listeners via Stimulus target callbacks.\n * `hoverArea` has no effect unless `openOnHover` is enabled.\n *\n * @remarks\n * Behavior only. Panel layout, mega-menu styling, and animation are the\n * consumer's CSS. Static placement is CSS; viewport-edge collision avoidance is\n * delegated to the opt-in `stimeo-ui/positioning` module (never imported here, so\n * the core stays zero-dependency). For an app command menu with arrow roving and\n * `role=\"menu\"`, use `stimeo--menubar` instead.\n */\nexport class NavigationMenuController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\", \"hoverArea\"];\n static override values = {\n openOnHover: { type: Boolean, default: false },\n hoverDelay: { type: Number, default: 150 },\n };\n static actions = [\"onTriggerKeydown\", \"toggle\"] as const;\n\n declare readonly triggerTargets: HTMLElement[];\n declare readonly panelTargets: HTMLElement[];\n declare readonly hoverAreaTargets: HTMLElement[];\n\n declare openOnHoverValue: boolean;\n declare hoverDelayValue: number;\n\n /** Open/close delay timers for hover mode; cleared together on disconnect. */\n readonly #hoverTimers = new SafeTimeout();\n\n /**\n * Elements currently carrying hover listeners. Removal always mirrors this\n * set (not a recomputed target snapshot), so add/remove stays symmetric even\n * when targets churn between connect and disconnect.\n */\n readonly #hoverWired = new Set<HTMLElement>();\n\n /** Escape-stack membership while any panel is open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Establishes the closed baseline and the dismissal listeners. */\n override connect(): void {\n this.#closeAll();\n document.addEventListener(\"click\", this.#onOutsideClick);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n if (this.openOnHoverValue) this.#addHoverListeners();\n }\n\n /** Removes every listener, pending hover timer, and stack membership registered while connected. */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#removeHoverListeners();\n this.#hoverTimers.clearAll();\n }\n\n /** Re-wires hover listeners when a target is added after connect (Turbo Streams etc.). */\n triggerTargetConnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** Re-wires hover listeners when a target is removed while connected. */\n triggerTargetDisconnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** See {@link NavigationMenuController.triggerTargetConnected}. */\n panelTargetConnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** See {@link NavigationMenuController.triggerTargetDisconnected}. */\n panelTargetDisconnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** See {@link NavigationMenuController.triggerTargetConnected}. */\n hoverAreaTargetConnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** See {@link NavigationMenuController.triggerTargetDisconnected}. */\n hoverAreaTargetDisconnected(): void {\n this.#rewireHoverListeners();\n }\n\n /** Toggles a trigger's panel (single-open). Bound via `data-action` (click). */\n toggle(event: Event): void {\n const trigger = event.currentTarget as HTMLElement;\n if (this.#isExpanded(trigger)) {\n this.#closePanel(trigger);\n } else {\n this.#openPanel(trigger);\n }\n }\n\n /** `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order). */\n onTriggerKeydown(event: KeyboardEvent): void {\n const triggers = this.triggerTargets;\n const index = triggers.indexOf(event.currentTarget as HTMLElement);\n if (index === -1) return;\n const length = triggers.length;\n\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n triggers[(index + 1) % length]?.focus();\n } else if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n triggers[(index - 1 + length) % length]?.focus();\n }\n }\n\n /** Opens `trigger`'s panel, closing any other open panel first. */\n #openPanel(trigger: HTMLElement): void {\n this.#closeAll();\n const panel = this.#panelFor(trigger);\n if (!panel) return;\n panel.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n this.#syncEscapeLayer();\n }\n\n /** Closes `trigger`'s panel and reflects the collapsed state. */\n #closePanel(trigger: HTMLElement): void {\n const panel = this.#panelFor(trigger);\n if (panel) panel.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n this.#syncEscapeLayer();\n }\n\n /** Closes every open panel. */\n #closeAll(): void {\n for (const trigger of this.triggerTargets) this.#closePanel(trigger);\n }\n\n /**\n * Aligns Escape-stack membership with the open state: joins (or re-asserts to\n * the top) while a panel is open, leaves once none is. Re-asserting when the\n * user switches panels is deliberate — the nav is again the newest layer.\n */\n #syncEscapeLayer(): void {\n if (this.#isAnyOpen) {\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n } else {\n this.#escapeLayer.deactivate();\n }\n }\n\n /** Closes any open panel and returns focus to its trigger (Escape path). */\n #closeAndRestore(): void {\n const open = this.#openTrigger;\n if (!open) return;\n this.#closePanel(open);\n open.focus();\n }\n\n /** Closes panels when a click lands outside the nav element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (this.#isAnyOpen && !this.element.contains(event.target as Node)) this.#closeAll();\n };\n\n /**\n * Closes (without restoring focus) when focus leaves the nav for a known\n * external destination. A null/non-Node destination is indeterminate:\n * browsers use it for clicks on non-focusable content and for window\n * deactivation, so those never close the nav here (matching the popover\n * convention) — the outside-click handler decides pointer dismissal, and the\n * Escape stack's body-focus claim keeps keyboard dismissal working.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n const next = event.relatedTarget;\n if (!(next instanceof Node) || this.element.contains(next)) return;\n this.#closeAll();\n };\n\n /** Opens a trigger's panel after the hover delay (hover mode). */\n readonly #onPointerEnter = (event: Event): void => {\n const trigger = this.#triggerForHover(event.currentTarget as HTMLElement);\n if (!trigger) return;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#openPanel(trigger), this.hoverDelayValue);\n };\n\n /**\n * Closes the open panel after the hover delay (hover mode) — unless the\n * pointer moved directly into another part of the hover region (an adjacent\n * trigger, panel, or hoverArea): crossing a shared edge must not schedule a\n * spurious close, e.g. a hoverArea whose panel sits outside it as a sibling.\n */\n readonly #onPointerLeave = (event: Event): void => {\n const next = event instanceof MouseEvent ? event.relatedTarget : null;\n if (next instanceof Node && this.#hoverElements.some((el) => el.contains(next))) return;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#closeAll(), this.hoverDelayValue);\n };\n\n /** Wires hover open/close on each hover element (opt-in), tracking what was wired. */\n #addHoverListeners(): void {\n for (const element of this.#hoverElements) {\n element.addEventListener(\"mouseenter\", this.#onPointerEnter);\n element.addEventListener(\"mouseleave\", this.#onPointerLeave);\n this.#hoverWired.add(element);\n }\n }\n\n /** Removes the hover listeners from exactly the elements that were wired. */\n #removeHoverListeners(): void {\n for (const element of this.#hoverWired) {\n element.removeEventListener(\"mouseenter\", this.#onPointerEnter);\n element.removeEventListener(\"mouseleave\", this.#onPointerLeave);\n }\n this.#hoverWired.clear();\n }\n\n /**\n * Rebuilds the hover wiring from the current targets. Target callbacks call\n * this so items added or removed after connect (e.g. a Turbo Stream append)\n * participate in hover; the wired-set removal keeps the rebuild symmetric.\n */\n #rewireHoverListeners(): void {\n if (!this.openOnHoverValue) return;\n this.#removeHoverListeners();\n this.#addHoverListeners();\n }\n\n /**\n * Elements that participate in hover: each `hoverArea`, plus every trigger and\n * panel **not** wrapped by one. A wrapped trigger/panel must defer to its\n * wrapper — its own mouseleave would otherwise schedule a close while the\n * pointer is still inside the area (mouseenter does not re-fire on the wrapper\n * when moving among its descendants), flickering the panel shut.\n */\n get #hoverElements(): HTMLElement[] {\n const areas = this.hoverAreaTargets;\n const covered = (element: HTMLElement): boolean => areas.some((area) => area.contains(element));\n return [\n ...areas,\n ...this.triggerTargets.filter((trigger) => !covered(trigger)),\n ...this.panelTargets.filter((panel) => !covered(panel)),\n ];\n }\n\n /**\n * Resolves the trigger for a hovered element: the trigger itself, the trigger\n * controlling a hovered panel, or the first trigger contained in a hovered\n * `hoverArea` wrapper.\n */\n #triggerForHover(element: HTMLElement): HTMLElement | null {\n if (this.triggerTargets.includes(element)) return element;\n if (this.panelTargets.includes(element)) {\n return this.triggerTargets.find((trigger) => this.#panelFor(trigger) === element) ?? null;\n }\n if (this.hoverAreaTargets.includes(element)) {\n return this.triggerTargets.find((trigger) => element.contains(trigger)) ?? null;\n }\n return null;\n }\n\n /** The panel controlled by `trigger` (matched by `aria-controls`/`id`). */\n #panelFor(trigger: HTMLElement): HTMLElement | null {\n const id = trigger.getAttribute(\"aria-controls\");\n // Resolve against this controller's own panel targets (not a global id\n // lookup) so it stays scoped to this nav instance.\n return id ? (this.panelTargets.find((panel) => panel.id === id) ?? null) : null;\n }\n\n /** Whether `trigger`'s panel is currently expanded. */\n #isExpanded(trigger: HTMLElement): boolean {\n return trigger.getAttribute(\"aria-expanded\") === \"true\";\n }\n\n /** The trigger whose panel is currently open, if any. */\n get #openTrigger(): HTMLElement | null {\n return this.triggerTargets.find((trigger) => this.#isExpanded(trigger)) ?? null;\n }\n\n /** Whether any panel is currently open. */\n get #isAnyOpen(): boolean {\n return this.#openTrigger !== null;\n }\n}\n"]}
@@ -22,9 +22,10 @@ import { Controller } from '@hotwired/stimulus';
22
22
  * </div>
23
23
  *
24
24
  * The viewport's scroll position and size are watched (via the wired `scroll`
25
- * action, plus {@link LayoutObserver} for resize and a {@link MutationObserver}
26
- * for content changes). Optional page buttons scroll one viewport at a time and
27
- * have their `disabled` synced to the matching direction's remaining room.
25
+ * action, plus {@link LayoutObserver} for viewport/content resize, a
26
+ * {@link MutationObserver}, and descendant load events). Optional page buttons
27
+ * scroll one logical viewport page at a time (including RTL) and have their
28
+ * disabled state synced to the matching direction's remaining room.
28
29
  *
29
30
  * @remarks
30
31
  * Behavior only — shadows, arrows, and gradients are the consumer's CSS;
@@ -53,7 +54,14 @@ declare class OverflowIndicatorController extends Controller<HTMLElement> {
53
54
  thresholdValue: number;
54
55
  connect(): void;
55
56
  disconnect(): void;
56
- /** Re-measures remaining scroll room and reflects the state hooks. Public so it can be wired to the viewport's `scroll`. */
57
+ viewportTargetConnected(): void;
58
+ viewportTargetDisconnected(viewport: HTMLElement): void;
59
+ orientationValueChanged(): void;
60
+ thresholdValueChanged(): void;
61
+ /**
62
+ * Re-measures remaining scroll room and reflects the state hooks.
63
+ * Public so it can be wired to the viewport's `scroll`.
64
+ */
57
65
  update(): void;
58
66
  /** Scrolls one viewport page toward the `direction` param (`start`/`end`). */
59
67
  scrollByPage(event: Event): void;
@@ -58,7 +58,30 @@ var LayoutObserver = class {
58
58
  }
59
59
  };
60
60
 
61
+ // src/utils/logical_scroll.ts
62
+ function isRtl(element) {
63
+ return window.getComputedStyle(element).direction === "rtl";
64
+ }
65
+ function logicalScrollMetrics(element, horizontal) {
66
+ const max = Math.max(
67
+ 0,
68
+ horizontal ? element.scrollWidth - element.clientWidth : element.scrollHeight - element.clientHeight
69
+ );
70
+ const raw = horizontal ? element.scrollLeft : element.scrollTop;
71
+ const position = horizontal && isRtl(element) ? -raw : raw;
72
+ return { position: Math.min(max, Math.max(0, position)), max };
73
+ }
74
+ function physicalScrollDelta(element, horizontal, logicalDelta) {
75
+ return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;
76
+ }
77
+
78
+ // src/utils/reduced_motion.ts
79
+ function prefersReducedMotion() {
80
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
81
+ }
82
+
61
83
  // src/controllers/overflow_indicator_controller.ts
84
+ var DIRECTION_BUTTON_SELECTOR = "[data-stimeo--overflow-indicator-direction-param]";
62
85
  var OverflowIndicatorController = class extends Controller {
63
86
  static targets = ["viewport"];
64
87
  static values = {
@@ -67,38 +90,50 @@ var OverflowIndicatorController = class extends Controller {
67
90
  };
68
91
  static actions = ["scrollByPage", "update"];
69
92
  static events = ["change"];
70
- #layout = new LayoutObserver(() => this.update());
93
+ #layout = new LayoutObserver(() => {
94
+ if (this.#connected) this.update();
95
+ });
96
+ #connected = false;
97
+ #observedViewport = null;
98
+ #observedContent = /* @__PURE__ */ new Set();
71
99
  #mutationObserver = null;
100
+ #pendingButtonDisables = /* @__PURE__ */ new Map();
72
101
  /** Last reported room, so `change` fires only on transitions. */
73
102
  #state = null;
74
103
  connect() {
75
- if (!this.hasViewportTarget) return;
76
- this.#layout.observe(this.viewportTarget);
77
- this.#layout.observeViewport();
78
- if (typeof MutationObserver !== "undefined") {
79
- this.#mutationObserver = new MutationObserver(() => this.update());
80
- this.#mutationObserver.observe(this.viewportTarget, {
81
- childList: true,
82
- subtree: true,
83
- characterData: true
84
- });
85
- }
86
- this.update();
104
+ this.#connected = true;
105
+ this.#syncViewport();
87
106
  }
88
107
  disconnect() {
108
+ this.#connected = false;
109
+ this.#stopObservingViewport();
89
110
  this.#layout.disconnect();
90
- this.#mutationObserver?.disconnect();
91
- this.#mutationObserver = null;
111
+ this.#clearPendingButtonDisables();
92
112
  this.#state = null;
93
113
  }
94
- /** Re-measures remaining scroll room and reflects the state hooks. Public so it can be wired to the viewport's `scroll`. */
114
+ viewportTargetConnected() {
115
+ this.#syncViewport();
116
+ }
117
+ viewportTargetDisconnected(viewport) {
118
+ if (this.#observedViewport === viewport) this.#stopObservingViewport();
119
+ this.#syncViewport();
120
+ }
121
+ orientationValueChanged() {
122
+ if (this.#connected) this.update();
123
+ }
124
+ thresholdValueChanged() {
125
+ if (this.#connected) this.update();
126
+ }
127
+ /**
128
+ * Re-measures remaining scroll room and reflects the state hooks.
129
+ * Public so it can be wired to the viewport's `scroll`.
130
+ */
95
131
  update() {
96
132
  if (!this.hasViewportTarget) return;
97
133
  const vp = this.viewportTarget;
98
134
  const horizontal = this.orientationValue !== "vertical";
99
- const t = this.thresholdValue;
100
- const scrollPos = horizontal ? vp.scrollLeft : vp.scrollTop;
101
- const maxScroll = horizontal ? vp.scrollWidth - vp.clientWidth : vp.scrollHeight - vp.clientHeight;
135
+ const t = this.#threshold;
136
+ const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontal);
102
137
  const start = scrollPos > t;
103
138
  const end = scrollPos < maxScroll - t;
104
139
  vp.setAttribute("data-overflow-start", start ? "true" : "false");
@@ -114,11 +149,13 @@ var OverflowIndicatorController = class extends Controller {
114
149
  if (!this.hasViewportTarget) return;
115
150
  const direction = this.#directionFromEvent(event);
116
151
  if (!direction) return;
152
+ if (this.#state && !this.#state[direction]) return;
117
153
  const vp = this.viewportTarget;
118
154
  const horizontal = this.orientationValue !== "vertical";
119
155
  const page = horizontal ? vp.clientWidth : vp.clientHeight;
120
- const delta = direction === "start" ? -page : page;
121
- const behavior = this.#prefersReducedMotion() ? "auto" : "smooth";
156
+ const logicalDelta = direction === "start" ? -page : page;
157
+ const delta = physicalScrollDelta(vp, horizontal, logicalDelta);
158
+ const behavior = prefersReducedMotion() ? "auto" : "smooth";
122
159
  if (horizontal) {
123
160
  vp.scrollBy({ left: delta, behavior });
124
161
  } else {
@@ -127,10 +164,16 @@ var OverflowIndicatorController = class extends Controller {
127
164
  }
128
165
  /** Mirrors remaining room onto any direction buttons by toggling `disabled`. */
129
166
  #syncButtons(start, end) {
130
- const buttons = this.element.querySelectorAll(
131
- "[data-stimeo--overflow-indicator-direction-param]"
132
- );
167
+ for (const button of [...this.#pendingButtonDisables.keys()]) {
168
+ if (!button.isConnected || button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
169
+ this.#cancelPendingButtonDisable(button);
170
+ }
171
+ }
172
+ const buttons = this.element.querySelectorAll(DIRECTION_BUTTON_SELECTOR);
133
173
  for (const button of buttons) {
174
+ if (button.closest("[data-controller~='stimeo--overflow-indicator']") !== this.element) {
175
+ continue;
176
+ }
134
177
  const direction = button.getAttribute("data-stimeo--overflow-indicator-direction-param");
135
178
  if (direction === "start") this.#toggleButton(button, start);
136
179
  else if (direction === "end") this.#toggleButton(button, end);
@@ -143,7 +186,11 @@ var OverflowIndicatorController = class extends Controller {
143
186
  * whole control disabled) is therefore never blindly re-enabled.
144
187
  */
145
188
  #toggleButton(button, hasRoom) {
189
+ if (!this.#pendingButtonDisables.has(button) && button.hasAttribute("data-overflow-indicator-pending-disabled")) {
190
+ this.#cancelPendingButtonDisable(button);
191
+ }
146
192
  if (hasRoom) {
193
+ this.#cancelPendingButtonDisable(button);
147
194
  if (button.hasAttribute("data-overflow-indicator-disabled")) {
148
195
  button.disabled = false;
149
196
  button.removeAttribute("data-overflow-indicator-disabled");
@@ -151,17 +198,121 @@ var OverflowIndicatorController = class extends Controller {
151
198
  return;
152
199
  }
153
200
  if (button.disabled) return;
201
+ if (document.activeElement === button) {
202
+ this.#deferButtonDisable(button);
203
+ } else {
204
+ this.#disableButton(button);
205
+ }
206
+ }
207
+ /**
208
+ * Keeps a boundary button focusable until native blur, exposing its temporary
209
+ * inoperability with `aria-disabled` and making its action a no-op meanwhile.
210
+ */
211
+ #deferButtonDisable(button) {
212
+ if (this.#pendingButtonDisables.has(button)) return;
213
+ button.setAttribute("data-overflow-indicator-pending-disabled", "");
214
+ button.setAttribute(
215
+ "data-overflow-indicator-aria-disabled",
216
+ button.getAttribute("aria-disabled") ?? ""
217
+ );
218
+ button.setAttribute("aria-disabled", "true");
219
+ const onBlur = () => {
220
+ this.#cancelPendingButtonDisable(button);
221
+ if (!button.disabled) this.#disableButton(button);
222
+ };
223
+ this.#pendingButtonDisables.set(button, { onBlur });
224
+ button.addEventListener("blur", onBlur);
225
+ }
226
+ #disableButton(button) {
154
227
  button.disabled = true;
155
228
  button.setAttribute("data-overflow-indicator-disabled", "");
156
229
  }
230
+ #cancelPendingButtonDisable(button) {
231
+ const pending = this.#pendingButtonDisables.get(button);
232
+ if (pending) button.removeEventListener("blur", pending.onBlur);
233
+ this.#pendingButtonDisables.delete(button);
234
+ button.removeAttribute("data-overflow-indicator-pending-disabled");
235
+ const displaced = button.getAttribute("data-overflow-indicator-aria-disabled");
236
+ if (displaced !== null) {
237
+ button.removeAttribute("data-overflow-indicator-aria-disabled");
238
+ if (button.getAttribute("aria-disabled") === "true") {
239
+ if (displaced === "") button.removeAttribute("aria-disabled");
240
+ else button.setAttribute("aria-disabled", displaced);
241
+ }
242
+ }
243
+ }
244
+ #clearPendingButtonDisables() {
245
+ for (const button of [...this.#pendingButtonDisables.keys()]) {
246
+ this.#cancelPendingButtonDisable(button);
247
+ }
248
+ }
249
+ /** Moves resize/mutation/load observation to the current viewport target. */
250
+ #syncViewport() {
251
+ if (!this.#connected) return;
252
+ const next = this.hasViewportTarget ? this.viewportTarget : null;
253
+ if (next === this.#observedViewport) return;
254
+ this.#stopObservingViewport();
255
+ if (!next) return;
256
+ this.#observedViewport = next;
257
+ this.#layout.observe(next);
258
+ this.#layout.observeViewport();
259
+ next.addEventListener("load", this.#onContentLoad, true);
260
+ this.#syncContentObservation();
261
+ if (typeof MutationObserver !== "undefined") {
262
+ this.#mutationObserver = new MutationObserver(() => {
263
+ if (!this.#connected || this.#observedViewport !== next) return;
264
+ this.#syncContentObservation();
265
+ this.update();
266
+ });
267
+ this.#mutationObserver.observe(this.element, {
268
+ childList: true,
269
+ subtree: true,
270
+ characterData: true,
271
+ attributes: true,
272
+ attributeFilter: ["class", "style", "hidden"]
273
+ });
274
+ }
275
+ this.#state = null;
276
+ this.update();
277
+ }
278
+ /** Observes direct content boxes whose resize can change the viewport's scroll extent. */
279
+ #syncContentObservation() {
280
+ const next = new Set(this.#observedViewport?.children ?? []);
281
+ for (const content of this.#observedContent) {
282
+ if (!next.has(content)) {
283
+ this.#layout.unobserve(content);
284
+ this.#observedContent.delete(content);
285
+ }
286
+ }
287
+ for (const content of next) {
288
+ if (!this.#observedContent.has(content)) {
289
+ this.#observedContent.add(content);
290
+ this.#layout.observe(content);
291
+ }
292
+ }
293
+ }
294
+ #stopObservingViewport() {
295
+ this.#mutationObserver?.disconnect();
296
+ this.#mutationObserver = null;
297
+ this.#observedViewport?.removeEventListener("load", this.#onContentLoad, true);
298
+ if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);
299
+ for (const content of this.#observedContent) this.#layout.unobserve(content);
300
+ this.#observedContent.clear();
301
+ this.#observedViewport = null;
302
+ this.#layout.unobserveViewport();
303
+ }
304
+ #onContentLoad = () => {
305
+ if (this.#connected) this.update();
306
+ };
307
+ get #threshold() {
308
+ const value = this.thresholdValue;
309
+ return Number.isFinite(value) ? Math.max(0, value) : 1;
310
+ }
157
311
  #directionFromEvent(event) {
158
312
  const params = event.params;
159
313
  const direction = params?.direction;
160
314
  return direction === "start" || direction === "end" ? direction : null;
161
315
  }
162
- #prefersReducedMotion() {
163
- return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
164
- }
165
316
  };
166
317
 
167
318
  export { OverflowIndicatorController };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/controllers/overflow_indicator_controller.ts"],"names":[],"mappings":"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","file":"overflow_indicator_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\n\n/**\n * Headless **Overflow Indicator**: detects whether a scroll container can still\n * scroll toward its start and/or end and publishes it as `data-overflow-start` /\n * `data-overflow-end`. No APG widget — a state-detection utility. Consumers draw\n * edge shadows or arrows in CSS to signal \"more content this way\".\n *\n * Markup contract (identifier: `stimeo--overflow-indicator`):\n * <div data-controller=\"stimeo--overflow-indicator\"\n * data-stimeo--overflow-indicator-orientation-value=\"horizontal\">\n * <button type=\"button\" aria-label=\"Prev\"\n * data-stimeo--overflow-indicator-direction-param=\"start\"\n * data-action=\"click->stimeo--overflow-indicator#scrollByPage\">‹</button>\n * <div data-stimeo--overflow-indicator-target=\"viewport\"\n * data-action=\"scroll->stimeo--overflow-indicator#update\"\n * tabindex=\"0\" role=\"region\" aria-label=\"Products\"\n * style=\"overflow-x: auto;\"><!-- items --></div>\n * <button type=\"button\" aria-label=\"Next\"\n * data-stimeo--overflow-indicator-direction-param=\"end\"\n * data-action=\"click->stimeo--overflow-indicator#scrollByPage\">›</button>\n * </div>\n *\n * The viewport's scroll position and size are watched (via the wired `scroll`\n * action, plus {@link LayoutObserver} for resize and a {@link MutationObserver}\n * for content changes). Optional page buttons scroll one viewport at a time and\n * have their `disabled` synced to the matching direction's remaining room.\n *\n * @remarks\n * Behavior only — shadows, arrows, and gradients are the consumer's CSS;\n * `data-overflow-*` carry no ARIA semantics. All observers/listeners are released\n * on `disconnect()` (Turbo navigation included). `scrollByPage` honors\n * `prefers-reduced-motion`.\n */\nexport class OverflowIndicatorController extends Controller<HTMLElement> {\n static override targets = [\"viewport\"];\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n threshold: { type: Number, default: 1 },\n };\n static actions = [\"scrollByPage\", \"update\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly viewportTarget: HTMLElement;\n declare readonly hasViewportTarget: boolean;\n\n declare orientationValue: string;\n declare thresholdValue: number;\n\n readonly #layout = new LayoutObserver(() => this.update());\n #mutationObserver: MutationObserver | null = null;\n /** Last reported room, so `change` fires only on transitions. */\n #state: { start: boolean; end: boolean } | null = null;\n\n override connect(): void {\n if (!this.hasViewportTarget) return;\n this.#layout.observe(this.viewportTarget);\n this.#layout.observeViewport();\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#mutationObserver = new MutationObserver(() => this.update());\n this.#mutationObserver.observe(this.viewportTarget, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n this.update();\n }\n\n override disconnect(): void {\n this.#layout.disconnect();\n this.#mutationObserver?.disconnect();\n this.#mutationObserver = null;\n this.#state = null;\n }\n\n /** Re-measures remaining scroll room and reflects the state hooks. Public so it can be wired to the viewport's `scroll`. */\n update(): void {\n if (!this.hasViewportTarget) return;\n const vp = this.viewportTarget;\n const horizontal = this.orientationValue !== \"vertical\";\n const t = this.thresholdValue;\n\n const scrollPos = horizontal ? vp.scrollLeft : vp.scrollTop;\n const maxScroll = horizontal\n ? vp.scrollWidth - vp.clientWidth\n : vp.scrollHeight - vp.clientHeight;\n\n const start = scrollPos > t;\n const end = scrollPos < maxScroll - t;\n\n vp.setAttribute(\"data-overflow-start\", start ? \"true\" : \"false\");\n vp.setAttribute(\"data-overflow-end\", end ? \"true\" : \"false\");\n this.#syncButtons(start, end);\n\n if (!this.#state || this.#state.start !== start || this.#state.end !== end) {\n this.#state = { start, end };\n this.dispatch(\"change\", { detail: { start, end } });\n }\n }\n\n /** Scrolls one viewport page toward the `direction` param (`start`/`end`). */\n scrollByPage(event: Event): void {\n if (!this.hasViewportTarget) return;\n const direction = this.#directionFromEvent(event);\n if (!direction) return;\n\n const vp = this.viewportTarget;\n const horizontal = this.orientationValue !== \"vertical\";\n const page = horizontal ? vp.clientWidth : vp.clientHeight;\n const delta = direction === \"start\" ? -page : page;\n const behavior: ScrollBehavior = this.#prefersReducedMotion() ? \"auto\" : \"smooth\";\n\n if (horizontal) {\n vp.scrollBy({ left: delta, behavior });\n } else {\n vp.scrollBy({ top: delta, behavior });\n }\n }\n\n /** Mirrors remaining room onto any direction buttons by toggling `disabled`. */\n #syncButtons(start: boolean, end: boolean): void {\n const buttons = this.element.querySelectorAll<HTMLButtonElement>(\n \"[data-stimeo--overflow-indicator-direction-param]\",\n );\n for (const button of buttons) {\n const direction = button.getAttribute(\"data-stimeo--overflow-indicator-direction-param\");\n if (direction === \"start\") this.#toggleButton(button, start);\n else if (direction === \"end\") this.#toggleButton(button, end);\n }\n }\n\n /**\n * Reflects the remaining room onto a page button's `disabled`, owning only the\n * `disabled` it sets itself via a marker (`data-overflow-indicator-disabled`,\n * like `number-input`/`conditional-fields`). An author-disabled button (e.g. the\n * whole control disabled) is therefore never blindly re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, hasRoom: boolean): void {\n if (hasRoom) {\n if (button.hasAttribute(\"data-overflow-indicator-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-overflow-indicator-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n button.disabled = true;\n button.setAttribute(\"data-overflow-indicator-disabled\", \"\");\n }\n\n #directionFromEvent(event: Event): \"start\" | \"end\" | null {\n const params = (event as Event & { params?: { direction?: unknown } }).params;\n const direction = params?.direction;\n return direction === \"start\" || direction === \"end\" ? direction : null;\n }\n\n #prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/layout_observer.ts","../../src/utils/logical_scroll.ts","../../src/utils/reduced_motion.ts","../../src/controllers/overflow_indicator_controller.ts"],"names":[],"mappings":"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","file":"overflow_indicator_controller.js","sourcesContent":["/**\n * Unified element-size and viewport observation for Stimeo controllers.\n *\n * Layout-sensitive widgets (sliders, resizable panes, scroll spies, popovers)\n * need to react both to their *own* box changing — via {@link ResizeObserver} —\n * and to the *viewport* changing — via the `window` `resize` event. Wiring those\n * two sources by hand in every controller risks leaked listeners on\n * `disconnect()`. {@link LayoutObserver} owns both behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and\n * removes the viewport listener. Safe to call multiple times. Call this from a\n * controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n }\n}\n","/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * Each of those controllers used to duplicate the same guarded `matchMedia`\n * read; this one-liner keeps the media query string and the environment guard\n * single-sourced. The preference is intentionally re-read on every call — the\n * controllers check it at each animation/scroll start (WCAG 2.2 **2.3.3**), so\n * flipping the OS setting takes effect immediately without any listener or\n * cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { logicalScrollMetrics, physicalScrollDelta } from \"../utils/logical_scroll\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\nconst DIRECTION_BUTTON_SELECTOR = \"[data-stimeo--overflow-indicator-direction-param]\";\n\n/**\n * Headless **Overflow Indicator**: detects whether a scroll container can still\n * scroll toward its start and/or end and publishes it as `data-overflow-start` /\n * `data-overflow-end`. No APG widget — a state-detection utility. Consumers draw\n * edge shadows or arrows in CSS to signal \"more content this way\".\n *\n * Markup contract (identifier: `stimeo--overflow-indicator`):\n * <div data-controller=\"stimeo--overflow-indicator\"\n * data-stimeo--overflow-indicator-orientation-value=\"horizontal\">\n * <button type=\"button\" aria-label=\"Prev\"\n * data-stimeo--overflow-indicator-direction-param=\"start\"\n * data-action=\"click->stimeo--overflow-indicator#scrollByPage\">‹</button>\n * <div data-stimeo--overflow-indicator-target=\"viewport\"\n * data-action=\"scroll->stimeo--overflow-indicator#update\"\n * tabindex=\"0\" role=\"region\" aria-label=\"Products\"\n * style=\"overflow-x: auto;\"><!-- items --></div>\n * <button type=\"button\" aria-label=\"Next\"\n * data-stimeo--overflow-indicator-direction-param=\"end\"\n * data-action=\"click->stimeo--overflow-indicator#scrollByPage\">›</button>\n * </div>\n *\n * The viewport's scroll position and size are watched (via the wired `scroll`\n * action, plus {@link LayoutObserver} for viewport/content resize, a\n * {@link MutationObserver}, and descendant load events). Optional page buttons\n * scroll one logical viewport page at a time (including RTL) and have their\n * disabled state synced to the matching direction's remaining room.\n *\n * @remarks\n * Behavior only — shadows, arrows, and gradients are the consumer's CSS;\n * `data-overflow-*` carry no ARIA semantics. All observers/listeners are released\n * on `disconnect()` (Turbo navigation included). `scrollByPage` honors\n * `prefers-reduced-motion`.\n */\nexport class OverflowIndicatorController extends Controller<HTMLElement> {\n static override targets = [\"viewport\"];\n static override values = {\n orientation: { type: String, default: \"horizontal\" },\n threshold: { type: Number, default: 1 },\n };\n static actions = [\"scrollByPage\", \"update\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly viewportTarget: HTMLElement;\n declare readonly hasViewportTarget: boolean;\n\n declare orientationValue: string;\n declare thresholdValue: number;\n\n readonly #layout = new LayoutObserver(() => {\n if (this.#connected) this.update();\n });\n #connected = false;\n #observedViewport: HTMLElement | null = null;\n readonly #observedContent = new Set<Element>();\n #mutationObserver: MutationObserver | null = null;\n readonly #pendingButtonDisables = new Map<HTMLButtonElement, { onBlur: () => void }>();\n /** Last reported room, so `change` fires only on transitions. */\n #state: { start: boolean; end: boolean } | null = null;\n\n override connect(): void {\n this.#connected = true;\n this.#syncViewport();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#stopObservingViewport();\n this.#layout.disconnect();\n this.#clearPendingButtonDisables();\n this.#state = null;\n }\n\n viewportTargetConnected(): void {\n this.#syncViewport();\n }\n\n viewportTargetDisconnected(viewport: HTMLElement): void {\n if (this.#observedViewport === viewport) this.#stopObservingViewport();\n this.#syncViewport();\n }\n\n orientationValueChanged(): void {\n if (this.#connected) this.update();\n }\n\n thresholdValueChanged(): void {\n if (this.#connected) this.update();\n }\n\n /**\n * Re-measures remaining scroll room and reflects the state hooks.\n * Public so it can be wired to the viewport's `scroll`.\n */\n update(): void {\n if (!this.hasViewportTarget) return;\n const vp = this.viewportTarget;\n const horizontal = this.orientationValue !== \"vertical\";\n const t = this.#threshold;\n const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontal);\n\n const start = scrollPos > t;\n const end = scrollPos < maxScroll - t;\n\n vp.setAttribute(\"data-overflow-start\", start ? \"true\" : \"false\");\n vp.setAttribute(\"data-overflow-end\", end ? \"true\" : \"false\");\n this.#syncButtons(start, end);\n\n if (!this.#state || this.#state.start !== start || this.#state.end !== end) {\n this.#state = { start, end };\n this.dispatch(\"change\", { detail: { start, end } });\n }\n }\n\n /** Scrolls one viewport page toward the `direction` param (`start`/`end`). */\n scrollByPage(event: Event): void {\n if (!this.hasViewportTarget) return;\n const direction = this.#directionFromEvent(event);\n if (!direction) return;\n if (this.#state && !this.#state[direction]) return;\n\n const vp = this.viewportTarget;\n const horizontal = this.orientationValue !== \"vertical\";\n const page = horizontal ? vp.clientWidth : vp.clientHeight;\n const logicalDelta = direction === \"start\" ? -page : page;\n const delta = physicalScrollDelta(vp, horizontal, logicalDelta);\n const behavior: ScrollBehavior = prefersReducedMotion() ? \"auto\" : \"smooth\";\n\n if (horizontal) {\n vp.scrollBy({ left: delta, behavior });\n } else {\n vp.scrollBy({ top: delta, behavior });\n }\n }\n\n /** Mirrors remaining room onto any direction buttons by toggling `disabled`. */\n #syncButtons(start: boolean, end: boolean): void {\n for (const button of [...this.#pendingButtonDisables.keys()]) {\n if (\n !button.isConnected ||\n button.closest(\"[data-controller~='stimeo--overflow-indicator']\") !== this.element\n ) {\n this.#cancelPendingButtonDisable(button);\n }\n }\n const buttons = this.element.querySelectorAll<HTMLButtonElement>(DIRECTION_BUTTON_SELECTOR);\n for (const button of buttons) {\n if (button.closest(\"[data-controller~='stimeo--overflow-indicator']\") !== this.element) {\n continue;\n }\n const direction = button.getAttribute(\"data-stimeo--overflow-indicator-direction-param\");\n if (direction === \"start\") this.#toggleButton(button, start);\n else if (direction === \"end\") this.#toggleButton(button, end);\n }\n }\n\n /**\n * Reflects the remaining room onto a page button's `disabled`, owning only the\n * `disabled` it sets itself via a marker (`data-overflow-indicator-disabled`,\n * like `number-input`/`conditional-fields`). An author-disabled button (e.g. the\n * whole control disabled) is therefore never blindly re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, hasRoom: boolean): void {\n // A restored Turbo snapshot can carry pending markers from the previous visit\n // that this instance never created; release them from the DOM record before\n // deciding, or the displaced `aria-disabled` would never be given back.\n if (\n !this.#pendingButtonDisables.has(button) &&\n button.hasAttribute(\"data-overflow-indicator-pending-disabled\")\n ) {\n this.#cancelPendingButtonDisable(button);\n }\n if (hasRoom) {\n this.#cancelPendingButtonDisable(button);\n if (button.hasAttribute(\"data-overflow-indicator-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-overflow-indicator-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) {\n this.#deferButtonDisable(button);\n } else {\n this.#disableButton(button);\n }\n }\n\n /**\n * Keeps a boundary button focusable until native blur, exposing its temporary\n * inoperability with `aria-disabled` and making its action a no-op meanwhile.\n */\n #deferButtonDisable(button: HTMLButtonElement): void {\n if (this.#pendingButtonDisables.has(button)) return;\n button.setAttribute(\"data-overflow-indicator-pending-disabled\", \"\");\n // The ownership marker carries the value we displace, so the DOM alone is\n // enough to undo it. A Turbo snapshot is cloned before `disconnect()` runs,\n // so a restored page arrives with these markers while the new instance has\n // no in-memory record of them — an empty marker means \"there was none\".\n button.setAttribute(\n \"data-overflow-indicator-aria-disabled\",\n button.getAttribute(\"aria-disabled\") ?? \"\",\n );\n button.setAttribute(\"aria-disabled\", \"true\");\n const onBlur = (): void => {\n this.#cancelPendingButtonDisable(button);\n // If the author disabled it while focus was pending, do not claim ownership.\n if (!button.disabled) this.#disableButton(button);\n };\n this.#pendingButtonDisables.set(button, { onBlur });\n button.addEventListener(\"blur\", onBlur);\n }\n\n #disableButton(button: HTMLButtonElement): void {\n button.disabled = true;\n button.setAttribute(\"data-overflow-indicator-disabled\", \"\");\n }\n\n #cancelPendingButtonDisable(button: HTMLButtonElement): void {\n const pending = this.#pendingButtonDisables.get(button);\n if (pending) button.removeEventListener(\"blur\", pending.onBlur);\n this.#pendingButtonDisables.delete(button);\n button.removeAttribute(\"data-overflow-indicator-pending-disabled\");\n const displaced = button.getAttribute(\"data-overflow-indicator-aria-disabled\");\n if (displaced !== null) {\n button.removeAttribute(\"data-overflow-indicator-aria-disabled\");\n if (button.getAttribute(\"aria-disabled\") === \"true\") {\n if (displaced === \"\") button.removeAttribute(\"aria-disabled\");\n else button.setAttribute(\"aria-disabled\", displaced);\n }\n }\n }\n\n #clearPendingButtonDisables(): void {\n for (const button of [...this.#pendingButtonDisables.keys()]) {\n this.#cancelPendingButtonDisable(button);\n }\n }\n\n /** Moves resize/mutation/load observation to the current viewport target. */\n #syncViewport(): void {\n if (!this.#connected) return;\n const next = this.hasViewportTarget ? this.viewportTarget : null;\n if (next === this.#observedViewport) return;\n this.#stopObservingViewport();\n if (!next) return;\n\n this.#observedViewport = next;\n this.#layout.observe(next);\n this.#layout.observeViewport();\n next.addEventListener(\"load\", this.#onContentLoad, true);\n this.#syncContentObservation();\n\n if (typeof MutationObserver !== \"undefined\") {\n this.#mutationObserver = new MutationObserver(() => {\n if (!this.#connected || this.#observedViewport !== next) return;\n this.#syncContentObservation();\n this.update();\n });\n this.#mutationObserver.observe(this.element, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"hidden\"],\n });\n }\n this.#state = null;\n this.update();\n }\n\n /** Observes direct content boxes whose resize can change the viewport's scroll extent. */\n #syncContentObservation(): void {\n const next = new Set<Element>(this.#observedViewport?.children ?? []);\n for (const content of this.#observedContent) {\n if (!next.has(content)) {\n this.#layout.unobserve(content);\n this.#observedContent.delete(content);\n }\n }\n for (const content of next) {\n if (!this.#observedContent.has(content)) {\n this.#observedContent.add(content);\n this.#layout.observe(content);\n }\n }\n }\n\n #stopObservingViewport(): void {\n this.#mutationObserver?.disconnect();\n this.#mutationObserver = null;\n this.#observedViewport?.removeEventListener(\"load\", this.#onContentLoad, true);\n if (this.#observedViewport) this.#layout.unobserve(this.#observedViewport);\n for (const content of this.#observedContent) this.#layout.unobserve(content);\n this.#observedContent.clear();\n this.#observedViewport = null;\n this.#layout.unobserveViewport();\n }\n\n readonly #onContentLoad = (): void => {\n if (this.#connected) this.update();\n };\n\n get #threshold(): number {\n const value = this.thresholdValue;\n return Number.isFinite(value) ? Math.max(0, value) : 1;\n }\n\n #directionFromEvent(event: Event): \"start\" | \"end\" | null {\n const params = (event as Event & { params?: { direction?: unknown } }).params;\n const direction = params?.direction;\n return direction === \"start\" || direction === \"end\" ? direction : null;\n }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless password show/hide (unmask) toggle behavior.
5
+ *
6
+ * Markup contract (identifier: `stimeo--password-reveal`):
7
+ * <div data-controller="stimeo--password-reveal">
8
+ * <input type="password" aria-label="Password"
9
+ * data-stimeo--password-reveal-target="input">
10
+ * <button type="button" aria-pressed="false" aria-label="Show password"
11
+ * data-stimeo--password-reveal-target="toggle"
12
+ * data-action="stimeo--password-reveal#toggle"></button>
13
+ * </div>
14
+ *
15
+ * No dedicated APG pattern; this follows the toggle **Button** practice. The
16
+ * accessible name stays state-independent ("Show password") while the pressed
17
+ * state is conveyed by `aria-pressed`.
18
+ *
19
+ * @remarks
20
+ * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /
21
+ * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and
22
+ * the caret, so when (and only when) the input was the focused element its focus
23
+ * and selection are restored afterward; when the toggle button holds focus
24
+ * (keyboard use) focus is left on the button. An optional `autoHide` re-masks
25
+ * after a delay, and that timer is torn down on disconnect (Turbo).
26
+ */
27
+ declare class PasswordRevealController extends Controller<HTMLElement> {
28
+ #private;
29
+ static targets: string[];
30
+ static values: {
31
+ autoHide: {
32
+ type: NumberConstructor;
33
+ default: number;
34
+ };
35
+ };
36
+ static actions: readonly ["toggle"];
37
+ static events: readonly ["toggle"];
38
+ readonly inputTarget: HTMLInputElement;
39
+ readonly toggleTarget: HTMLElement;
40
+ readonly hasInputTarget: boolean;
41
+ readonly hasToggleTarget: boolean;
42
+ autoHideValue: number;
43
+ connect(): void;
44
+ disconnect(): void;
45
+ /** Toggles the input between masked and revealed. Bound via `data-action`. */
46
+ toggle(): void;
47
+ }
48
+
49
+ export { PasswordRevealController };