stimeo-ui 0.6.0 → 0.7.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 +62 -0
- package/dist/controllers/alert_dialog_controller.d.ts +2 -0
- package/dist/controllers/alert_dialog_controller.js +75 -4
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +47 -5
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +2 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +2 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -0
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +52 -20
- package/dist/controllers/character_counter_controller.js +338 -63
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +3 -0
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +3 -0
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +2 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +75 -4
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +42 -14
- package/dist/controllers/conditional_fields_controller.js +345 -51
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -0
- package/dist/controllers/confirm_controller.js +75 -4
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +2 -0
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +4 -0
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +2 -0
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +3 -0
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +75 -4
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +56 -24
- package/dist/controllers/direct_upload_controller.js +201 -45
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +14 -6
- package/dist/controllers/dirty_form_controller.js +192 -29
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +2 -0
- package/dist/controllers/dismissible_controller.js +83 -18
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +75 -4
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +2 -0
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +2 -0
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +2 -0
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -0
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -0
- package/dist/controllers/focus_controller.js +75 -4
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +50 -10
- package/dist/controllers/form_field_controller.js +280 -62
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +10 -8
- package/dist/controllers/form_validation_controller.js +208 -83
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +2 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +2 -0
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +2 -0
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +2 -0
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +2 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -0
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +2 -0
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +2 -0
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +2 -0
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +2 -0
- package/dist/controllers/number_input_controller.js +47 -5
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +3 -0
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +2 -0
- package/dist/controllers/overflow_menu_controller.js +2 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +2 -0
- package/dist/controllers/pagination_controller.js +2 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +2 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +2 -0
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +30 -16
- package/dist/controllers/persist_controller.js +432 -122
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +2 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +3 -0
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +3 -0
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +2 -0
- package/dist/controllers/rating_controller.js +9 -5
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +2 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +2 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +2 -0
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +23 -15
- package/dist/controllers/scroll_area_controller.js +462 -162
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +2 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -11
- package/dist/controllers/separator_controller.js +354 -38
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +83 -10
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -0
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -0
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +2 -0
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +2 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +2 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +94 -38
- package/dist/controllers/submit_once_controller.js +399 -121
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +2 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +2 -0
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -0
- package/dist/controllers/theme_controller.js +8 -6
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +2 -0
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -0
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +2 -0
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +2 -0
- package/dist/controllers/tree_view_controller.js +2 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +2387 -861
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +107 -3
- package/dist/inspector/cli.js +192 -14
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +254 -46
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +7 -7
- package/dist/inspector/manifest.json +434 -44
- package/dist/positioning/index.d.ts +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controllers/conditional_fields_controller.ts"],"names":[],"mappings":"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","file":"conditional_fields_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Trigger controls whose state drives region visibility. */\ntype Trigger = HTMLInputElement | HTMLSelectElement;\n/** Form controls inside a region that get disabled while hidden. */\ntype Disableable = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;\n\n/** Marker on inputs this controller disabled (so we never re-enable authored-disabled ones). */\nconst DISABLED_MARKER = \"data-conditional-disabled\";\n\n/**\n * Headless dependent-fields behavior: shows/hides (and enables/disables) regions\n * based on trigger control values, keeping `hidden` / `aria-hidden` / `disabled` in\n * sync (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--conditional-fields`):\n * <form data-controller=\"stimeo--conditional-fields\">\n * <input type=\"checkbox\" data-stimeo--conditional-fields-target=\"trigger\">\n * <fieldset data-stimeo--conditional-fields-target=\"region\" data-when-checked>\n * …address…\n * </fieldset>\n * </form>\n *\n * Each region declares its condition with a data attribute: `data-when-checked`,\n * `data-when-unchecked`, or `data-when-value=\"x\"`. When the condition (combined\n * across triggers per `match`) holds the region is shown and its inputs enabled;\n * otherwise it is hidden, `aria-hidden`, and — with `disableHidden` — its inputs are\n * disabled so they drop out of the submission. Hiding a region that holds focus\n * first retreats focus to a trigger.\n *\n * @remarks\n * Behavior only. Visibility/enabled state lives entirely on the elements' `hidden` /\n * `disabled` / `aria-hidden`, re-derived from triggers on `connect()` (Morph-safe).\n * The change/input listener is removed on `disconnect()` (Turbo navigation included).\n */\nexport class ConditionalFieldsController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"region\"];\n static override values = {\n disableHidden: { type: Boolean, default: true },\n match: { type: String, default: \"any\" },\n };\n static actions = [\"evaluate\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly triggerTargets: Trigger[];\n declare readonly regionTargets: HTMLElement[];\n declare readonly hasTriggerTarget: boolean;\n\n declare disableHiddenValue: boolean;\n declare matchValue: string;\n\n readonly #lastVisible = new WeakMap<HTMLElement, boolean>();\n\n readonly #onChange = (): void => {\n this.evaluate();\n };\n\n override connect(): void {\n this.evaluate();\n this.element.addEventListener(\"change\", this.#onChange);\n this.element.addEventListener(\"input\", this.#onChange);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"change\", this.#onChange);\n this.element.removeEventListener(\"input\", this.#onChange);\n }\n\n /** Re-evaluates every region against the current trigger state. */\n evaluate(): void {\n // Resolved once per pass: this runs per keystroke (input/change bubble up from\n // the whole form) and every Stimulus target access re-queries the scope.\n const triggers = this.triggerTargets;\n for (const region of this.regionTargets) {\n this.#applyRegion(region, this.#isVisible(region, triggers));\n }\n }\n\n /** Applies a region's visibility, syncing hidden/aria/disabled and emitting change. */\n #applyRegion(region: HTMLElement, visible: boolean): void {\n const previous = this.#lastVisible.get(region) ?? !region.hidden;\n if (visible === previous && this.#lastVisible.has(region)) return;\n this.#lastVisible.set(region, visible);\n\n if (visible) {\n region.hidden = false;\n region.removeAttribute(\"aria-hidden\");\n region.setAttribute(\"data-visible\", \"true\");\n this.#setRegionDisabled(region, false);\n } else {\n this.#retreatFocus(region);\n region.hidden = true;\n region.setAttribute(\"aria-hidden\", \"true\");\n region.removeAttribute(\"data-visible\");\n this.#setRegionDisabled(region, true);\n }\n\n if (visible !== previous) {\n this.dispatch(\"change\", { detail: { region, visible } });\n }\n }\n\n /** Whether a region's declared condition holds across `triggers` per `match`. */\n #isVisible(region: HTMLElement, triggers: Trigger[]): boolean {\n const predicate = this.#predicateFor(region);\n if (predicate === null) return !region.hidden; // no condition declared → leave as-is\n if (triggers.length === 0) return false;\n return this.matchValue === \"all\" ? triggers.every(predicate) : triggers.some(predicate);\n }\n\n /** Builds the per-trigger predicate from the region's `data-when-*` attribute. */\n #predicateFor(region: HTMLElement): ((trigger: Trigger) => boolean) | null {\n if (region.hasAttribute(\"data-when-checked\")) {\n return (trigger) => this.#isChecked(trigger);\n }\n if (region.hasAttribute(\"data-when-unchecked\")) {\n return (trigger) => !this.#isChecked(trigger);\n }\n const wanted = region.dataset.whenValue;\n if (wanted !== undefined) {\n return (trigger) => this.#matchesValue(trigger, wanted);\n }\n return null;\n }\n\n #isChecked(trigger: Trigger): boolean {\n return trigger instanceof HTMLInputElement && trigger.checked;\n }\n\n /** A trigger \"has\" a value: selected radio/checkbox value, or the control value. */\n #matchesValue(trigger: Trigger, wanted: string): boolean {\n if (\n trigger instanceof HTMLInputElement &&\n (trigger.type === \"checkbox\" || trigger.type === \"radio\")\n ) {\n return trigger.checked && trigger.value === wanted;\n }\n return trigger.value === wanted;\n }\n\n /** Enables/disables a region's inputs, tracking only the ones we disabled. */\n #setRegionDisabled(region: HTMLElement, disabled: boolean): void {\n if (!this.disableHiddenValue) return;\n const controls = region.querySelectorAll<Disableable>(\"input, textarea, select, button\");\n for (const control of Array.from(controls)) {\n if (disabled) {\n if (!control.disabled) {\n control.disabled = true;\n control.setAttribute(DISABLED_MARKER, \"true\");\n }\n } else if (control.hasAttribute(DISABLED_MARKER)) {\n control.disabled = false;\n control.removeAttribute(DISABLED_MARKER);\n }\n }\n }\n\n /** Moves focus out of a region about to be hidden, to a trigger when possible. */\n #retreatFocus(region: HTMLElement): void {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !region.contains(active)) return;\n active.blur();\n if (this.hasTriggerTarget) this.triggerTargets[0]?.focus();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/focus_candidate.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/conditional_fields_controller.ts"],"names":[],"mappings":"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","file":"conditional_fields_controller.js","sourcesContent":["/**\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 for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\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","/**\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 (the `MicrotaskCoalescer` split).\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","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 { canTakeFocus } from \"../utils/focus_candidate\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Trigger controls whose live state drives region visibility. */\ntype Trigger = HTMLInputElement | HTMLSelectElement;\n/** Form controls inside a region that can be excluded from submission. */\ntype Disableable = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;\n/** One logical visibility transition settled during an evaluation pass. */\ntype RegionTransition = { region: HTMLElement; visible: boolean };\n\n/** Marker on controls this component disabled, so authored disabled state remains untouched. */\nconst DISABLED_MARKER = \"data-conditional-disabled\";\n\n/** Retained-DOM inputs and controller outputs that can require reconciliation. */\nconst OBSERVED_ATTRIBUTES = [\n \"hidden\",\n \"aria-hidden\",\n \"data-visible\",\n DISABLED_MARKER,\n \"disabled\",\n \"data-when-checked\",\n \"data-when-unchecked\",\n \"data-when-value\",\n \"checked\",\n \"selected\",\n \"value\",\n \"type\",\n];\n\n/** Attribute value expected after one controller-owned DOM write. */\ntype ExpectedAttribute = string | null;\n\n/**\n * Headless dependent-fields behavior: shows/hides (and enables/disables) regions\n * from trigger control values while keeping submission and accessibility state in\n * sync (no dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--conditional-fields`):\n * <form data-controller=\"stimeo--conditional-fields\">\n * <input type=\"checkbox\" data-stimeo--conditional-fields-target=\"trigger\">\n * <fieldset data-stimeo--conditional-fields-target=\"region\" data-when-checked>\n * …address…\n * </fieldset>\n * </form>\n *\n * A region declares one condition: `data-when-checked`, `data-when-unchecked`,\n * or `data-when-value=\"x\"`. The condition is combined across all triggers using\n * `match` (`\"any\"` by default, or `\"all\"`). A region with no condition preserves\n * its authored `hidden` state; a declared condition with no triggers is false.\n *\n * Hidden regions receive `hidden` and `aria-hidden=\"true\"`; visible regions expose\n * `data-visible=\"true\"`. With `disableHidden`, descendant controls are disabled and\n * marked with `data-conditional-disabled=\"true\"`, and only marked controls are\n * re-enabled. Before a focused region is hidden, focus moves to the first trigger\n * that can actually receive it. If every trigger is unsafe — including one inside\n * another region being hidden in the same pass — the controller root temporarily\n * receives `tabindex=\"-1\"` and focus.\n *\n * `change` and `reconcile` dispatch `{ region, visible }`.\n *\n * @remarks\n * Behavior only. Trigger `input` / `change`, dynamic targets and descendants,\n * condition attributes, Values, native form reset, and Turbo retained-element\n * morphs all reconcile against the live DOM. Direct writes to an input's live\n * `checked` / `value` property remain observable only when the caller also emits\n * the corresponding native event. DOM repair is silent. User input or the public\n * action dispatches `stimeo--conditional-fields:change`; a lifecycle, Value,\n * reset, mutation, or morph reconciliation dispatches\n * `stimeo--conditional-fields:reconcile` instead. Initial connection is silent.\n * Every listener, observer, queued pass, and borrowed root `tabindex` is released\n * on `disconnect()`.\n */\nexport class ConditionalFieldsController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"region\"];\n static override values = {\n disableHidden: { type: Boolean, default: true },\n match: { type: String, default: \"any\" },\n };\n static actions = [\"evaluate\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly triggerTargets: Trigger[];\n declare readonly regionTargets: HTMLElement[];\n\n declare disableHiddenValue: boolean;\n declare matchValue: string;\n\n /** Logical state last committed for each current region; rebuilt on every connect. */\n #lastVisible = new WeakMap<HTMLElement, boolean>();\n /** Controller writes awaiting an observer callback, separated from authored morphs. */\n #internalWrites = new WeakMap<Element, Map<string, ExpectedAttribute>>();\n #observing = false;\n\n /** Coalesces one target/morph/mutation batch into one full DOM reconciliation. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());\n /** Provides the last-resort focus landmark without claiming an authored tabindex. */\n readonly #rootTabindex = new TabindexLoan<HTMLElement>();\n /** Watches retained targets and controls whose declarative or reflected state changed. */\n readonly #observer = new MutationObserver((records) => this.#onMutations(records));\n\n /** Evaluates only events whose source is one of this instance's live trigger targets. */\n readonly #onTriggerInput = (event: Event): void => {\n const target = event.target;\n const triggers = this.triggerTargets;\n if (target instanceof Element && triggers.some((trigger) => trigger === target)) {\n this.#commitChange(triggers);\n }\n };\n\n /** Reconciles property-only retained-element changes after Turbo morphs. */\n readonly #onMorph = (): void => {\n this.#reconcile.schedule();\n };\n\n /** Reconciles the settled defaults after a non-cancelled owning form reset. */\n readonly #onReset = (event: Event): void => {\n const form = event.target;\n if (form instanceof HTMLFormElement && this.#hasTriggerOwnedBy(form)) {\n queueMicrotask(() => {\n if (!event.defaultPrevented) this.#reconcile.schedule();\n });\n }\n };\n\n /** Reflects initial live state and opens every retained-DOM reconciliation path. */\n override connect(): void {\n this.#lastVisible = new WeakMap();\n this.#internalWrites = new WeakMap();\n this.#reconcile.activate();\n this.#settle();\n this.#observer.observe(this.element, {\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n childList: true,\n subtree: true,\n });\n this.#observing = true;\n this.element.addEventListener(\"change\", this.#onTriggerInput);\n this.element.addEventListener(\"input\", this.#onTriggerInput);\n this.element.addEventListener(\"turbo:morph-element\", this.#onMorph);\n document.addEventListener(\"reset\", this.#onReset, true);\n }\n\n /** Releases all external resources and returns the temporary focus landmark loan. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#observing = false;\n this.#observer.disconnect();\n this.element.removeEventListener(\"change\", this.#onTriggerInput);\n this.element.removeEventListener(\"input\", this.#onTriggerInput);\n this.element.removeEventListener(\"turbo:morph-element\", this.#onMorph);\n document.removeEventListener(\"reset\", this.#onReset, true);\n this.#rootTabindex.returnAll();\n }\n\n /** Reconciles after a trigger target is inserted or restored. */\n triggerTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles after a trigger target is removed or replaced. */\n triggerTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Initializes a region target inserted or restored at runtime. */\n regionTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles the remaining region set after a target leaves. */\n regionTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles when application code or a morph changes the match policy. */\n matchValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Reconciles control ownership when hidden controls become enabled or excluded. */\n disableHiddenValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-evaluates every region and repairs all reflected DOM state. */\n evaluate(): void {\n this.#commitChange();\n }\n\n /**\n * Computes one settled plan, shows destinations before hiding sources, and then\n * returns logical transitions in DOM order after every region is internally coherent.\n */\n #settle(triggers: Trigger[] = this.triggerTargets): RegionTransition[] {\n const regions = this.regionTargets;\n const plan = new Map<HTMLElement, boolean>();\n for (const region of regions) plan.set(region, this.#isVisible(region, triggers));\n\n const changed = new Set<HTMLElement>();\n const applicationOrder = [\n ...regions.filter((region) => plan.get(region) === true),\n ...regions.filter((region) => plan.get(region) === false),\n ];\n for (const region of applicationOrder) {\n const visible = plan.get(region);\n if (visible !== undefined && this.#applyRegion(region, visible, plan)) {\n changed.add(region);\n }\n }\n\n return regions\n .filter((region) => changed.has(region))\n .map((region) => ({ region, visible: plan.get(region) === true }));\n }\n\n /** Reports logical transitions caused by user input or the public action. */\n #commitChange(triggers: Trigger[] = this.triggerTargets): void {\n for (const detail of this.#settle(triggers)) {\n this.dispatch(\"change\", { detail });\n }\n }\n\n /** Reports logical transitions derived from DOM, lifecycle, reset, or Value state. */\n #reconcileDom(): void {\n for (const detail of this.#settle()) {\n this.dispatch(\"reconcile\", { detail });\n }\n }\n\n /** Applies one planned state while keeping event history separate from DOM repair. */\n #applyRegion(\n region: HTMLElement,\n visible: boolean,\n plan: ReadonlyMap<HTMLElement, boolean>,\n ): boolean {\n const previous = this.#lastVisible.get(region) ?? !region.hidden;\n const changed = visible !== previous;\n this.#lastVisible.set(region, visible);\n\n if (!visible) this.#retreatFocus(region, plan);\n this.#syncRegionState(region, visible);\n this.#syncRegionControls(region, visible);\n return changed;\n }\n\n /** Reflects visibility idempotently, recording writes so the observer ignores its own work. */\n #syncRegionState(region: HTMLElement, visible: boolean): void {\n this.#writeAttribute(region, \"hidden\", visible ? null : \"\");\n this.#writeAttribute(region, \"aria-hidden\", visible ? null : \"true\");\n this.#writeAttribute(region, \"data-visible\", visible ? \"true\" : null);\n }\n\n /** Enables/disables descendant controls, touching only state carrying our marker. */\n #syncRegionControls(region: HTMLElement, visible: boolean): void {\n const shouldDisable = !visible && this.disableHiddenValue;\n const controls = region.querySelectorAll<Disableable>(\"input, textarea, select, button\");\n for (const control of controls) {\n const marked = control.hasAttribute(DISABLED_MARKER);\n if (shouldDisable) {\n if (!control.disabled) {\n this.#writeAttribute(control, \"disabled\", \"\");\n this.#writeAttribute(control, DISABLED_MARKER, \"true\");\n } else if (marked && control.getAttribute(DISABLED_MARKER) !== \"true\") {\n this.#writeAttribute(control, DISABLED_MARKER, \"true\");\n }\n } else if (marked) {\n this.#writeAttribute(control, \"disabled\", null);\n this.#writeAttribute(control, DISABLED_MARKER, null);\n }\n }\n }\n\n /** Whether a region's declared condition holds across `triggers` per `match`. */\n #isVisible(region: HTMLElement, triggers: Trigger[]): boolean {\n const predicate = this.#predicateFor(region);\n if (predicate === null) return !region.hidden;\n return (\n triggers.length > 0 &&\n (this.matchValue === \"all\" ? triggers.every(predicate) : triggers.some(predicate))\n );\n }\n\n /** Builds the deterministic per-trigger predicate from the region's condition. */\n #predicateFor(region: HTMLElement): ((trigger: Trigger) => boolean) | null {\n if (region.hasAttribute(\"data-when-checked\")) {\n return (trigger) => this.#isChecked(trigger);\n }\n if (region.hasAttribute(\"data-when-unchecked\")) {\n return (trigger) => !this.#isChecked(trigger);\n }\n const wanted = region.dataset.whenValue;\n if (wanted !== undefined) {\n return (trigger) => this.#matchesValue(trigger, wanted);\n }\n return null;\n }\n\n #isChecked(trigger: Trigger): boolean {\n return trigger instanceof HTMLInputElement && trigger.checked;\n }\n\n /** A trigger has a value when a radio/checkbox is selected, or its control value matches. */\n #matchesValue(trigger: Trigger, wanted: string): boolean {\n if (\n trigger instanceof HTMLInputElement &&\n (trigger.type === \"checkbox\" || trigger.type === \"radio\")\n ) {\n return trigger.checked && trigger.value === wanted;\n }\n return trigger.value === wanted;\n }\n\n /** Moves focus to the first safe trigger, or to the controller landmark as a fallback. */\n #retreatFocus(region: HTMLElement, plan: ReadonlyMap<HTMLElement, boolean>): void {\n const active = document.activeElement;\n if (active && region.contains(active)) this.#moveFocus(plan);\n }\n\n /** Moves focus after the caller establishes that the active node is being hidden. */\n #moveFocus(plan: ReadonlyMap<HTMLElement, boolean>): void {\n const destination = this.triggerTargets.find(\n (trigger) => canTakeFocus(trigger) && this.#survivesPlan(trigger, plan),\n );\n if (destination) {\n this.#rootTabindex.returnAll();\n destination.focus();\n return;\n }\n\n this.#rootTabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Whether a focus candidate sits outside every region this pass will hide. */\n #survivesPlan(candidate: HTMLElement, plan: ReadonlyMap<HTMLElement, boolean>): boolean {\n return [...plan].every(([region, visible]) => visible || !region.contains(candidate));\n }\n\n /** Records one idempotent attribute write before MutationObserver sees it. */\n #writeAttribute(element: Element, name: string, value: ExpectedAttribute): void {\n if (element.getAttribute(name) !== value) {\n if (this.#observing) {\n let writes = this.#internalWrites.get(element);\n if (!writes) {\n writes = new Map();\n this.#internalWrites.set(element, writes);\n }\n writes.set(name, value);\n }\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n }\n }\n\n /** Separates authored records from controller writes and queues one relevant repair. */\n #onMutations(records: MutationRecord[]): void {\n const triggers = this.triggerTargets;\n const regions = this.regionTargets;\n const touchedWrites = new Map<Element, Set<string>>();\n let needsReconcile = false;\n\n for (const record of records) {\n if (record.type === \"attributes\" && record.target instanceof Element) {\n const name = record.attributeName;\n const writes = this.#internalWrites.get(record.target);\n if (name && writes?.has(name)) {\n let names = touchedWrites.get(record.target);\n if (!names) {\n names = new Set();\n touchedWrites.set(record.target, names);\n }\n names.add(name);\n if (record.target.getAttribute(name) === writes.get(name)) continue;\n }\n }\n if (\n record.target instanceof Element &&\n this.#elementAffectsState(record.target, triggers, regions)\n ) {\n needsReconcile = true;\n }\n }\n\n for (const [element, names] of touchedWrites) {\n const writes = this.#internalWrites.get(element);\n for (const name of names) writes?.delete(name);\n }\n if (needsReconcile) this.#reconcile.schedule();\n }\n\n /** Whether an authored mutation target belongs to a trigger, region, or managed control. */\n #elementAffectsState(target: Element, triggers: Trigger[], regions: HTMLElement[]): boolean {\n return (\n triggers.some((trigger) => trigger === target || trigger.contains(target)) ||\n regions.some((region) => region === target || region.contains(target))\n );\n }\n\n /** Whether a form owns at least one current trigger, including `form=` associations. */\n #hasTriggerOwnedBy(form: HTMLFormElement): boolean {\n return this.triggerTargets.some((trigger) => trigger.form === form);\n }\n}\n"]}
|
|
@@ -25,6 +25,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
25
25
|
* <a href="/items/1" data-action="click->stimeo--confirm#request"
|
|
26
26
|
* data-stimeo--confirm-message-param="Delete this item?">Delete</a>
|
|
27
27
|
*
|
|
28
|
+
* `open` dispatches `{ message }`; `resolve` dispatches `{ confirmed }`.
|
|
29
|
+
*
|
|
28
30
|
* @remarks
|
|
29
31
|
* Behavior only — the dialog's a11y (focus trap, restore, roles) is delegated to
|
|
30
32
|
* the shared {@link FocusTrap}; this controller adds the Turbo bridge and the
|
|
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
115
115
|
}
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
+
// src/utils/focus_candidate.ts
|
|
119
|
+
function inheritsFieldsetDisabled(control) {
|
|
120
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
121
|
+
while (fieldset) {
|
|
122
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
123
|
+
if (!legend?.contains(control)) return true;
|
|
124
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function canTakeFocus(element) {
|
|
129
|
+
if (element.closest("[hidden], [inert]")) return false;
|
|
130
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
131
|
+
if (!("disabled" in element)) return true;
|
|
132
|
+
if (element.disabled) return false;
|
|
133
|
+
return !inheritsFieldsetDisabled(element);
|
|
134
|
+
}
|
|
135
|
+
var TAB_STOP_CANDIDATE_SELECTOR = [
|
|
136
|
+
"a[href]",
|
|
137
|
+
"area[href]",
|
|
138
|
+
"button",
|
|
139
|
+
"input",
|
|
140
|
+
"select",
|
|
141
|
+
"textarea",
|
|
142
|
+
"summary",
|
|
143
|
+
"iframe",
|
|
144
|
+
"audio[controls]",
|
|
145
|
+
"video[controls]",
|
|
146
|
+
"[tabindex]",
|
|
147
|
+
"[contenteditable]"
|
|
148
|
+
].join(",");
|
|
149
|
+
function isRenderedForFocus(element) {
|
|
150
|
+
const check = element.checkVisibility;
|
|
151
|
+
return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
|
|
152
|
+
}
|
|
153
|
+
function authoredTabindex(element) {
|
|
154
|
+
const value = element.getAttribute("tabindex");
|
|
155
|
+
if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
|
|
156
|
+
return Number(value);
|
|
157
|
+
}
|
|
158
|
+
function hasNativeTabStop(element) {
|
|
159
|
+
if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
|
|
160
|
+
return element.hasAttribute("href");
|
|
161
|
+
}
|
|
162
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (element instanceof HTMLInputElement) return element.type !== "hidden";
|
|
166
|
+
if (element instanceof HTMLIFrameElement) return true;
|
|
167
|
+
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
168
|
+
return element.hasAttribute("controls");
|
|
169
|
+
}
|
|
170
|
+
if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
|
|
171
|
+
const details = element.parentElement;
|
|
172
|
+
return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasEditableTabStop(element) {
|
|
177
|
+
const value = element.getAttribute("contenteditable")?.toLowerCase();
|
|
178
|
+
return value === "" || value === "true" || value === "plaintext-only";
|
|
179
|
+
}
|
|
180
|
+
function isTabStop(element) {
|
|
181
|
+
if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
|
|
182
|
+
const tabindex = authoredTabindex(element);
|
|
183
|
+
if (tabindex !== null) return tabindex >= 0;
|
|
184
|
+
return hasNativeTabStop(element) || hasEditableTabStop(element);
|
|
185
|
+
}
|
|
186
|
+
function tabStopsWithin(root) {
|
|
187
|
+
return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
|
|
188
|
+
isTabStop
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
118
192
|
// src/utils/focus_trap.ts
|
|
119
|
-
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
120
193
|
var FocusTrap = class {
|
|
121
194
|
/** The element focused before activation, restored on deactivation. */
|
|
122
195
|
#previouslyFocused = null;
|
|
@@ -278,9 +351,7 @@ var FocusTrap = class {
|
|
|
278
351
|
}
|
|
279
352
|
/** Collects the container's currently focusable descendants in DOM order. */
|
|
280
353
|
#focusableElements() {
|
|
281
|
-
return
|
|
282
|
-
(el) => !el.hidden
|
|
283
|
-
);
|
|
354
|
+
return tabStopsWithin(this.#getContainer());
|
|
284
355
|
}
|
|
285
356
|
};
|
|
286
357
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_trap.ts","../../src/controllers/confirm_controller.ts"],"names":[],"mappings":"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","file":"confirm_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\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 * 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","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/**\n * Selector matching the elements considered focusable. Shared by the trap's Tab\n * cycling and by form-validation's invalid-focus delegation.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return Array.from(this.#getContainer().querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hidden,\n );\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/** The slice of Turbo's form config this controller swaps the confirm method on. */\ninterface TurboFormsConfig {\n confirm?: (message: string, element?: HTMLElement) => boolean | Promise<boolean>;\n}\ninterface TurboLike {\n config?: { forms?: TurboFormsConfig };\n}\n\n/**\n * Headless **confirm bridge** — replaces the native `window.confirm()` Turbo uses\n * for `data-turbo-confirm` with an accessible **Alert Dialog** (WAI-ARIA APG Alert\n * Dialog pattern). Reuses the shared {@link FocusTrap}; the consumer only writes the\n * dialog markup and \"what to do on confirm\".\n *\n * Markup contract (identifier: `stimeo--confirm`):\n * <div data-controller=\"stimeo--confirm\">\n * <div data-stimeo--confirm-target=\"dialog\" role=\"alertdialog\" aria-modal=\"true\"\n * aria-labelledby=\"ct\" aria-describedby=\"cm\" hidden>\n * <h2 id=\"ct\" data-stimeo--confirm-target=\"title\">Are you sure?</h2>\n * <p id=\"cm\" data-stimeo--confirm-target=\"message\"></p>\n * <button data-stimeo--confirm-target=\"cancel\"\n * data-action=\"click->stimeo--confirm#cancel\"></button>\n * <button data-stimeo--confirm-target=\"confirm\"\n * data-action=\"click->stimeo--confirm#confirm\"></button>\n * </div>\n * </div>\n *\n * <!-- Driven automatically through Turbo's confirm hook: -->\n * <form data-turbo-confirm=\"Delete this item?\" action=\"/items/1\" method=\"post\">…</form>\n * <!-- Or intercept any link/button directly: -->\n * <a href=\"/items/1\" data-action=\"click->stimeo--confirm#request\"\n * data-stimeo--confirm-message-param=\"Delete this item?\">Delete</a>\n *\n * @remarks\n * Behavior only — the dialog's a11y (focus trap, restore, roles) is delegated to\n * the shared {@link FocusTrap}; this controller adds the Turbo bridge and the\n * confirm/cancel resolution. On `connect()` it swaps `Turbo.config.forms.confirm`\n * for a Promise-returning method and restores the original on `disconnect()` (Turbo\n * navigation included), so registration never leaks or stacks. Escape cancels\n * (returns `false`); when no dialog target exists it degrades to native\n * `window.confirm`. The least-destructive button (cancel, by default) takes initial\n * focus.\n */\nexport class ConfirmController extends Controller<HTMLElement> {\n static override targets = [\"dialog\", \"title\", \"message\", \"confirm\", \"cancel\"];\n static override values = {\n confirmLabel: { type: String, default: \"OK\" },\n cancelLabel: { type: String, default: \"Cancel\" },\n initialFocus: { type: String, default: \"cancel\" },\n };\n static actions = [\"confirm\", \"cancel\", \"request\"] as const;\n static events = [\"open\", \"resolve\"] as const;\n\n declare readonly dialogTarget: HTMLElement;\n /**\n * Static heading slot naming the dialog (via `aria-labelledby`). Never written\n * by the controller — the accessible name is author-owned; only `message` and\n * the button labels are injected per prompt.\n */\n declare readonly titleTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly confirmTarget: HTMLElement;\n declare readonly cancelTarget: HTMLElement;\n declare readonly hasDialogTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n declare readonly hasConfirmTarget: boolean;\n declare readonly hasCancelTarget: boolean;\n\n declare confirmLabelValue: string;\n declare cancelLabelValue: string;\n declare initialFocusValue: string;\n\n /** Resolver for the in-flight confirmation Promise (one dialog at a time). */\n #pending: ((confirmed: boolean) => void) | null = null;\n /** Turbo's forms config and its original confirm method, for restore. */\n #turboForms: TurboFormsConfig | null = null;\n #previousConfirm: TurboFormsConfig[\"confirm\"] = undefined;\n\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.#resolve(false),\n initialFocus: () => this.#initialFocusElement(),\n });\n\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n this.#installTurboHook();\n }\n\n override disconnect(): void {\n if (this.#turboForms) {\n this.#turboForms.confirm = this.#previousConfirm;\n this.#turboForms = null;\n }\n // Settle any pending confirmation as cancelled WITHOUT restoring focus: a Turbo\n // teardown must not move focus. The trailing deactivate covers the (defensive)\n // case where the trap is active with nothing pending.\n this.#resolve(false, false);\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Confirms the pending request (resolves `true`). Bound via `data-action`. */\n confirm(): void {\n this.#resolve(true);\n }\n\n /** Cancels the pending request (resolves `false`). Bound via `data-action`. */\n cancel(): void {\n this.#resolve(false);\n }\n\n /**\n * Intercepts a link/button click, shows the confirm dialog, and continues the\n * original action (form submit or navigation) only when confirmed. The message\n * comes from the `message` action param or the element's `data-turbo-confirm`.\n */\n request(event: Event): void {\n const element = (event.currentTarget ?? event.target) as HTMLElement | null;\n if (!element) return;\n event.preventDefault();\n\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = typeof params?.message === \"string\" ? params.message : null;\n const message = fromParam ?? element.getAttribute(\"data-turbo-confirm\") ?? \"\";\n\n void this.#prompt(message).then((confirmed) => {\n if (confirmed) this.#continue(element);\n });\n }\n\n /**\n * Opens the dialog for `message` and resolves once the user confirms or cancels.\n * Degrades to native `window.confirm` when no dialog target is present.\n */\n #prompt(message: string): Promise<boolean> {\n if (!this.hasDialogTarget) return Promise.resolve(window.confirm(message));\n // A second prompt while one is open cancels the first to keep a single dialog.\n this.#resolve(false);\n\n if (this.hasMessageTarget) this.messageTarget.textContent = message;\n if (this.hasConfirmTarget) this.confirmTarget.textContent = this.confirmLabelValue;\n if (this.hasCancelTarget) this.cancelTarget.textContent = this.cancelLabelValue;\n\n return new Promise<boolean>((resolve) => {\n this.#pending = resolve;\n this.dialogTarget.hidden = false;\n this.dispatch(\"open\", { detail: { message } });\n this.#trap.activate();\n });\n }\n\n /**\n * Settles the pending Promise, closes the dialog, and emits `resolve`.\n * `restoreFocus` is forwarded to the trap so teardown (disconnect) can settle a\n * pending confirmation without moving focus.\n */\n #resolve(confirmed: boolean, restoreFocus = true): void {\n const pending = this.#pending;\n if (!pending) return;\n this.#pending = null;\n\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n this.#trap.deactivate({ restoreFocus });\n this.dispatch(\"resolve\", { detail: { confirmed } });\n pending(confirmed);\n }\n\n /** Continues the intercepted action when confirmed: submit a form, else navigate. */\n #continue(element: HTMLElement): void {\n if (element instanceof HTMLAnchorElement && element.href) {\n window.location.href = element.href;\n return;\n }\n const form = element instanceof HTMLFormElement ? element : element.closest(\"form\");\n if (form) {\n form.requestSubmit(element instanceof HTMLButtonElement ? element : undefined);\n }\n }\n\n /** Swaps Turbo's confirm method for a Promise-returning bridge to this dialog. */\n #installTurboHook(): void {\n const turbo = (window as Window & { Turbo?: TurboLike }).Turbo;\n const forms = turbo?.config?.forms;\n if (!forms) return;\n this.#turboForms = forms;\n this.#previousConfirm = forms.confirm;\n forms.confirm = (message: string) => this.#prompt(message);\n }\n\n /** The button to focus on open — the least-destructive one (cancel) by default. */\n #initialFocusElement(): HTMLElement | null {\n if (this.initialFocusValue === \"confirm\" && this.hasConfirmTarget) return this.confirmTarget;\n return this.hasCancelTarget ? this.cancelTarget : null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/confirm_controller.ts"],"names":[],"mappings":"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","file":"confirm_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\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 * 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 * 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 for the consumer that needs this rule *without* the rest of\n * {@link canTakeFocus}: `toolbar` bounds its own `hidden` walk at the toolbar\n * root, which is a different rule, so it composes the two itself.\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","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/** The slice of Turbo's form config this controller swaps the confirm method on. */\ninterface TurboFormsConfig {\n confirm?: (message: string, element?: HTMLElement) => boolean | Promise<boolean>;\n}\ninterface TurboLike {\n config?: { forms?: TurboFormsConfig };\n}\n\n/**\n * Headless **confirm bridge** — replaces the native `window.confirm()` Turbo uses\n * for `data-turbo-confirm` with an accessible **Alert Dialog** (WAI-ARIA APG Alert\n * Dialog pattern). Reuses the shared {@link FocusTrap}; the consumer only writes the\n * dialog markup and \"what to do on confirm\".\n *\n * Markup contract (identifier: `stimeo--confirm`):\n * <div data-controller=\"stimeo--confirm\">\n * <div data-stimeo--confirm-target=\"dialog\" role=\"alertdialog\" aria-modal=\"true\"\n * aria-labelledby=\"ct\" aria-describedby=\"cm\" hidden>\n * <h2 id=\"ct\" data-stimeo--confirm-target=\"title\">Are you sure?</h2>\n * <p id=\"cm\" data-stimeo--confirm-target=\"message\"></p>\n * <button data-stimeo--confirm-target=\"cancel\"\n * data-action=\"click->stimeo--confirm#cancel\"></button>\n * <button data-stimeo--confirm-target=\"confirm\"\n * data-action=\"click->stimeo--confirm#confirm\"></button>\n * </div>\n * </div>\n *\n * <!-- Driven automatically through Turbo's confirm hook: -->\n * <form data-turbo-confirm=\"Delete this item?\" action=\"/items/1\" method=\"post\">…</form>\n * <!-- Or intercept any link/button directly: -->\n * <a href=\"/items/1\" data-action=\"click->stimeo--confirm#request\"\n * data-stimeo--confirm-message-param=\"Delete this item?\">Delete</a>\n *\n * `open` dispatches `{ message }`; `resolve` dispatches `{ confirmed }`.\n *\n * @remarks\n * Behavior only — the dialog's a11y (focus trap, restore, roles) is delegated to\n * the shared {@link FocusTrap}; this controller adds the Turbo bridge and the\n * confirm/cancel resolution. On `connect()` it swaps `Turbo.config.forms.confirm`\n * for a Promise-returning method and restores the original on `disconnect()` (Turbo\n * navigation included), so registration never leaks or stacks. Escape cancels\n * (returns `false`); when no dialog target exists it degrades to native\n * `window.confirm`. The least-destructive button (cancel, by default) takes initial\n * focus.\n */\nexport class ConfirmController extends Controller<HTMLElement> {\n static override targets = [\"dialog\", \"title\", \"message\", \"confirm\", \"cancel\"];\n static override values = {\n confirmLabel: { type: String, default: \"OK\" },\n cancelLabel: { type: String, default: \"Cancel\" },\n initialFocus: { type: String, default: \"cancel\" },\n };\n static actions = [\"confirm\", \"cancel\", \"request\"] as const;\n static events = [\"open\", \"resolve\"] as const;\n\n declare readonly dialogTarget: HTMLElement;\n /**\n * Static heading slot naming the dialog (via `aria-labelledby`). Never written\n * by the controller — the accessible name is author-owned; only `message` and\n * the button labels are injected per prompt.\n */\n declare readonly titleTarget: HTMLElement;\n declare readonly messageTarget: HTMLElement;\n declare readonly confirmTarget: HTMLElement;\n declare readonly cancelTarget: HTMLElement;\n declare readonly hasDialogTarget: boolean;\n declare readonly hasMessageTarget: boolean;\n declare readonly hasConfirmTarget: boolean;\n declare readonly hasCancelTarget: boolean;\n\n declare confirmLabelValue: string;\n declare cancelLabelValue: string;\n declare initialFocusValue: string;\n\n /** Resolver for the in-flight confirmation Promise (one dialog at a time). */\n #pending: ((confirmed: boolean) => void) | null = null;\n /** Turbo's forms config and its original confirm method, for restore. */\n #turboForms: TurboFormsConfig | null = null;\n #previousConfirm: TurboFormsConfig[\"confirm\"] = undefined;\n\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.#resolve(false),\n initialFocus: () => this.#initialFocusElement(),\n });\n\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n this.#installTurboHook();\n }\n\n override disconnect(): void {\n if (this.#turboForms) {\n this.#turboForms.confirm = this.#previousConfirm;\n this.#turboForms = null;\n }\n // Settle any pending confirmation as cancelled WITHOUT restoring focus: a Turbo\n // teardown must not move focus. The trailing deactivate covers the (defensive)\n // case where the trap is active with nothing pending.\n this.#resolve(false, false);\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Confirms the pending request (resolves `true`). Bound via `data-action`. */\n confirm(): void {\n this.#resolve(true);\n }\n\n /** Cancels the pending request (resolves `false`). Bound via `data-action`. */\n cancel(): void {\n this.#resolve(false);\n }\n\n /**\n * Intercepts a link/button click, shows the confirm dialog, and continues the\n * original action (form submit or navigation) only when confirmed. The message\n * comes from the `message` action param or the element's `data-turbo-confirm`.\n */\n request(event: Event): void {\n const element = (event.currentTarget ?? event.target) as HTMLElement | null;\n if (!element) return;\n event.preventDefault();\n\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParam = typeof params?.message === \"string\" ? params.message : null;\n const message = fromParam ?? element.getAttribute(\"data-turbo-confirm\") ?? \"\";\n\n void this.#prompt(message).then((confirmed) => {\n if (confirmed) this.#continue(element);\n });\n }\n\n /**\n * Opens the dialog for `message` and resolves once the user confirms or cancels.\n * Degrades to native `window.confirm` when no dialog target is present.\n */\n #prompt(message: string): Promise<boolean> {\n if (!this.hasDialogTarget) return Promise.resolve(window.confirm(message));\n // A second prompt while one is open cancels the first to keep a single dialog.\n this.#resolve(false);\n\n if (this.hasMessageTarget) this.messageTarget.textContent = message;\n if (this.hasConfirmTarget) this.confirmTarget.textContent = this.confirmLabelValue;\n if (this.hasCancelTarget) this.cancelTarget.textContent = this.cancelLabelValue;\n\n return new Promise<boolean>((resolve) => {\n this.#pending = resolve;\n this.dialogTarget.hidden = false;\n this.dispatch(\"open\", { detail: { message } });\n this.#trap.activate();\n });\n }\n\n /**\n * Settles the pending Promise, closes the dialog, and emits `resolve`.\n * `restoreFocus` is forwarded to the trap so teardown (disconnect) can settle a\n * pending confirmation without moving focus.\n */\n #resolve(confirmed: boolean, restoreFocus = true): void {\n const pending = this.#pending;\n if (!pending) return;\n this.#pending = null;\n\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n this.#trap.deactivate({ restoreFocus });\n this.dispatch(\"resolve\", { detail: { confirmed } });\n pending(confirmed);\n }\n\n /** Continues the intercepted action when confirmed: submit a form, else navigate. */\n #continue(element: HTMLElement): void {\n if (element instanceof HTMLAnchorElement && element.href) {\n window.location.href = element.href;\n return;\n }\n const form = element instanceof HTMLFormElement ? element : element.closest(\"form\");\n if (form) {\n form.requestSubmit(element instanceof HTMLButtonElement ? element : undefined);\n }\n }\n\n /** Swaps Turbo's confirm method for a Promise-returning bridge to this dialog. */\n #installTurboHook(): void {\n const turbo = (window as Window & { Turbo?: TurboLike }).Turbo;\n const forms = turbo?.config?.forms;\n if (!forms) return;\n this.#turboForms = forms;\n this.#previousConfirm = forms.confirm;\n forms.confirm = (message: string) => this.#prompt(message);\n }\n\n /** The button to focus on open — the least-destructive one (cancel) by default. */\n #initialFocusElement(): HTMLElement | null {\n if (this.initialFocusValue === \"confirm\" && this.hasConfirmTarget) return this.confirmTarget;\n return this.hasCancelTarget ? this.cancelTarget : null;\n }\n}\n"]}
|
|
@@ -18,6 +18,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
18
18
|
* With `once` (default) later starts are ignored (`data-count-up-done` records
|
|
19
19
|
* a finished run across Turbo cache restores).
|
|
20
20
|
*
|
|
21
|
+
* `end` dispatches `{ value }`.
|
|
22
|
+
*
|
|
21
23
|
* @remarks
|
|
22
24
|
* Behavior only — no formatting is imposed: the authored text is parsed for
|
|
23
25
|
* its integer value (separators are ignored) and restored verbatim at the end;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/controllers/count_up_controller.ts"],"names":[],"mappings":"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","file":"count_up_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/**\n * Headless **count-up**: animates a number from `from` up to the value already\n * in the DOM — typically started the moment the element scrolls into view by\n * composing with the `stimeo--intersection` primitive (the classic \"animate\n * when visible\"). Distinct from `stimeo--countdown` (time-based). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--count-up`):\n * <span data-controller=\"stimeo--intersection stimeo--count-up\"\n * data-stimeo--intersection-once-value=\"true\"\n * data-action=\"stimeo--intersection:enter->stimeo--count-up#start\">1200</span>\n *\n * The **final number stays authored in the markup** (SEO / no-JS / SR read the\n * real value); `start` animates the displayed text from `from` to it over\n * `duration` ms with an ease-out curve, then restores the exact authored text.\n * With `once` (default) later starts are ignored (`data-count-up-done` records\n * a finished run across Turbo cache restores).\n *\n * @remarks\n * Behavior only — no formatting is imposed: the authored text is parsed for\n * its integer value (separators are ignored) and restored verbatim at the end;\n * intermediate frames render plain integers. Accessibility: when the user\n * prefers reduced motion the animation is skipped entirely (the value just\n * stays final — WCAG 2.3.3). The element is not a live region, so the ticking\n * intermediate numbers are never actively announced; during the run the\n * authored value is additionally kept in `aria-label` (best-effort — generic\n * roles may ignore it) and that lingering label doubles as the\n * interrupted-run marker `connect()` restores from after a Turbo cache\n * snapshot taken mid-animation. The animation frame is canceled on\n * `disconnect()` (Turbo navigation included) and the authored text restored.\n */\nexport class CountUpController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1200 },\n from: { type: Number, default: 0 },\n once: { type: Boolean, default: true },\n };\n static actions = [\"start\"] as const;\n static events = [\"end\"] as const;\n\n declare durationValue: number;\n declare fromValue: number;\n declare onceValue: boolean;\n\n #frame: number | null = null;\n /** The authored final text, restored verbatim when the run settles. */\n #finalText = \"\";\n\n override connect(): void {\n // Turbo snapshots the page BEFORE the body swap, so a cached page can hold\n // a mid-animation frame (disconnect()'s settle runs too late for it). The\n // OWN-label marker (never a bare aria-label — that may be authored) flags\n // the interrupted run: the label it owns still holds the authored text.\n if (this.element.hasAttribute(\"data-count-up-label\")) {\n this.element.textContent =\n this.element.getAttribute(\"aria-label\") ?? this.element.textContent;\n this.#restoreLabel();\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n }\n\n override disconnect(): void {\n // A run cannot survive the element: settle instantly so the cached\n // snapshot holds the real value, never a mid-animation frame.\n if (this.#frame !== null) this.#settle();\n }\n\n /**\n * Starts the animation (typically from `stimeo--intersection:enter` via\n * `data-action`). No-ops while running, and after a finished run when `once`.\n */\n start(): void {\n if (this.#frame !== null) return;\n if (this.onceValue && this.element.hasAttribute(\"data-count-up-done\")) return;\n\n this.#finalText = this.element.textContent ?? \"\";\n const target = Number.parseInt(this.#finalText.replace(/[^0-9-]/g, \"\"), 10);\n if (Number.isNaN(target)) return;\n\n // Reduced motion: no ticking, just the final value (WCAG 2.3.3).\n if (prefersReducedMotion()) {\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n this.dispatch(\"end\", { detail: { value: target } });\n return;\n }\n\n // AT keeps the real value while the visible text ticks. An authored\n // aria-label is parked (save-restore, never clobbered) and the override is\n // marker-owned so connect()/settle() only ever touch what this set.\n const authored = this.element.getAttribute(\"aria-label\");\n if (authored !== null) {\n this.element.setAttribute(\"data-count-up-original-label\", authored);\n }\n this.element.setAttribute(\"data-count-up-label\", \"true\");\n this.element.setAttribute(\"aria-label\", this.#finalText);\n const started = performance.now();\n const step = (now: number): void => {\n const t = Math.min((now - started) / this.durationValue, 1);\n const eased = 1 - (1 - t) ** 3; // ease-out cubic\n this.element.textContent = String(\n Math.round(this.fromValue + (target - this.fromValue) * eased),\n );\n if (t < 1) {\n this.#frame = requestAnimationFrame(step);\n } else {\n this.#settle();\n this.dispatch(\"end\", { detail: { value: target } });\n }\n };\n this.#frame = requestAnimationFrame(step);\n }\n\n /** Ends the run: cancels the frame and restores the authored presentation. */\n #settle(): void {\n if (this.#frame !== null) cancelAnimationFrame(this.#frame);\n this.#frame = null;\n this.element.textContent = this.#finalText;\n this.#restoreLabel();\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n\n /** Releases the marker-owned aria-label, restoring any parked authored value. */\n #restoreLabel(): void {\n if (!this.element.hasAttribute(\"data-count-up-label\")) return;\n const original = this.element.getAttribute(\"data-count-up-original-label\");\n if (original !== null) {\n this.element.setAttribute(\"aria-label\", original);\n this.element.removeAttribute(\"data-count-up-original-label\");\n } else {\n this.element.removeAttribute(\"aria-label\");\n }\n this.element.removeAttribute(\"data-count-up-label\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/controllers/count_up_controller.ts"],"names":[],"mappings":"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","file":"count_up_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * This one-liner keeps the media query string and the environment guard\n * single-sourced across them. The preference is intentionally re-read on every\n * call — the controllers check it at each animation/scroll start (WCAG 2.2\n * **2.3.3**), so flipping the OS setting takes effect immediately without any\n * listener or cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/**\n * Headless **count-up**: animates a number from `from` up to the value already\n * in the DOM — typically started the moment the element scrolls into view by\n * composing with the `stimeo--intersection` primitive (the classic \"animate\n * when visible\"). Distinct from `stimeo--countdown` (time-based). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--count-up`):\n * <span data-controller=\"stimeo--intersection stimeo--count-up\"\n * data-stimeo--intersection-once-value=\"true\"\n * data-action=\"stimeo--intersection:enter->stimeo--count-up#start\">1200</span>\n *\n * The **final number stays authored in the markup** (SEO / no-JS / SR read the\n * real value); `start` animates the displayed text from `from` to it over\n * `duration` ms with an ease-out curve, then restores the exact authored text.\n * With `once` (default) later starts are ignored (`data-count-up-done` records\n * a finished run across Turbo cache restores).\n *\n * `end` dispatches `{ value }`.\n *\n * @remarks\n * Behavior only — no formatting is imposed: the authored text is parsed for\n * its integer value (separators are ignored) and restored verbatim at the end;\n * intermediate frames render plain integers. Accessibility: when the user\n * prefers reduced motion the animation is skipped entirely (the value just\n * stays final — WCAG 2.3.3). The element is not a live region, so the ticking\n * intermediate numbers are never actively announced; during the run the\n * authored value is additionally kept in `aria-label` (best-effort — generic\n * roles may ignore it) and that lingering label doubles as the\n * interrupted-run marker `connect()` restores from after a Turbo cache\n * snapshot taken mid-animation. The animation frame is canceled on\n * `disconnect()` (Turbo navigation included) and the authored text restored.\n */\nexport class CountUpController extends Controller<HTMLElement> {\n static override values = {\n duration: { type: Number, default: 1200 },\n from: { type: Number, default: 0 },\n once: { type: Boolean, default: true },\n };\n static actions = [\"start\"] as const;\n static events = [\"end\"] as const;\n\n declare durationValue: number;\n declare fromValue: number;\n declare onceValue: boolean;\n\n #frame: number | null = null;\n /** The authored final text, restored verbatim when the run settles. */\n #finalText = \"\";\n\n override connect(): void {\n // Turbo snapshots the page BEFORE the body swap, so a cached page can hold\n // a mid-animation frame (disconnect()'s settle runs too late for it). The\n // OWN-label marker (never a bare aria-label — that may be authored) flags\n // the interrupted run: the label it owns still holds the authored text.\n if (this.element.hasAttribute(\"data-count-up-label\")) {\n this.element.textContent =\n this.element.getAttribute(\"aria-label\") ?? this.element.textContent;\n this.#restoreLabel();\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n }\n\n override disconnect(): void {\n // A run cannot survive the element: settle instantly so the cached\n // snapshot holds the real value, never a mid-animation frame.\n if (this.#frame !== null) this.#settle();\n }\n\n /**\n * Starts the animation (typically from `stimeo--intersection:enter` via\n * `data-action`). No-ops while running, and after a finished run when `once`.\n */\n start(): void {\n if (this.#frame !== null) return;\n if (this.onceValue && this.element.hasAttribute(\"data-count-up-done\")) return;\n\n this.#finalText = this.element.textContent ?? \"\";\n const target = Number.parseInt(this.#finalText.replace(/[^0-9-]/g, \"\"), 10);\n if (Number.isNaN(target)) return;\n\n // Reduced motion: no ticking, just the final value (WCAG 2.3.3).\n if (prefersReducedMotion()) {\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n this.dispatch(\"end\", { detail: { value: target } });\n return;\n }\n\n // AT keeps the real value while the visible text ticks. An authored\n // aria-label is parked (save-restore, never clobbered) and the override is\n // marker-owned so connect()/settle() only ever touch what this set.\n const authored = this.element.getAttribute(\"aria-label\");\n if (authored !== null) {\n this.element.setAttribute(\"data-count-up-original-label\", authored);\n }\n this.element.setAttribute(\"data-count-up-label\", \"true\");\n this.element.setAttribute(\"aria-label\", this.#finalText);\n const started = performance.now();\n const step = (now: number): void => {\n const t = Math.min((now - started) / this.durationValue, 1);\n const eased = 1 - (1 - t) ** 3; // ease-out cubic\n this.element.textContent = String(\n Math.round(this.fromValue + (target - this.fromValue) * eased),\n );\n if (t < 1) {\n this.#frame = requestAnimationFrame(step);\n } else {\n this.#settle();\n this.dispatch(\"end\", { detail: { value: target } });\n }\n };\n this.#frame = requestAnimationFrame(step);\n }\n\n /** Ends the run: cancels the frame and restores the authored presentation. */\n #settle(): void {\n if (this.#frame !== null) cancelAnimationFrame(this.#frame);\n this.#frame = null;\n this.element.textContent = this.#finalText;\n this.#restoreLabel();\n this.element.setAttribute(\"data-count-up-done\", \"true\");\n }\n\n /** Releases the marker-owned aria-label, restoring any parked authored value. */\n #restoreLabel(): void {\n if (!this.element.hasAttribute(\"data-count-up-label\")) return;\n const original = this.element.getAttribute(\"data-count-up-original-label\");\n if (original !== null) {\n this.element.setAttribute(\"aria-label\", original);\n this.element.removeAttribute(\"data-count-up-original-label\");\n } else {\n this.element.removeAttribute(\"aria-label\");\n }\n this.element.removeAttribute(\"data-count-up-label\");\n }\n}\n"]}
|
|
@@ -24,6 +24,10 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
24
24
|
* slot free of live-region semantics: with `announceText` set it would say the
|
|
25
25
|
* same thing twice.
|
|
26
26
|
*
|
|
27
|
+
* `complete` dispatches `{}`; `tick` dispatches
|
|
28
|
+
* `{ remaining: number, direction: "down" | "up" }`, where `remaining` is in
|
|
29
|
+
* milliseconds.
|
|
30
|
+
*
|
|
27
31
|
* @remarks
|
|
28
32
|
* Behavior only — slot text is updated, not styled. Pause/resume shifts an
|
|
29
33
|
* internal time anchor so the displayed amount is preserved across a pause, down
|