stimeo-ui 0.12.0 → 0.13.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 +37 -0
- package/dist/cable/index.d.ts +67 -13
- package/dist/cable/index.js +438 -152
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +1 -1
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -1
- package/dist/controllers/calendar_controller.js +10 -2
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +2 -2
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -2
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js +10 -10
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +1 -1
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +2 -2
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -1
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -2
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +3 -3
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +1 -1
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +23 -10
- package/dist/controllers/optimistic_controller.js +141 -20
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +2 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +3 -3
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +1 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -3
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +3 -2
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/toast_controller.js +1 -1
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +4 -3
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +101 -38
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +1 -1
- package/dist/inspector/cli.js +1 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +1 -1
- package/dist/inspector/manifest.json +6 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../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":["/** 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","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * 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 { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { isRtl } from \"../utils/logical_scroll\";\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. An outside click, or focus leaving the nav for a known external\n * destination, closes it. `ArrowLeft`/`ArrowRight` move focus between the\n * *navigable* triggers without rewriting `tabindex` (they keep their natural Tab\n * order); a modified press (`Alt`/`Ctrl`/`Meta`/`Shift`, e.g. the browser's\n * `Alt+←` history back) is left to the browser. Hover open/close is opt-in via\n * `openOnHover`.\n *\n * Every `panel` must be a **descendant of the nav element**: Stimulus resolves\n * targets within the controller's own scope, so a panel moved outside it (a\n * portal) never becomes a `panel` target and its trigger then opens nothing.\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 **exactly one**\n * trigger (e.g. the `<li>` holding a top-level *link*, its disclosure button, and\n * its panel — the APG \"Disclosure Navigation with Top-Level Links\" arrangement)\n * and hovering anywhere over that wrapper opens the contained trigger's panel.\n * Wrapping several triggers at once (marking the whole `<ul>`, say) is not\n * supported: every hover over the area then resolves to the *first* contained\n * trigger. A trigger or panel inside a `hoverArea` defers to the wrapper (its own\n * edges stop scheduling open/close), so pointer movement within the area never\n * flickers the panel. Keep a trigger's panel inside its `hoverArea`: a panel left\n * outside simply falls back to the default two-region behavior — moving straight\n * across a 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; the same\n * callbacks also run during Stimulus teardown, so they no-op once disconnected.\n * Flipping `openOnHover` at runtime wires or unwires the listeners in place.\n * `hoverArea` has no effect unless `openOnHover` is enabled.\n *\n * Pointer-driven closing never hides focused content — hiding the panel would\n * take the focused link with it and drop focus to the body. So a hover *leave*\n * keeps the panel open while focus is inside it, and a hover *switch* to another\n * trigger returns focus to the outgoing trigger before hiding its panel. For the\n * same reason, while `openOnHover` is on, a click on a trigger whose hover region\n * the pointer currently occupies is a no-op: closing it would leave it unopenable\n * until the pointer left and re-entered (`mouseenter` does not re-fire in place).\n * `Escape` and outside clicks dismiss regardless.\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 a rebuild driven by target churn\n * can never strand a listener on an element that stopped being a target.\n */\n readonly #hoverWired = new Set<HTMLElement>();\n\n /**\n * The trigger whose hover region the pointer currently occupies, if any. Only\n * the click semantics under `openOnHover` read it (see {@link toggle}).\n */\n #hoveredTrigger: HTMLElement | null = null;\n\n /**\n * Whether the controller is between `connect()` and `disconnect()`. Stimulus\n * disconnects the controller **before** disconnecting its targets, so the\n * target callbacks below also fire during teardown; without this flag their\n * rebuild would re-wire hover listeners onto an already-disconnected nav.\n */\n #connected = false;\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.#connected = true;\n this.#closeAll();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\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 taken while connected. */\n override disconnect(): void {\n this.#connected = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#removeHoverListeners();\n this.#hoverTimers.clearAll();\n }\n\n /**\n * Follows a runtime flip of `openOnHover` (a morph or a scripted attribute\n * change) by wiring or unwiring the hover listeners in place. Stimulus also\n * fires this once before `connect()`, which the `#connected` guard skips —\n * `connect()` owns the initial wiring.\n */\n openOnHoverValueChanged(): void {\n if (!this.#connected) return;\n if (this.openOnHoverValue) this.#addHoverListeners();\n else this.#removeHoverListeners();\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 /**\n * Toggles a trigger's panel (single-open). Bound via `data-action` (click).\n *\n * An explicit activation always wins over a scheduled hover open/close, so any\n * pending hover timer is dropped first — otherwise a close scheduled just\n * before the click would fire into the freshly opened panel.\n *\n * Under `openOnHover`, closing a panel the pointer itself is holding open is\n * refused: the pointer stays put, `mouseenter` does not re-fire, and the panel\n * would be unopenable until the pointer left and came back. Keyboard\n * activation (pointer elsewhere) still toggles it closed, and `Escape` and\n * outside clicks dismiss either way.\n */\n toggle(event: Event): void {\n const trigger = event.currentTarget as HTMLElement;\n this.#hoverTimers.clearAll();\n if (!this.#isExpanded(trigger)) {\n this.#openPanel(trigger);\n return;\n }\n if (this.openOnHoverValue && this.#hoveredTrigger === trigger) return;\n this.#closePanel(trigger);\n }\n\n /**\n * `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order).\n *\n * Only navigable triggers are destinations, so a `hidden` or disabled item\n * never swallows the press. A modified arrow belongs to the browser or OS\n * (`Alt+←`/`Alt+→` is history back/forward), and with no destination at all\n * (a single-trigger nav) the press is left to the page — `preventDefault()` is\n * called only when focus actually moves, keeping the scroll-suppression\n * contract honest.\n */\n onTriggerKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO move the\n // trigger focus — composition depends on this yield. It runs ahead of the\n // modifier check, so a claimed press is yielded whether or not it carries a\n // modifier.\n if (event.defaultPrevented) return;\n if (event.key !== \"ArrowRight\" && event.key !== \"ArrowLeft\") return;\n if (isReservedArrowChord(event)) return;\n\n const current = event.currentTarget as HTMLElement;\n // The focused trigger stays in the ring even when it is not navigable\n // itself, so arrows still lead *out* of a trigger disabled while focused.\n const triggers = this.triggerTargets.filter(\n (trigger) => trigger === current || this.#isNavigable(trigger),\n );\n const index = triggers.indexOf(current);\n if (index === -1 || triggers.length < 2) return;\n\n event.preventDefault();\n // Logical, not physical: the triggers are an ordered row, so which one is\n // \"next\" follows the writing direction. Read from the controller element —\n // the nav lays the triggers out, and a trigger may carry its own `dir`.\n const forward = isRtl(this.element) ? \"ArrowLeft\" : \"ArrowRight\";\n const step = event.key === forward ? 1 : -1;\n triggers[(index + step + triggers.length) % triggers.length]?.focus();\n }\n\n /**\n * Opens `trigger`'s panel, closing any other open panel first. Re-opening an\n * already-open panel only re-asserts the Escape layer: the close/open\n * round-trip would rewrite `aria-expanded` and `hidden` for no state change.\n */\n #openPanel(trigger: HTMLElement): void {\n // Guarded here rather than in `toggle`, because hover reaches this method\n // without going through it. Reaching an `aria-disabled` trigger is what the\n // attribute asks for — opening its panel is the activation it forbids, and\n // opening a popup counts as activation.\n if (trigger.getAttribute(\"aria-disabled\") === \"true\") return;\n if (this.#isExpanded(trigger)) {\n this.#syncEscapeLayer();\n return;\n }\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 /**\n * Closes every open panel and normalizes `aria-expanded=\"false\"` on **all**\n * triggers, so a trigger whose panel was hidden by other means cannot keep\n * advertising an open panel.\n */\n #closeAll(): void {\n for (const trigger of this.triggerTargets) this.#closePanel(trigger);\n }\n\n /**\n * Whether a trigger can receive arrow-key focus: not `hidden`, not a natively\n * `disabled` control (mirrors `toolbar`). CSS-only visibility cannot be\n * detected headlessly and stays the consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable.** APG separates the attributes by\n * intent: `disabled` is for controls a neighbour makes inferable, while\n * `aria-disabled` marks one that must remain *discoverable* — and a nav\n * section the user cannot even arrow to is a section they cannot learn exists.\n * Opening is suppressed separately, so the trigger announces itself and does\n * nothing.\n */\n #isNavigable(trigger: HTMLElement): boolean {\n if (trigger.hasAttribute(\"hidden\")) return false;\n return !(trigger as HTMLButtonElement | HTMLInputElement).disabled;\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 /**\n * Closes any open panel and returns focus to its trigger (Escape path). A\n * pending hover open would otherwise re-open the panel the user just\n * dismissed, so the reservation is dropped with it.\n */\n #closeAndRestore(): void {\n const open = this.#openTrigger;\n if (!open) return;\n this.#hoverTimers.clearAll();\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.#hoveredTrigger = trigger;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#openFromHover(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.#hoveredTrigger = null;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);\n };\n\n /**\n * Hover-driven open. Switching panels would hide the outgoing one, so focus\n * standing inside it is handed back to its trigger first: pointer movement\n * must never send focus to the body (the keyboard user would lose their\n * place). Focus outside the closing panel is left alone.\n */\n #openFromHover(trigger: HTMLElement): void {\n // Checked before the rescue below, not only inside `#openPanel`. The rescue\n // exists to get focus out of a panel that is about to be hidden; when the\n // destination refuses to open, nothing is hidden and moving focus buys\n // nothing — it just walks the caret out of a panel that stays open.\n if (trigger.getAttribute(\"aria-disabled\") === \"true\") return;\n const open = this.#openTrigger;\n if (open && open !== trigger && this.#holdsFocus(this.#panelFor(open))) open.focus();\n this.#openPanel(trigger);\n }\n\n /**\n * Hover-driven close: skipped while focus sits inside the panel it would hide,\n * for the same no-focus-loss reason (cf. `hover_card`'s delayed close). The\n * panel then stays open until `Escape`, an outside click, or focus leaving the\n * nav dismisses it — all of which restore or keep focus deliberately.\n */\n #closeFromHover(): void {\n const open = this.#openTrigger;\n if (open && this.#holdsFocus(this.#panelFor(open))) return;\n this.#closeAll();\n }\n\n /** Whether `panel` exists and currently contains the focused element. */\n #holdsFocus(panel: HTMLElement | null): boolean {\n return panel?.contains(document.activeElement) ?? false;\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 this.#hoveredTrigger = null;\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 * Stimulus fires those callbacks during teardown too, hence the `#connected`\n * guard — a rebuild after `disconnect()` would resurrect the listeners.\n */\n #rewireHoverListeners(): void {\n if (!this.#connected || !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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../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":["/** 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","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { isRtl } from \"../utils/logical_scroll\";\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 `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. An outside click, or focus leaving the nav for a known external\n * destination, closes it. `ArrowLeft`/`ArrowRight` move focus between the\n * *navigable* triggers without rewriting `tabindex` (they keep their natural Tab\n * order); a modified press (`Alt`/`Ctrl`/`Meta`/`Shift`, e.g. the browser's\n * `Alt+←` history back) is left to the browser. Hover open/close is opt-in via\n * `openOnHover`.\n *\n * Every `panel` must be a **descendant of the nav element**: Stimulus resolves\n * targets within the controller's own scope, so a panel moved outside it (a\n * portal) never becomes a `panel` target and its trigger then opens nothing.\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 **exactly one**\n * trigger (e.g. the `<li>` holding a top-level *link*, its disclosure button, and\n * its panel — the APG \"Disclosure Navigation with Top-Level Links\" arrangement)\n * and hovering anywhere over that wrapper opens the contained trigger's panel.\n * Wrapping several triggers at once (marking the whole `<ul>`, say) is not\n * supported: every hover over the area then resolves to the *first* contained\n * trigger. A trigger or panel inside a `hoverArea` defers to the wrapper (its own\n * edges stop scheduling open/close), so pointer movement within the area never\n * flickers the panel. Keep a trigger's panel inside its `hoverArea`: a panel left\n * outside simply falls back to the default two-region behavior — moving straight\n * across a 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; the same\n * callbacks also run during Stimulus teardown, so they no-op once disconnected.\n * Flipping `openOnHover` at runtime wires or unwires the listeners in place.\n * `hoverArea` has no effect unless `openOnHover` is enabled.\n *\n * Pointer-driven closing never hides focused content — hiding the panel would\n * take the focused link with it and drop focus to the body. So a hover *leave*\n * keeps the panel open while focus is inside it, and a hover *switch* to another\n * trigger returns focus to the outgoing trigger before hiding its panel. For the\n * same reason, while `openOnHover` is on, a click on a trigger whose hover region\n * the pointer currently occupies is a no-op: closing it would leave it unopenable\n * until the pointer left and re-entered (`mouseenter` does not re-fire in place).\n * `Escape` and outside clicks dismiss regardless.\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 a rebuild driven by target churn\n * can never strand a listener on an element that stopped being a target.\n */\n readonly #hoverWired = new Set<HTMLElement>();\n\n /**\n * The trigger whose hover region the pointer currently occupies, if any. Only\n * the click semantics under `openOnHover` read it (see {@link toggle}).\n */\n #hoveredTrigger: HTMLElement | null = null;\n\n /**\n * Whether the controller is between `connect()` and `disconnect()`. Stimulus\n * disconnects the controller **before** disconnecting its targets, so the\n * target callbacks below also fire during teardown; without this flag their\n * rebuild would re-wire hover listeners onto an already-disconnected nav.\n */\n #connected = false;\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.#connected = true;\n this.#closeAll();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\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 taken while connected. */\n override disconnect(): void {\n this.#connected = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#removeHoverListeners();\n this.#hoverTimers.clearAll();\n }\n\n /**\n * Follows a runtime flip of `openOnHover` (a morph or a scripted attribute\n * change) by wiring or unwiring the hover listeners in place. Stimulus also\n * fires this once before `connect()`, which the `#connected` guard skips —\n * `connect()` owns the initial wiring.\n */\n openOnHoverValueChanged(): void {\n if (!this.#connected) return;\n if (this.openOnHoverValue) this.#addHoverListeners();\n else this.#removeHoverListeners();\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 /**\n * Toggles a trigger's panel (single-open). Bound via `data-action` (click).\n *\n * An explicit activation always wins over a scheduled hover open/close, so any\n * pending hover timer is dropped first — otherwise a close scheduled just\n * before the click would fire into the freshly opened panel.\n *\n * Under `openOnHover`, closing a panel the pointer itself is holding open is\n * refused: the pointer stays put, `mouseenter` does not re-fire, and the panel\n * would be unopenable until the pointer left and came back. Keyboard\n * activation (pointer elsewhere) still toggles it closed, and `Escape` and\n * outside clicks dismiss either way.\n */\n toggle(event: Event): void {\n const trigger = event.currentTarget as HTMLElement;\n this.#hoverTimers.clearAll();\n if (!this.#isExpanded(trigger)) {\n this.#openPanel(trigger);\n return;\n }\n if (this.openOnHoverValue && this.#hoveredTrigger === trigger) return;\n this.#closePanel(trigger);\n }\n\n /**\n * `ArrowLeft`/`ArrowRight` move focus between triggers (keeping Tab order).\n *\n * Only navigable triggers are destinations, so a `hidden` or disabled item\n * never swallows the press. A modified arrow belongs to the browser or OS\n * (`Alt+←`/`Alt+→` is history back/forward), and with no destination at all\n * (a single-trigger nav) the press is left to the page — `preventDefault()` is\n * called only when focus actually moves, keeping the scroll-suppression\n * contract honest.\n */\n onTriggerKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO move the\n // trigger focus — composition depends on this yield. It runs ahead of the\n // modifier check, so a claimed press is yielded whether or not it carries a\n // modifier.\n if (event.defaultPrevented) return;\n if (event.key !== \"ArrowRight\" && event.key !== \"ArrowLeft\") return;\n if (isReservedArrowChord(event)) return;\n\n const current = event.currentTarget as HTMLElement;\n // The focused trigger stays in the ring even when it is not navigable\n // itself, so arrows still lead *out* of a trigger disabled while focused.\n const triggers = this.triggerTargets.filter(\n (trigger) => trigger === current || this.#isNavigable(trigger),\n );\n const index = triggers.indexOf(current);\n if (index === -1 || triggers.length < 2) return;\n\n event.preventDefault();\n // Logical, not physical: the triggers are an ordered row, so which one is\n // \"next\" follows the writing direction. Read from the controller element —\n // the nav lays the triggers out, and a trigger may carry its own `dir`.\n const forward = isRtl(this.element) ? \"ArrowLeft\" : \"ArrowRight\";\n const step = event.key === forward ? 1 : -1;\n triggers[(index + step + triggers.length) % triggers.length]?.focus();\n }\n\n /**\n * Opens `trigger`'s panel, closing any other open panel first. Re-opening an\n * already-open panel only re-asserts the Escape layer: the close/open\n * round-trip would rewrite `aria-expanded` and `hidden` for no state change.\n */\n #openPanel(trigger: HTMLElement): void {\n // Guarded here rather than in `toggle`, because hover reaches this method\n // without going through it. Reaching an `aria-disabled` trigger is what the\n // attribute asks for — opening its panel is the activation it forbids, and\n // opening a popup counts as activation.\n if (trigger.getAttribute(\"aria-disabled\") === \"true\") return;\n if (this.#isExpanded(trigger)) {\n this.#syncEscapeLayer();\n return;\n }\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 /**\n * Closes every open panel and normalizes `aria-expanded=\"false\"` on **all**\n * triggers, so a trigger whose panel was hidden by other means cannot keep\n * advertising an open panel.\n */\n #closeAll(): void {\n for (const trigger of this.triggerTargets) this.#closePanel(trigger);\n }\n\n /**\n * Whether a trigger can receive arrow-key focus: not `hidden`, not a natively\n * `disabled` control (mirrors `toolbar`). CSS-only visibility cannot be\n * detected headlessly and stays the consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable.** APG separates the attributes by\n * intent: `disabled` is for controls a neighbour makes inferable, while\n * `aria-disabled` marks one that must remain *discoverable* — and a nav\n * section the user cannot even arrow to is a section they cannot learn exists.\n * Opening is suppressed separately, so the trigger announces itself and does\n * nothing.\n */\n #isNavigable(trigger: HTMLElement): boolean {\n if (trigger.hasAttribute(\"hidden\")) return false;\n return !(trigger as HTMLButtonElement | HTMLInputElement).disabled;\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 /**\n * Closes any open panel and returns focus to its trigger (Escape path). A\n * pending hover open would otherwise re-open the panel the user just\n * dismissed, so the reservation is dropped with it.\n */\n #closeAndRestore(): void {\n const open = this.#openTrigger;\n if (!open) return;\n this.#hoverTimers.clearAll();\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.#hoveredTrigger = trigger;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#openFromHover(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.#hoveredTrigger = null;\n this.#hoverTimers.clearAll();\n this.#hoverTimers.set(() => this.#closeFromHover(), this.hoverDelayValue);\n };\n\n /**\n * Hover-driven open. Switching panels would hide the outgoing one, so focus\n * standing inside it is handed back to its trigger first: pointer movement\n * must never send focus to the body (the keyboard user would lose their\n * place). Focus outside the closing panel is left alone.\n */\n #openFromHover(trigger: HTMLElement): void {\n // Checked before the rescue below, not only inside `#openPanel`. The rescue\n // exists to get focus out of a panel that is about to be hidden; when the\n // destination refuses to open, nothing is hidden and moving focus buys\n // nothing — it just walks the caret out of a panel that stays open.\n if (trigger.getAttribute(\"aria-disabled\") === \"true\") return;\n const open = this.#openTrigger;\n if (open && open !== trigger && this.#holdsFocus(this.#panelFor(open))) open.focus();\n this.#openPanel(trigger);\n }\n\n /**\n * Hover-driven close: skipped while focus sits inside the panel it would hide,\n * for the same no-focus-loss reason (cf. `hover_card`'s delayed close). The\n * panel then stays open until `Escape`, an outside click, or focus leaving the\n * nav dismisses it — all of which restore or keep focus deliberately.\n */\n #closeFromHover(): void {\n const open = this.#openTrigger;\n if (open && this.#holdsFocus(this.#panelFor(open))) return;\n this.#closeAll();\n }\n\n /** Whether `panel` exists and currently contains the focused element. */\n #holdsFocus(panel: HTMLElement | null): boolean {\n return panel?.contains(document.activeElement) ?? false;\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 this.#hoveredTrigger = null;\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 * Stimulus fires those callbacks during teardown too, hence the `#connected`\n * guard — a rebuild after `disconnect()` would resurrect the listeners.\n */\n #rewireHoverListeners(): void {\n if (!this.#connected || !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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/focus_candidate.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/nested_form_controller.ts"],"names":[],"mappings":"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","file":"nested_form_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Selects any nested-form root; the nearest one is an element's owning instance. */\nconst ROOT_SELECTOR = '[data-controller~=\"stimeo--nested-form\"]';\n/** Selects per-row remove buttons (resolved through delegation, never per-row wiring). */\nconst REMOVE_SELECTOR = '[data-stimeo--nested-form-target=\"remove\"]';\n/** Selects the hidden `_destroy` input a persisted row carries. */\nconst DESTROY_FLAG_SELECTOR = '[data-stimeo--nested-form-target=\"destroyFlag\"]';\n\n/** `_destroy` values Rails treats as truthy; the flag value is the destruction truth source. */\nconst DESTROYED_VALUES = new Set([\"1\", \"true\"]);\n\n/**\n * Headless **nested / dynamic fields** for Rails `fields_for` +\n * `accepts_nested_attributes_for` (no dedicated APG pattern — form editing).\n * Clone a `<template>` row, renumber its index, and remove rows by flagging\n * `_destroy` (persisted) or dropping them from the DOM (unsaved).\n *\n * Markup contract (identifier: `stimeo--nested-form`):\n * <div data-controller=\"stimeo--nested-form\" data-stimeo--nested-form-min-value=\"1\">\n * <div data-stimeo--nested-form-target=\"list\">\n * <!-- existing rows; a persisted row carries a _destroy flag + remove button -->\n * </div>\n * <template data-stimeo--nested-form-target=\"template\">\n * <fieldset>\n * <input name=\"order[items_attributes][__INDEX__][name]\">\n * <button type=\"button\" data-stimeo--nested-form-target=\"remove\">Remove</button>\n * </fieldset>\n * </template>\n * <button type=\"button\" data-stimeo--nested-form-target=\"add\"\n * data-action=\"click->stimeo--nested-form#add\">Add</button>\n * </div>\n *\n * Values: `min` / `max` bound the effective row count (`max` `0` = unlimited;\n * both are followed at runtime), `indexPlaceholder` is the template token\n * replaced per row (default `__INDEX__`), and `announce` + `countMessage` (a\n * `{count}` template) opt into the announcer bridge.\n *\n * `add` dispatches `{ index, element }`; `remove` dispatches `{ element, persisted }`;\n * `reconcile` dispatches `{ count, atMin, atMax }` when a change the controller did\n * not perform itself — rows appended or removed by Turbo Streams / a morph, or a\n * runtime `min` / `max` change — moves the published state.\n *\n * @remarks\n * Behavior only — server-side `accepts_nested_attributes_for`, per-field\n * validation, and reordering are out of scope. Row state lives **only** in the DOM\n * (inserted nodes + each `_destroy` hidden input); there is no module-scope index\n * counter, so the controller stays idempotent across Turbo swaps. A row counts as\n * destroyed when its own `_destroy` flag holds a truthy value — `hidden` is the\n * visual half the controller writes alongside the flag, so a consumer hiding rows\n * for other reasons does not affect the count. Remove buttons and destroy flags\n * are resolved by **delegation scoped to their nearest nested-form root**, so\n * dynamically-added rows work without per-row `data-action` and one instance\n * nested inside another never acts on the inner instance's buttons or flags.\n * External row changes are observed on the list and reconciled once per mutation\n * batch. Adding a row moves focus to its first tab stop; removing returns focus to\n * the nearest surviving row's first tab stop, falling back to the add button and\n * finally to the root via a temporary `tabindex` (WCAG 2.2 2.4.3) — candidates\n * that cannot take focus (natively `disabled`, inside `fieldset[disabled]`, or not\n * rendered) are skipped. Count changes from the controller's own add / remove are\n * announced through the shared `stimeo--announcer` (WCAG 2.2 4.1.3) when\n * `announce` + `countMessage` are set; reconciliation stays silent to assistive\n * tech. The add button's `disabled` is managed only while `max` is set, and the\n * authored value is restored on teardown. A template must produce exactly one\n * root element; markup lacking the required `list` / `template` targets, or a\n * template producing anything else, is named on the console once per connection\n * and every operation stays a safe no-op with nothing left in the list. Clicking\n * remove on a row whose flag is already truthy only completes its hiding —\n * nothing effective changes, so no event and no announcement. The delegated\n * listener, the observer, and every lease are released on `disconnect()`.\n */\nexport class NestedFormController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"add\", \"remove\", \"destroyFlag\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 0 },\n indexPlaceholder: { type: String, default: \"__INDEX__\" },\n announce: { type: Boolean, default: true },\n countMessage: { type: String, default: \"\" },\n };\n static actions = [\"add\"] as const;\n static events = [\"add\", \"remove\", \"reconcile\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly addTarget: HTMLButtonElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n declare readonly hasAddTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare indexPlaceholderValue: string;\n declare announceValue: boolean;\n declare countMessageValue: string;\n\n /** Monotonic source for unique row indices; never a row-state counter. */\n #lastIndex = 0;\n\n #warnedMissing = false;\n #warnedTemplate = false;\n\n /** The state last written to the hooks; reconciliation reports only real moves. */\n #published: { count: number; atMin: boolean; atMax: boolean } | null = null;\n\n /** Watches the list for row changes the controller did not perform itself. */\n #observer: MutationObserver | null = null;\n\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileNow());\n /** Restores the authored add-button `disabled` when a lease ends. */\n readonly #addDisabled = new AttributeLease<HTMLButtonElement>(\"disabled\");\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n /** Makes the root a programmatic focus destination when no other candidate survives. */\n readonly #tabindex = new TabindexLoan();\n\n /**\n * Delegated click handler for the per-row remove buttons (dynamic-safe). Only\n * buttons whose nearest nested-form root is this instance are acted on, so a\n * nested inner form's buttons never remove an outer row.\n */\n readonly #onClick = (event: Event): void => {\n const target = event.target as HTMLElement | null;\n const button = target?.closest<HTMLElement>(REMOVE_SELECTOR);\n if (!button || this.#ownerOf(button) !== this.element) return;\n const row = this.#rowContaining(button);\n if (row) this.#removeRow(row);\n };\n\n override connect(): void {\n this.#warnedMissing = false;\n this.#warnedTemplate = false;\n this.element.addEventListener(\"click\", this.#onClick);\n this.#reconcile.activate();\n this.#beforeCache.activate();\n if (!this.hasListTarget || !this.hasTemplateTarget) this.#warnMissing();\n this.#refresh();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClick);\n this.#observer?.disconnect();\n this.#observer = null;\n this.#reconcile.cancel();\n this.#beforeCache.deactivate();\n this.#rewindForCache();\n this.#tabindex.returnAll();\n this.#published = null;\n }\n\n /** Follows an arriving or swapped-in list: rebind to the primary, then reconcile. */\n listTargetConnected(): void {\n this.#rebindObserver();\n this.#reconcile.schedule();\n }\n\n /** Follows a departing list the same way — the primary may have changed. */\n listTargetDisconnected(): void {\n this.#rebindObserver();\n this.#reconcile.schedule();\n }\n\n /**\n * Points the observer at the current primary list. Re-deriving on every list\n * arrival and departure makes the binding independent of the order Stimulus\n * reports an overlapping swap in — a staggered swap (successor appended before\n * the old list leaves) ends observed and reconciled either way.\n */\n #rebindObserver(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n if (!this.hasListTarget) return;\n this.#observer = new MutationObserver(() => this.#reconcile.schedule());\n this.#observer.observe(this.listTarget, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"value\"],\n });\n }\n\n /** Returns the lease with a departing add button; a new one re-arms on refresh. */\n addTargetDisconnected(target: HTMLButtonElement): void {\n this.#addDisabled.return(target);\n this.#reconcile.schedule();\n }\n\n addTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `min`. */\n minValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `max`. */\n maxValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Clones the template row, replaces the index placeholder with a unique value,\n * appends it, focuses its first tab stop, and announces the new count. No-ops at\n * `max`, when the required targets are missing (named on the console once per\n * connection), or when the template does not produce exactly one root element\n * (also named once; the insertion is rolled back so nothing accumulates).\n */\n add(): void {\n if (!this.hasListTarget || !this.hasTemplateTarget) {\n this.#warnMissing();\n return;\n }\n if (this.#atMax) return;\n\n const index = this.#nextIndex();\n const markup = this.templateTarget.innerHTML.replaceAll(\n this.indexPlaceholderValue,\n String(index),\n );\n const list = this.listTarget;\n const beforeNodes = list.childNodes.length;\n const beforeElements = list.childElementCount;\n list.insertAdjacentHTML(\"beforeend\", markup);\n const added = (Array.from(list.children) as HTMLElement[]).slice(beforeElements);\n if (added.length !== 1) {\n // A row is exactly one element. Anything else — escaped text, nothing, or\n // several roots (which the max gate above cannot account for) — rolls back\n // so repeated clicks cannot accumulate garbage or overshoot the max.\n while (list.childNodes.length > beforeNodes) list.lastChild?.remove();\n this.#warnBadTemplate(added.length);\n return;\n }\n const row = added[0] as HTMLElement;\n\n this.#refresh();\n firstTabStop(row)?.focus();\n this.dispatch(\"add\", { detail: { index, element: row } });\n this.#announce();\n }\n\n /**\n * Removes a row: a persisted row (one carrying its own `destroyFlag`) has the\n * flag set to `1` and is hidden so Rails destroys it on submit; an unsaved row\n * is dropped from the DOM. Returns focus to a surviving row. No-ops at `min`.\n */\n #removeRow(row: HTMLElement): void {\n const rows = this.#effectiveRows;\n if (this.#destroyed(row)) {\n // An already-destroyed row left visible (a server re-render keeps the\n // flag but not the hiding): complete the visual half. The effective\n // state does not move, so no event, no announcement, and no min gate.\n row.hidden = true;\n this.#focusAfterRemove(this.#positionAmong(rows, row));\n return;\n }\n if (rows.length <= this.minValue) return;\n\n const position = rows.indexOf(row);\n const flag = this.#destroyFlagOf(row);\n const persisted = flag !== null;\n if (flag) {\n flag.value = \"1\";\n row.hidden = true;\n } else {\n row.remove();\n }\n\n this.#refresh();\n this.#focusAfterRemove(Math.max(0, position));\n this.dispatch(\"remove\", { detail: { element: row, persisted } });\n this.#announce();\n }\n\n /**\n * The index of the first effective row following `row` in document order.\n * -1 (no following row) feeds the focus slices as a negative index, which\n * yields the same fully-reversed nearest-first order as `rows.length` would.\n */\n #positionAmong(rows: HTMLElement[], row: HTMLElement): number {\n return rows.findIndex(\n (candidate) =>\n (row.compareDocumentPosition(candidate) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0,\n );\n }\n\n /**\n * Moves focus to the first tab stop of the nearest surviving row — following\n * rows first, then preceding ones — falling back to the add button and finally\n * to the root via a temporary `tabindex`.\n */\n #focusAfterRemove(position: number): void {\n const rows = this.#effectiveRows;\n const ordered = [...rows.slice(position), ...rows.slice(0, position).reverse()];\n for (const row of ordered) {\n const stop = firstTabStop(row);\n if (stop) {\n stop.focus();\n return;\n }\n }\n if (this.hasAddTarget && isTabStop(this.addTarget)) {\n this.addTarget.focus();\n return;\n }\n this.#tabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Recomputes the count and min/max hooks from the DOM and records them as published. */\n #refresh(): void {\n if (!this.hasListTarget) return;\n const count = this.#effectiveRows.length;\n const atMin = count <= this.minValue;\n const atMax = this.maxValue > 0 && count >= this.maxValue;\n this.element.setAttribute(\"data-nested-count\", String(count));\n this.#reflect(\"data-nested-at-max\", atMax);\n this.#reflect(\"data-nested-at-min\", atMin);\n if (this.hasAddTarget) {\n // The button's disabled is controller state only while a max exists;\n // without one there is nothing to derive and the authored value stands.\n if (this.maxValue > 0) this.#addDisabled.write(this.addTarget, atMax ? \"\" : null);\n else this.#addDisabled.return(this.addTarget);\n }\n this.#published = { count, atMin, atMax };\n }\n\n /**\n * Applies row changes the controller did not perform itself (Turbo Streams,\n * morphs, runtime Value changes): refreshes the hooks and reports a moved\n * public state as `reconcile`. The controller's own operations refresh\n * synchronously first, so their observer echo arrives here as a no-move.\n */\n #reconcileNow(): void {\n const previous = this.#published;\n this.#refresh();\n const current = this.#published;\n if (!previous || !current) return;\n const moved =\n previous.count !== current.count ||\n previous.atMin !== current.atMin ||\n previous.atMax !== current.atMax;\n if (moved) this.dispatch(\"reconcile\", { detail: { ...current } });\n }\n\n /** Bridges the count change to the shared announcer when configured. */\n #announce(): void {\n if (!this.announceValue || this.countMessageValue === \"\") return;\n announce(fillTemplate(this.countMessageValue, { count: this.#effectiveRows.length }));\n }\n\n /** Sets `attribute` to `\"true\"` when `on`, else removes it. */\n #reflect(attribute: string, on: boolean): void {\n if (on) this.element.setAttribute(attribute, \"true\");\n else this.element.removeAttribute(attribute);\n }\n\n /** Returns the disabled lease so an authored value never leaks into a snapshot. */\n #rewindForCache(): void {\n if (this.hasAddTarget) this.#addDisabled.return(this.addTarget);\n }\n\n /** Names the missing required target(s) once per connection. */\n #warnMissing(): void {\n if (this.#warnedMissing) return;\n this.#warnedMissing = true;\n const missing = [\n this.hasListTarget ? null : 'a \"list\" target',\n this.hasTemplateTarget ? null : 'a \"template\" target',\n ]\n .filter((part): part is string => part !== null)\n .join(\" and \");\n console.warn(\n `Stimeo UI: \"${this.identifier}\" cannot manage rows because its markup lacks ${missing}.`,\n );\n }\n\n /** Names a template that does not produce exactly one element, once per connection. */\n #warnBadTemplate(produced: number): void {\n if (this.#warnedTemplate) return;\n this.#warnedTemplate = true;\n const reason = produced === 0 ? \"produces no element\" : \"must produce exactly one root element\";\n console.warn(`Stimeo UI: \"${this.identifier}\" added no row because its template ${reason}.`);\n }\n\n /** A strictly-increasing unique index (collision-free even on rapid adds). */\n #nextIndex(): number {\n const index = Math.max(Date.now(), this.#lastIndex + 1);\n this.#lastIndex = index;\n return index;\n }\n\n get #atMax(): boolean {\n return this.maxValue > 0 && this.#effectiveRows.length >= this.maxValue;\n }\n\n /**\n * Direct child rows of the list whose own destroy flag is not set. Callers\n * reach this only behind a list-presence gate (`add`, `#refresh`, and the\n * click path through `#rowContaining`).\n */\n get #effectiveRows(): HTMLElement[] {\n return (Array.from(this.listTarget.children) as HTMLElement[]).filter(\n (row) => !this.#destroyed(row),\n );\n }\n\n /** Whether `row` is flagged for destruction; the flag value is the truth source. */\n #destroyed(row: HTMLElement): boolean {\n const flag = this.#destroyFlagOf(row);\n return flag !== null && DESTROYED_VALUES.has(flag.value);\n }\n\n /** The row's own destroy flag, skipping flags owned by a nested inner form. */\n #destroyFlagOf(row: HTMLElement): HTMLInputElement | null {\n for (const flag of row.querySelectorAll<HTMLInputElement>(DESTROY_FLAG_SELECTOR)) {\n if (this.#ownerOf(flag) === this.element) return flag;\n }\n return null;\n }\n\n /** The nearest nested-form root that owns `el`. */\n #ownerOf(el: Element): Element | null {\n return el.closest(ROOT_SELECTOR);\n }\n\n /** The nearest ancestor of `el` that is a direct child of the list, else null. */\n #rowContaining(el: Element): HTMLElement | null {\n if (!this.hasListTarget) return null;\n let node: Element | null = el;\n while (node && node.parentElement !== this.listTarget) {\n node = node.parentElement;\n }\n return node as HTMLElement | null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/attribute_lease.ts","../../src/utils/before_cache_reset.ts","../../src/utils/focus_candidate.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/nested_form_controller.ts"],"names":[],"mappings":"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","file":"nested_form_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/** One temporarily controlled attribute value and the authored value it displaced. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n written: string | null;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n * A consumer that must not let a derived value become the authored baseline of a\n * cached page returns its leases from its own `turbo:before-cache` rewind, where the\n * matching `disconnect()` also releases the subscription.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #records = new Map<T, AttributeLeaseRecord>();\n\n /** @param attribute - The attribute whose temporary values this lease owns. */\n constructor(attribute: string) {\n this.#attribute = attribute;\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.written = value;\n } else {\n this.#records.set(element, {\n original: element.getAttribute(this.#attribute),\n written: value,\n });\n }\n\n this.#reflect(element, value);\n }\n\n /** Returns one lease without overwriting a value subsequently authored by a consumer. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n const stillOwned = element.getAttribute(this.#attribute) === record.written;\n if (stillOwned) this.#reflect(element, record.original);\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n if (element.getAttribute(this.#attribute) === value) return;\n if (value === null) element.removeAttribute(this.#attribute);\n else element.setAttribute(this.#attribute, value);\n }\n\n /** Returns every outstanding lease using the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { firstTabStop, isTabStop } from \"../utils/focus_candidate\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Selects any nested-form root; the nearest one is an element's owning instance. */\nconst ROOT_SELECTOR = '[data-controller~=\"stimeo--nested-form\"]';\n/** Selects per-row remove buttons (resolved through delegation, never per-row wiring). */\nconst REMOVE_SELECTOR = '[data-stimeo--nested-form-target=\"remove\"]';\n/** Selects the hidden `_destroy` input a persisted row carries. */\nconst DESTROY_FLAG_SELECTOR = '[data-stimeo--nested-form-target=\"destroyFlag\"]';\n\n/** `_destroy` values Rails treats as truthy; the flag value is the destruction truth source. */\nconst DESTROYED_VALUES = new Set([\"1\", \"true\"]);\n\n/**\n * Headless **nested / dynamic fields** for Rails `fields_for` +\n * `accepts_nested_attributes_for` (no dedicated APG pattern — form editing).\n * Clone a `<template>` row, renumber its index, and remove rows by flagging\n * `_destroy` (persisted) or dropping them from the DOM (unsaved).\n *\n * Markup contract (identifier: `stimeo--nested-form`):\n * <div data-controller=\"stimeo--nested-form\" data-stimeo--nested-form-min-value=\"1\">\n * <div data-stimeo--nested-form-target=\"list\">\n * <!-- existing rows; a persisted row carries a _destroy flag + remove button -->\n * </div>\n * <template data-stimeo--nested-form-target=\"template\">\n * <fieldset>\n * <input name=\"order[items_attributes][__INDEX__][name]\">\n * <button type=\"button\" data-stimeo--nested-form-target=\"remove\">Remove</button>\n * </fieldset>\n * </template>\n * <button type=\"button\" data-stimeo--nested-form-target=\"add\"\n * data-action=\"click->stimeo--nested-form#add\">Add</button>\n * </div>\n *\n * Values: `min` / `max` bound the effective row count (`max` `0` = unlimited;\n * both are followed at runtime), `indexPlaceholder` is the template token\n * replaced per row (default `__INDEX__`), and `announce` + `countMessage` (a\n * `{count}` template) opt into the announcer bridge.\n *\n * `add` dispatches `{ index, element }`; `remove` dispatches `{ element, persisted }`;\n * `reconcile` dispatches `{ count, atMin, atMax }` when a change the controller did\n * not perform itself — rows appended or removed by Turbo Streams / a morph, or a\n * runtime `min` / `max` change — moves the published state.\n *\n * @remarks\n * Behavior only — server-side `accepts_nested_attributes_for`, per-field\n * validation, and reordering are out of scope. Row state lives **only** in the DOM\n * (inserted nodes + each `_destroy` hidden input); there is no module-scope index\n * counter, so the controller stays idempotent across Turbo swaps. A row counts as\n * destroyed when its own `_destroy` flag holds a truthy value — `hidden` is the\n * visual half the controller writes alongside the flag, so a consumer hiding rows\n * for other reasons does not affect the count. Remove buttons and destroy flags\n * are resolved by **delegation scoped to their nearest nested-form root**, so\n * dynamically-added rows work without per-row `data-action` and one instance\n * nested inside another never acts on the inner instance's buttons or flags.\n * External row changes are observed on the list and reconciled once per mutation\n * batch. Adding a row moves focus to its first tab stop; removing returns focus to\n * the nearest surviving row's first tab stop, falling back to the add button and\n * finally to the root via a temporary `tabindex` (WCAG 2.2 2.4.3) — candidates\n * that cannot take focus (natively `disabled`, inside `fieldset[disabled]`, or not\n * rendered) are skipped. Count changes from the controller's own add / remove are\n * announced through the shared `stimeo--announcer` (WCAG 2.2 4.1.3) when\n * `announce` + `countMessage` are set; reconciliation stays silent to assistive\n * tech. The add button's `disabled` is managed only while `max` is set, and the\n * authored value is restored on teardown. A template must produce exactly one\n * root element; markup lacking the required `list` / `template` targets, or a\n * template producing anything else, is named on the console once per connection\n * and every operation stays a safe no-op with nothing left in the list. Clicking\n * remove on a row whose flag is already truthy only completes its hiding —\n * nothing effective changes, so no event and no announcement. The delegated\n * listener, the observer, and every lease are released on `disconnect()`.\n */\nexport class NestedFormController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"add\", \"remove\", \"destroyFlag\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 0 },\n indexPlaceholder: { type: String, default: \"__INDEX__\" },\n announce: { type: Boolean, default: true },\n countMessage: { type: String, default: \"\" },\n };\n static actions = [\"add\"] as const;\n static events = [\"add\", \"remove\", \"reconcile\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly addTarget: HTMLButtonElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n declare readonly hasAddTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare indexPlaceholderValue: string;\n declare announceValue: boolean;\n declare countMessageValue: string;\n\n /** Monotonic source for unique row indices; never a row-state counter. */\n #lastIndex = 0;\n\n #warnedMissing = false;\n #warnedTemplate = false;\n\n /** The state last written to the hooks; reconciliation reports only real moves. */\n #published: { count: number; atMin: boolean; atMax: boolean } | null = null;\n\n /** Watches the list for row changes the controller did not perform itself. */\n #observer: MutationObserver | null = null;\n\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileNow());\n /** Restores the authored add-button `disabled` when a lease ends. */\n readonly #addDisabled = new AttributeLease<HTMLButtonElement>(\"disabled\");\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n /** Makes the root a programmatic focus destination when no other candidate survives. */\n readonly #tabindex = new TabindexLoan();\n\n /**\n * Delegated click handler for the per-row remove buttons (dynamic-safe). Only\n * buttons whose nearest nested-form root is this instance are acted on, so a\n * nested inner form's buttons never remove an outer row.\n */\n readonly #onClick = (event: Event): void => {\n const target = event.target as HTMLElement | null;\n const button = target?.closest<HTMLElement>(REMOVE_SELECTOR);\n if (!button || this.#ownerOf(button) !== this.element) return;\n const row = this.#rowContaining(button);\n if (row) this.#removeRow(row);\n };\n\n override connect(): void {\n this.#warnedMissing = false;\n this.#warnedTemplate = false;\n this.element.addEventListener(\"click\", this.#onClick);\n this.#reconcile.activate();\n this.#beforeCache.activate();\n if (!this.hasListTarget || !this.hasTemplateTarget) this.#warnMissing();\n this.#refresh();\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"click\", this.#onClick);\n this.#observer?.disconnect();\n this.#observer = null;\n this.#reconcile.cancel();\n this.#beforeCache.deactivate();\n this.#rewindForCache();\n this.#tabindex.returnAll();\n this.#published = null;\n }\n\n /** Follows an arriving or swapped-in list: rebind to the primary, then reconcile. */\n listTargetConnected(): void {\n this.#rebindObserver();\n this.#reconcile.schedule();\n }\n\n /** Follows a departing list the same way — the primary may have changed. */\n listTargetDisconnected(): void {\n this.#rebindObserver();\n this.#reconcile.schedule();\n }\n\n /**\n * Points the observer at the current primary list. Re-deriving on every list\n * arrival and departure makes the binding independent of the order Stimulus\n * reports an overlapping swap in — a staggered swap (successor appended before\n * the old list leaves) ends observed and reconciled either way.\n */\n #rebindObserver(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n if (!this.hasListTarget) return;\n this.#observer = new MutationObserver(() => this.#reconcile.schedule());\n this.#observer.observe(this.listTarget, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"value\"],\n });\n }\n\n /** Returns the lease with a departing add button; a new one re-arms on refresh. */\n addTargetDisconnected(target: HTMLButtonElement): void {\n this.#addDisabled.return(target);\n this.#reconcile.schedule();\n }\n\n addTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `min`. */\n minValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-clamps when application code or a Turbo morph changes `max`. */\n maxValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Clones the template row, replaces the index placeholder with a unique value,\n * appends it, focuses its first tab stop, and announces the new count. No-ops at\n * `max`, when the required targets are missing (named on the console once per\n * connection), or when the template does not produce exactly one root element\n * (also named once; the insertion is rolled back so nothing accumulates).\n */\n add(): void {\n if (!this.hasListTarget || !this.hasTemplateTarget) {\n this.#warnMissing();\n return;\n }\n if (this.#atMax) return;\n\n const index = this.#nextIndex();\n const markup = this.templateTarget.innerHTML.replaceAll(\n this.indexPlaceholderValue,\n String(index),\n );\n const list = this.listTarget;\n const beforeNodes = list.childNodes.length;\n const beforeElements = list.childElementCount;\n list.insertAdjacentHTML(\"beforeend\", markup);\n const added = (Array.from(list.children) as HTMLElement[]).slice(beforeElements);\n if (added.length !== 1) {\n // A row is exactly one element. Anything else — escaped text, nothing, or\n // several roots (which the max gate above cannot account for) — rolls back\n // so repeated clicks cannot accumulate garbage or overshoot the max.\n while (list.childNodes.length > beforeNodes) list.lastChild?.remove();\n this.#warnBadTemplate(added.length);\n return;\n }\n const row = added[0] as HTMLElement;\n\n this.#refresh();\n firstTabStop(row)?.focus();\n this.dispatch(\"add\", { detail: { index, element: row } });\n this.#announce();\n }\n\n /**\n * Removes a row: a persisted row (one carrying its own `destroyFlag`) has the\n * flag set to `1` and is hidden so Rails destroys it on submit; an unsaved row\n * is dropped from the DOM. Returns focus to a surviving row. No-ops at `min`.\n */\n #removeRow(row: HTMLElement): void {\n const rows = this.#effectiveRows;\n if (this.#destroyed(row)) {\n // An already-destroyed row left visible (a server re-render keeps the\n // flag but not the hiding): complete the visual half. The effective\n // state does not move, so no event, no announcement, and no min gate.\n row.hidden = true;\n this.#focusAfterRemove(this.#positionAmong(rows, row));\n return;\n }\n if (rows.length <= this.minValue) return;\n\n const position = rows.indexOf(row);\n const flag = this.#destroyFlagOf(row);\n const persisted = flag !== null;\n if (flag) {\n flag.value = \"1\";\n row.hidden = true;\n } else {\n row.remove();\n }\n\n this.#refresh();\n this.#focusAfterRemove(Math.max(0, position));\n this.dispatch(\"remove\", { detail: { element: row, persisted } });\n this.#announce();\n }\n\n /**\n * The index of the first effective row following `row` in document order.\n * -1 (no following row) feeds the focus slices as a negative index, which\n * yields the same fully-reversed nearest-first order as `rows.length` would.\n */\n #positionAmong(rows: HTMLElement[], row: HTMLElement): number {\n return rows.findIndex(\n (candidate) =>\n (row.compareDocumentPosition(candidate) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0,\n );\n }\n\n /**\n * Moves focus to the first tab stop of the nearest surviving row — following\n * rows first, then preceding ones — falling back to the add button and finally\n * to the root via a temporary `tabindex`.\n */\n #focusAfterRemove(position: number): void {\n const rows = this.#effectiveRows;\n const ordered = [...rows.slice(position), ...rows.slice(0, position).reverse()];\n for (const row of ordered) {\n const stop = firstTabStop(row);\n if (stop) {\n stop.focus();\n return;\n }\n }\n if (this.hasAddTarget && isTabStop(this.addTarget)) {\n this.addTarget.focus();\n return;\n }\n this.#tabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Recomputes the count and min/max hooks from the DOM and records them as published. */\n #refresh(): void {\n if (!this.hasListTarget) return;\n const count = this.#effectiveRows.length;\n const atMin = count <= this.minValue;\n const atMax = this.maxValue > 0 && count >= this.maxValue;\n this.element.setAttribute(\"data-nested-count\", String(count));\n this.#reflect(\"data-nested-at-max\", atMax);\n this.#reflect(\"data-nested-at-min\", atMin);\n if (this.hasAddTarget) {\n // The button's disabled is controller state only while a max exists;\n // without one there is nothing to derive and the authored value stands.\n if (this.maxValue > 0) this.#addDisabled.write(this.addTarget, atMax ? \"\" : null);\n else this.#addDisabled.return(this.addTarget);\n }\n this.#published = { count, atMin, atMax };\n }\n\n /**\n * Applies row changes the controller did not perform itself (Turbo Streams,\n * morphs, runtime Value changes): refreshes the hooks and reports a moved\n * public state as `reconcile`. The controller's own operations refresh\n * synchronously first, so their observer echo arrives here as a no-move.\n */\n #reconcileNow(): void {\n const previous = this.#published;\n this.#refresh();\n const current = this.#published;\n if (!previous || !current) return;\n const moved =\n previous.count !== current.count ||\n previous.atMin !== current.atMin ||\n previous.atMax !== current.atMax;\n if (moved) this.dispatch(\"reconcile\", { detail: { ...current } });\n }\n\n /** Bridges the count change to the shared announcer when configured. */\n #announce(): void {\n if (!this.announceValue || this.countMessageValue === \"\") return;\n announce(fillTemplate(this.countMessageValue, { count: this.#effectiveRows.length }));\n }\n\n /** Sets `attribute` to `\"true\"` when `on`, else removes it. */\n #reflect(attribute: string, on: boolean): void {\n if (on) this.element.setAttribute(attribute, \"true\");\n else this.element.removeAttribute(attribute);\n }\n\n /** Returns the disabled lease so an authored value never leaks into a snapshot. */\n #rewindForCache(): void {\n if (this.hasAddTarget) this.#addDisabled.return(this.addTarget);\n }\n\n /** Names the missing required target(s) once per connection. */\n #warnMissing(): void {\n if (this.#warnedMissing) return;\n this.#warnedMissing = true;\n const missing = [\n this.hasListTarget ? null : 'a \"list\" target',\n this.hasTemplateTarget ? null : 'a \"template\" target',\n ]\n .filter((part): part is string => part !== null)\n .join(\" and \");\n console.warn(\n `Stimeo UI: \"${this.identifier}\" cannot manage rows because its markup lacks ${missing}.`,\n );\n }\n\n /** Names a template that does not produce exactly one element, once per connection. */\n #warnBadTemplate(produced: number): void {\n if (this.#warnedTemplate) return;\n this.#warnedTemplate = true;\n const reason = produced === 0 ? \"produces no element\" : \"must produce exactly one root element\";\n console.warn(`Stimeo UI: \"${this.identifier}\" added no row because its template ${reason}.`);\n }\n\n /** A strictly-increasing unique index (collision-free even on rapid adds). */\n #nextIndex(): number {\n const index = Math.max(Date.now(), this.#lastIndex + 1);\n this.#lastIndex = index;\n return index;\n }\n\n get #atMax(): boolean {\n return this.maxValue > 0 && this.#effectiveRows.length >= this.maxValue;\n }\n\n /**\n * Direct child rows of the list whose own destroy flag is not set. Callers\n * reach this only behind a list-presence gate (`add`, `#refresh`, and the\n * click path through `#rowContaining`).\n */\n get #effectiveRows(): HTMLElement[] {\n return (Array.from(this.listTarget.children) as HTMLElement[]).filter(\n (row) => !this.#destroyed(row),\n );\n }\n\n /** Whether `row` is flagged for destruction; the flag value is the truth source. */\n #destroyed(row: HTMLElement): boolean {\n const flag = this.#destroyFlagOf(row);\n return flag !== null && DESTROYED_VALUES.has(flag.value);\n }\n\n /** The row's own destroy flag, skipping flags owned by a nested inner form. */\n #destroyFlagOf(row: HTMLElement): HTMLInputElement | null {\n for (const flag of row.querySelectorAll<HTMLInputElement>(DESTROY_FLAG_SELECTOR)) {\n if (this.#ownerOf(flag) === this.element) return flag;\n }\n return null;\n }\n\n /** The nearest nested-form root that owns `el`. */\n #ownerOf(el: Element): Element | null {\n return el.closest(ROOT_SELECTOR);\n }\n\n /** The nearest ancestor of `el` that is a direct child of the list, else null. */\n #rowContaining(el: Element): HTMLElement | null {\n if (!this.hasListTarget) return null;\n let node: Element | null = el;\n while (node && node.parentElement !== this.listTarget) {\n node = node.parentElement;\n }\n return node as HTMLElement | null;\n }\n}\n"]}
|