stimeo-ui 0.1.0-beta.2 → 0.1.0-beta.3

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 CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  While the version is `0.x`, the public API (the `stimeo--*` data attributes) may
8
8
  change between releases.
9
9
 
10
+ ## [0.1.0-beta.3] - 2026-07-18
11
+
12
+ Maintenance release: one bug fix in the focus-trap primitive shared by the
13
+ modal overlay controllers.
14
+
15
+ ### Fixed
16
+
17
+ - dialog, alert-dialog, confirm, drawer, command-palette, sidebar: navigating
18
+ away while the overlay is open no longer bakes the background scroll lock
19
+ into Turbo's page cache. A restored page previously treated the locked
20
+ `<body>` as its baseline, so closing the overlay could leave the page
21
+ unscrollable; the shared focus trap now reverts its side effects on
22
+ `turbo:before-cache`.
23
+ - drawer: the usage example in the API reference now shows the backdrop
24
+ overlay `hidden` at rest and a neutral `<div>` as the panel element.
25
+
10
26
  ## [0.1.0-beta.2] - 2026-07-12
11
27
 
12
28
  The Inspector grows two new faces: an MCP server for AI coding agents and a
@@ -70,6 +86,7 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
70
86
  by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
71
87
  RubyGems.
72
88
 
89
+ [0.1.0-beta.3]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-beta.3
73
90
  [0.1.0-beta.2]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-beta.2
74
91
  [0.1.0-beta.1]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-beta.1
75
92
  [0.1.0-alpha.1]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.1.0-alpha.1
package/README.md CHANGED
@@ -31,7 +31,7 @@ owns the look entirely.
31
31
  ### Rails with importmap (recommended)
32
32
 
33
33
  ```bash
34
- bundle add stimeo-ui --version "0.1.0.pre.beta.2"
34
+ bundle add stimeo-ui --version "0.1.0.pre.beta.3"
35
35
  bin/rails generate stimeo:install
36
36
  ```
37
37
 
@@ -49,6 +49,7 @@ var FocusTrap = class {
49
49
  }
50
50
  if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
51
51
  document.addEventListener("keydown", this.#onKeydown);
52
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
52
53
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
53
54
  }
54
55
  /**
@@ -62,6 +63,7 @@ var FocusTrap = class {
62
63
  if (!this.#activeState) return;
63
64
  this.#activeState = false;
64
65
  document.removeEventListener("keydown", this.#onKeydown);
66
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
65
67
  if (this.#scrollLocked) {
66
68
  document.body.style.overflow = this.#previousBodyOverflow;
67
69
  this.#scrollLocked = false;
@@ -77,6 +79,17 @@ var FocusTrap = class {
77
79
  if (option === void 0) return fallback;
78
80
  return typeof option === "function" ? option() : option;
79
81
  }
82
+ /**
83
+ * Reverts the side effects just before Turbo caches the page snapshot, so an
84
+ * overlay left open does not bake the scroll lock into `body[style]` — a
85
+ * restored page would feed that locked value back into {@link activate} as the
86
+ * baseline, and closing would then never unlock the page. Markup state stays
87
+ * untouched (restore-open designs reopen against a clean baseline), and focus
88
+ * is left alone mid-navigation. The listener lives only while active.
89
+ */
90
+ #onBeforeCache = () => {
91
+ this.deactivate({ restoreFocus: false });
92
+ };
80
93
  /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */
81
94
  #onKeydown = (event) => {
82
95
  if (event.key === "Escape") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/command_palette_controller.ts"],"names":[],"mappings":"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","file":"command_palette_controller.js","sourcesContent":["/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / drawer).\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). {@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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 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 document.removeEventListener(\"keydown\", this.#onKeydown);\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 /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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/**\n * Headless, highly accessible Command Palette behavior.\n *\n * Markup contract (identifier: `stimeo--command-palette`):\n * <div data-controller=\"stimeo--command-palette\"\n * data-stimeo--command-palette-hotkey-value=\"mod+k\">\n * <div data-stimeo--command-palette-target=\"dialog\" role=\"dialog\"\n * aria-modal=\"true\" aria-label=\"Command palette\"\n * data-action=\"click->stimeo--command-palette#closeOnBackdrop\" hidden>\n * <input data-stimeo--command-palette-target=\"input\" role=\"combobox\"\n * aria-expanded=\"false\" aria-controls=\"cmdk-list\"\n * aria-autocomplete=\"list\" aria-label=\"Search commands\"\n * data-action=\"input->stimeo--command-palette#filter\n * keydown->stimeo--command-palette#onKeydown\" />\n * <ul id=\"cmdk-list\" data-stimeo--command-palette-target=\"list\" role=\"listbox\">\n * <li id=\"cmd-new\" role=\"option\"\n * data-stimeo--command-palette-target=\"option\"\n * data-action=\"click->stimeo--command-palette#selectByClick\">New…</li>\n * <li role=\"option\" data-stimeo--command-palette-target=\"option\"\n * data-disabled=\"true\">Section heading (shown but not selectable)</li>\n * </ul>\n * <p data-stimeo--command-palette-target=\"empty\" hidden>No commands</p>\n * </div>\n * </div>\n *\n * Implements a composite **Dialog (Modal)** + **Combobox** (Listbox options) pattern.\n * A customizable global hotkey (Command/Ctrl + K) toggles it, focus is trapped\n * within the dialog, virtual focus roves via `aria-activedescendant`, and filtering\n * is performed in memory.\n *\n * @remarks\n * Behavior only. The combobox concerns (filtering, virtual focus, selection) live\n * here; the modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by dialog / alert-dialog / drawer). `Escape` closes and\n * `Tab`/`Shift+Tab` cycle focus regardless of which element inside the dialog holds\n * focus (input, close button, …), because the trap listens at the document level.\n * Styling, transitions, and the actual command handlers remain the consumer's.\n */\nexport class CommandPaletteController extends Controller<HTMLElement> {\n static override targets = [\"dialog\", \"input\", \"list\", \"option\", \"empty\"];\n static override values = {\n hotkey: { type: String, default: \"mod+k\" },\n open: { type: Boolean, default: false },\n };\n static actions = [\n \"close\",\n \"closeOnBackdrop\",\n \"filter\",\n \"onKeydown\",\n \"open\",\n \"selectByClick\",\n \"toggle\",\n ] as const;\n static events = [\"select\"] as const;\n\n declare readonly dialogTarget: HTMLElement;\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly emptyTarget: HTMLElement;\n\n declare readonly hasDialogTarget: boolean;\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n declare readonly hasEmptyTarget: boolean;\n\n declare hotkeyValue: string;\n declare openValue: boolean;\n\n /** The index of the currently active option within the visible subset. */\n #activeIndex = -1;\n\n /**\n * Owns the modal side effects (focus trap, scroll lock, background `inert`, focus\n * restore). Escape closes; focus on open goes to the input, and is restored to\n * whatever opened the palette on close.\n */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n initialFocus: () => (this.hasInputTarget ? this.inputTarget : null),\n });\n\n /**\n * Initializes the global hotkey handler and establishes the initial open state.\n *\n * The DOM is the source of truth on reconnect (Turbo cache restore / morph): if\n * the restored snapshot already shows the dialog open, honor that rather than\n * re-deriving from the declarative `open` Value (which would slam a user-opened\n * palette shut). The `open` Value only seeds the initial state of a genuinely\n * fresh render. We normalize to a clean closed baseline first so {@link open}\n * runs its full setup — the {@link FocusTrap} is a fresh instance after a\n * reconnect and must be re-activated.\n */\n override connect(): void {\n document.addEventListener(\"keydown\", this.#onGlobalKeydown);\n const shouldOpen = this.#isOpen || this.openValue;\n this.#resetToClosedState();\n if (shouldOpen) this.open();\n }\n\n /** Tears down the global hotkey listener and reverts the modal side effects. */\n override disconnect(): void {\n document.removeEventListener(\"keydown\", this.#onGlobalKeydown);\n this.#trap.deactivate({ restoreFocus: false });\n this.#resetToClosedState();\n }\n\n /** Toggles the open state of the command palette. */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the palette, traps focus, and shifts focus to the input. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.openValue = true;\n if (this.hasInputTarget) this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#resetFilter();\n this.#trap.activate();\n }\n\n /** Closes the palette and restores focus back to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.#resetToClosedState();\n this.#trap.deactivate();\n }\n\n /** Filters option elements in-memory matching the input value. Bound to input target. */\n filter(): void {\n if (!this.hasInputTarget) return;\n const query = this.inputTarget.value.trim().toLowerCase();\n // Disabled options (e.g. group headings) may still be shown, but they do not\n // count toward the empty state and are never navigable/selectable.\n let hasSelectableMatch = false;\n\n for (const option of this.optionTargets) {\n const searchText = (option.dataset.searchValue || option.textContent || \"\")\n .trim()\n .toLowerCase();\n const matches = searchText.includes(query);\n\n if (matches) {\n option.removeAttribute(\"hidden\");\n if (!this.#isDisabled(option)) hasSelectableMatch = true;\n } else {\n option.setAttribute(\"hidden\", \"true\");\n }\n }\n\n if (this.hasEmptyTarget) {\n this.emptyTarget.hidden = hasSelectableMatch;\n }\n\n this.#setActiveIndex(hasSelectableMatch ? 0 : -1);\n }\n\n /** Selects the clicked option. Bound to option targets. */\n selectByClick(event: MouseEvent): void {\n const target = event.currentTarget as HTMLElement | null;\n if (!target) return;\n const option = target.closest(\"[role='option']\") as HTMLElement | null;\n // Ignore clicks on disabled options (e.g. group headings) so they never fire select.\n if (option && !this.#isDisabled(option)) {\n this.#confirmSelection(option);\n }\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /**\n * Combobox navigation keys (arrows / Home / End / Enter), bound to the input.\n * `Tab` (focus trap) and `Escape` (close) are owned by the {@link FocusTrap} at\n * the document level, so they work no matter which element inside the dialog has\n * focus — not only the input.\n */\n onKeydown(event: KeyboardEvent): void {\n if (!this.#isOpen) return;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.#navigate(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.#navigate(-1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#setActiveIndex(0);\n break;\n case \"End\":\n event.preventDefault();\n this.#setActiveIndex(this.#visibleOptions.length - 1);\n break;\n case \"Enter\": {\n event.preventDefault();\n const activeOption = this.#visibleOptions[this.#activeIndex];\n if (activeOption) this.#confirmSelection(activeOption);\n break;\n }\n }\n }\n\n #navigate(direction: number): void {\n const visible = this.#visibleOptions;\n if (visible.length === 0) return;\n\n let newIndex = this.#activeIndex + direction;\n if (newIndex >= visible.length) newIndex = 0;\n if (newIndex < 0) newIndex = visible.length - 1;\n\n this.#setActiveIndex(newIndex);\n }\n\n #setActiveIndex(index: number): void {\n const visible = this.#visibleOptions;\n this.#activeIndex = index;\n\n visible.forEach((option, i) => {\n if (i === index) {\n option.setAttribute(\"aria-selected\", \"true\");\n option.setAttribute(\"data-active\", \"true\");\n if (this.hasInputTarget) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", option.id || \"\");\n }\n option.scrollIntoView({ block: \"nearest\" });\n } else {\n option.setAttribute(\"aria-selected\", \"false\");\n option.removeAttribute(\"data-active\");\n }\n });\n\n if (index === -1 && this.hasInputTarget) {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n\n #confirmSelection(option: HTMLElement): void {\n const value = option.dataset.value || option.textContent || \"\";\n this.dispatch(\"select\", { detail: { value, option } });\n this.close();\n }\n\n #resetFilter(): void {\n if (this.hasInputTarget) this.inputTarget.value = \"\";\n for (const option of this.optionTargets) {\n option.removeAttribute(\"hidden\");\n }\n if (this.hasEmptyTarget) this.emptyTarget.hidden = true;\n this.#setActiveIndex(0);\n }\n\n get #visibleOptions(): HTMLElement[] {\n // Only options that are both shown and not disabled are navigable/selectable.\n return this.optionTargets.filter(\n (option) => !option.hasAttribute(\"hidden\") && !this.#isDisabled(option),\n );\n }\n\n #isDisabled(option: HTMLElement): boolean {\n return option.dataset.disabled === \"true\";\n }\n\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n\n readonly #onGlobalKeydown = (event: KeyboardEvent): void => {\n const hotkey = this.hotkeyValue.toLowerCase();\n const isMod = hotkey.includes(\"mod+\");\n const key = hotkey.split(\"+\").pop();\n\n if (!key) return;\n\n // \"mod\" accepts *either* Cmd or Ctrl so the documented \"Cmd+K / Ctrl+K\" works\n // on every platform — including Ctrl+K on macOS, not just Cmd+K. Without \"mod\",\n // the hotkey is a bare key that must be pressed with no Cmd/Ctrl held.\n const modPressed = isMod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;\n const keyMatch = event.key.toLowerCase() === key;\n\n if (modPressed && keyMatch) {\n event.preventDefault();\n this.toggle();\n }\n };\n\n /** Resets transient open state so reconnect starts from a predictable closed snapshot. */\n #resetToClosedState(): void {\n this.#activeIndex = -1;\n this.openValue = false;\n\n if (this.hasDialogTarget) {\n this.dialogTarget.hidden = true;\n }\n\n if (this.hasInputTarget) {\n this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/command_palette_controller.ts"],"names":[],"mappings":";;;;;AA+BO,IAAM,SAAA,GACX,2IAAA;AA6CK,IAAM,YAAN,MAAgB;AAAA;AAAA,EAErB,kBAAA,GAAyC,IAAA;AAAA;AAAA,EAEzC,qBAAA,GAAwB,EAAA;AAAA;AAAA,EAExB,aAAA,GAAgB,KAAA;AAAA;AAAA,EAEhB,mBAAkC,EAAC;AAAA;AAAA,EAEnC,YAAA,GAAe,KAAA;AAAA;AAAA,EAGN,aAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,WAAA,CAAY,YAAA,EAAiC,OAAA,GAA4B,EAAC,EAAG;AAC3E,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,YAAA,EAAc;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAIpB,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAA,CAAK,qBACH,MAAA,YAAkB,WAAA,IAAe,MAAA,KAAW,QAAA,CAAS,OAAO,MAAA,GAAS,IAAA;AACvE,IAAA,IAAI,KAAK,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,IAAI,CAAA,EAAG;AAC9C,MAAA,IAAA,CAAK,qBAAA,GAAwB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AACjD,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,MAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,IACvB;AACA,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA,OAAQ,kBAAA,EAAmB;AACrE,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AACnE,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,WAAW,IAAI,CAAA,OAAQ,aAAA,EAAc;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAW,EAAE,YAAA,GAAe,IAAA,EAAK,GAAgC,EAAC,EAAS;AACzE,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AACpB,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACvD,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AACtE,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,IAAA,CAAK,qBAAA;AACpC,MAAA,IAAA,CAAK,aAAA,GAAgB,KAAA;AAAA,IACvB;AACA,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,MAAM,SAAS,IAAA,CAAK,kBAAA,IAAsB,IAAA,CAAK,QAAA,CAAS,iBAAgB,IAAK,IAAA;AAC7E,MAAA,MAAA,EAAQ,KAAA,EAAM;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,KAAA,CAAM,QAA+C,QAAA,EAA4B;AAC/E,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,QAAA;AACjC,IAAA,OAAO,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,EAAO,GAAI,MAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUS,iBAAiB,MAAY;AACpC,IAAA,IAAA,CAAK,UAAA,CAAW,EAAE,YAAA,EAAc,KAAA,EAAO,CAAA;AAAA,EACzC,CAAA;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAI,KAAA,CAAM,QAAQ,QAAA,EAAU;AAC1B,MAAA,IAAI,IAAA,CAAK,SAAS,QAAA,EAAU;AAC1B,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AAAA,MACzB;AACA,MAAA;AAAA,IACF;AACA,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EAC9C,CAAA;AAAA;AAAA,EAGA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,MAAM,IAAA,GAAO,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAC3C,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AAGxB,IAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,IAAS,CAAC,KAAK,aAAA,EAAc,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG;AACvE,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,CAAM,QAAA,IAAY,MAAA,KAAW,KAAA,EAAO;AACtC,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,EAAM,KAAA,EAAM;AAAA,IACd,CAAA,MAAA,IAAW,CAAC,KAAA,CAAM,QAAA,IAAY,WAAW,IAAA,EAAM;AAC7C,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,kBAAA,GAA2B;AACzB,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,IAAA,CAAK,mBAAmB,EAAC;AACzB,IAAA,KAAA,MAAW,WAAW,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,EAAG;AACxD,MAAA,IAAI,EAAE,mBAAmB,WAAA,CAAA,EAAc;AACvC,MAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,SAAS,CAAA,IAAK,QAAQ,KAAA,EAAO;AAClD,MAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAChB,MAAA,IAAA,CAAK,gBAAA,CAAiB,KAAK,OAAO,CAAA;AAAA,IACpC;AAAA,EACF;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,gBAAA,EAAkB;AAC3C,MAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,IAClB;AACA,IAAA,IAAA,CAAK,mBAAmB,EAAC;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,YAAA,IAAe;AAC/C,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,CAAC,CAAA,EAAG;AAChB,MAAA,SAAA,CAAU,CAAC,EAAE,KAAA,EAAM;AACnB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,SAAA,CAAU,QAAA,GAAW,EAAA;AACrB,IAAA,SAAA,CAAU,KAAA,EAAM;AAAA,EAClB;AAAA;AAAA,EAGA,kBAAA,GAAoC;AAClC,IAAA,OAAO,KAAA,CAAM,KAAK,IAAA,CAAK,aAAA,GAAgB,gBAAA,CAA8B,SAAS,CAAC,CAAA,CAAE,MAAA;AAAA,MAC/E,CAAC,EAAA,KAAO,CAAC,EAAA,CAAG;AAAA,KACd;AAAA,EACF;AACF,CAAA;;;AC5NO,IAAM,wBAAA,GAAN,cAAuC,UAAA,CAAwB;AAAA,EACpE,OAAgB,OAAA,GAAU,CAAC,UAAU,OAAA,EAAS,MAAA,EAAQ,UAAU,OAAO,CAAA;AAAA,EACvE,OAAgB,MAAA,GAAS;AAAA,IACvB,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,OAAA,EAAQ;AAAA,IACzC,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA;AAAM,GACxC;AAAA,EACA,OAAO,OAAA,GAAU;AAAA,IACf,OAAA;AAAA,IACA,iBAAA;AAAA,IACA,QAAA;AAAA,IACA,WAAA;AAAA,IACA,MAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,GACF;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAiBzB,YAAA,GAAe,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAON,KAAA,GAAQ,IAAI,SAAA,CAAU,MAAM,KAAK,YAAA,EAAc;AAAA,IACtD,QAAA,EAAU,MAAM,IAAA,CAAK,KAAA,EAAM;AAAA,IAC3B,YAAA,EAAc,MAAO,IAAA,CAAK,cAAA,GAAiB,KAAK,WAAA,GAAc;AAAA,GAC/D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,OAAA,GAAgB;AACvB,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,gBAAgB,CAAA;AAC1D,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,SAAA;AACxC,IAAA,IAAA,CAAK,mBAAA,EAAoB;AACzB,IAAA,IAAI,UAAA,OAAiB,IAAA,EAAK;AAAA,EAC5B;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,gBAAgB,CAAA;AAC7D,IAAA,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAE,YAAA,EAAc,OAAO,CAAA;AAC7C,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,KAAK,OAAA,EAAS;AAChB,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ;AAAA,EACF;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,OAAA,EAAS;AAC3C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAI,KAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,iBAAiB,MAAM,CAAA;AAC9E,IAAA,IAAA,CAAK,YAAA,EAAa;AAClB,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,CAAC,KAAK,OAAA,EAAS;AAC5C,IAAA,IAAA,CAAK,mBAAA,EAAoB;AACzB,IAAA,IAAA,CAAK,MAAM,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAC1B,IAAA,MAAM,QAAQ,IAAA,CAAK,WAAA,CAAY,KAAA,CAAM,IAAA,GAAO,WAAA,EAAY;AAGxD,IAAA,IAAI,kBAAA,GAAqB,KAAA;AAEzB,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,aAAA,EAAe;AACvC,MAAA,MAAM,UAAA,GAAA,CAAc,OAAO,OAAA,CAAQ,WAAA,IAAe,OAAO,WAAA,IAAe,EAAA,EACrE,IAAA,EAAK,CACL,WAAA,EAAY;AACf,MAAA,MAAM,OAAA,GAAU,UAAA,CAAW,QAAA,CAAS,KAAK,CAAA;AAEzC,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,MAAA,CAAO,gBAAgB,QAAQ,CAAA;AAC/B,QAAA,IAAI,CAAC,IAAA,CAAK,WAAA,CAAY,MAAM,GAAG,kBAAA,GAAqB,IAAA;AAAA,MACtD,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,YAAA,CAAa,UAAU,MAAM,CAAA;AAAA,MACtC;AAAA,IACF;AAEA,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,IAAA,CAAK,YAAY,MAAA,GAAS,kBAAA;AAAA,IAC5B;AAEA,IAAA,IAAA,CAAK,eAAA,CAAgB,kBAAA,GAAqB,CAAA,GAAI,EAAE,CAAA;AAAA,EAClD;AAAA;AAAA,EAGA,cAAc,KAAA,EAAyB;AACrC,IAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAA;AAE/C,IAAA,IAAI,MAAA,IAAU,CAAC,IAAA,CAAK,WAAA,CAAY,MAAM,CAAA,EAAG;AACvC,MAAA,IAAA,CAAK,kBAAkB,MAAM,CAAA;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAGA,gBAAgB,KAAA,EAAyB;AACvC,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,YAAA,OAAmB,KAAA,EAAM;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,KAAA,EAA4B;AACpC,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AAEnB,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,WAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,UAAU,CAAC,CAAA;AAChB,QAAA;AAAA,MACF,KAAK,SAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,UAAU,EAAE,CAAA;AACjB,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,gBAAgB,CAAC,CAAA;AACtB,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,eAAA,CAAgB,IAAA,CAAK,eAAA,CAAgB,MAAA,GAAS,CAAC,CAAA;AACpD,QAAA;AAAA,MACF,KAAK,OAAA,EAAS;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,MAAM,YAAA,GAAe,IAAA,CAAK,eAAA,CAAgB,IAAA,CAAK,YAAY,CAAA;AAC3D,QAAA,IAAI,YAAA,EAAc,IAAA,CAAK,iBAAA,CAAkB,YAAY,CAAA;AACrD,QAAA;AAAA,MACF;AAAA;AACF,EACF;AAAA,EAEA,UAAU,SAAA,EAAyB;AACjC,IAAA,MAAM,UAAU,IAAA,CAAK,eAAA;AACrB,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAE1B,IAAA,IAAI,QAAA,GAAW,KAAK,YAAA,GAAe,SAAA;AACnC,IAAA,IAAI,QAAA,IAAY,OAAA,CAAQ,MAAA,EAAQ,QAAA,GAAW,CAAA;AAC3C,IAAA,IAAI,QAAA,GAAW,CAAA,EAAG,QAAA,GAAW,OAAA,CAAQ,MAAA,GAAS,CAAA;AAE9C,IAAA,IAAA,CAAK,gBAAgB,QAAQ,CAAA;AAAA,EAC/B;AAAA,EAEA,gBAAgB,KAAA,EAAqB;AACnC,IAAA,MAAM,UAAU,IAAA,CAAK,eAAA;AACrB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AAEpB,IAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,MAAA,EAAQ,CAAA,KAAM;AAC7B,MAAA,IAAI,MAAM,KAAA,EAAO;AACf,QAAA,MAAA,CAAO,YAAA,CAAa,iBAAiB,MAAM,CAAA;AAC3C,QAAA,MAAA,CAAO,YAAA,CAAa,eAAe,MAAM,CAAA;AACzC,QAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,UAAA,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,uBAAA,EAAyB,MAAA,CAAO,MAAM,EAAE,CAAA;AAAA,QACxE;AACA,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,KAAA,EAAO,SAAA,EAAW,CAAA;AAAA,MAC5C,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAC5C,QAAA,MAAA,CAAO,gBAAgB,aAAa,CAAA;AAAA,MACtC;AAAA,IACF,CAAC,CAAA;AAED,IAAA,IAAI,KAAA,KAAU,EAAA,IAAM,IAAA,CAAK,cAAA,EAAgB;AACvC,MAAA,IAAA,CAAK,WAAA,CAAY,gBAAgB,uBAAuB,CAAA;AAAA,IAC1D;AAAA,EACF;AAAA,EAEA,kBAAkB,MAAA,EAA2B;AAC3C,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,KAAA,IAAS,OAAO,WAAA,IAAe,EAAA;AAC5D,IAAA,IAAA,CAAK,QAAA,CAAS,UAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,MAAA,IAAU,CAAA;AACrD,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA,EAEA,YAAA,GAAqB;AACnB,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,KAAA,GAAQ,EAAA;AAClD,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,aAAA,EAAe;AACvC,MAAA,MAAA,CAAO,gBAAgB,QAAQ,CAAA;AAAA,IACjC;AACA,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,IAAA;AACnD,IAAA,IAAA,CAAK,gBAAgB,CAAC,CAAA;AAAA,EACxB;AAAA,EAEA,IAAI,eAAA,GAAiC;AAEnC,IAAA,OAAO,KAAK,aAAA,CAAc,MAAA;AAAA,MACxB,CAAC,MAAA,KAAW,CAAC,MAAA,CAAO,YAAA,CAAa,QAAQ,CAAA,IAAK,CAAC,IAAA,CAAK,WAAA,CAAY,MAAM;AAAA,KACxE;AAAA,EACF;AAAA,EAEA,YAAY,MAAA,EAA8B;AACxC,IAAA,OAAO,MAAA,CAAO,QAAQ,QAAA,KAAa,MAAA;AAAA,EACrC;AAAA,EAEA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,CAAC,IAAA,CAAK,YAAA,CAAa,MAAA;AAAA,EACpD;AAAA,EAES,gBAAA,GAAmB,CAAC,KAAA,KAA+B;AAC1D,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,WAAA,CAAY,WAAA,EAAY;AAC5C,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA;AACpC,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,KAAA,CAAM,GAAG,EAAE,GAAA,EAAI;AAElC,IAAA,IAAI,CAAC,GAAA,EAAK;AAKV,IAAA,MAAM,UAAA,GAAa,KAAA,GAAQ,KAAA,CAAM,OAAA,IAAW,KAAA,CAAM,UAAU,CAAC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,OAAA;AACrF,IAAA,MAAM,QAAA,GAAW,KAAA,CAAM,GAAA,CAAI,WAAA,EAAY,KAAM,GAAA;AAE7C,IAAA,IAAI,cAAc,QAAA,EAAU;AAC1B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,CAAK,MAAA,EAAO;AAAA,IACd;AAAA,EACF,CAAA;AAAA;AAAA,EAGA,mBAAA,GAA4B;AAC1B,IAAA,IAAA,CAAK,YAAA,GAAe,EAAA;AACpB,IAAA,IAAA,CAAK,SAAA,GAAY,KAAA;AAEjB,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,IAAA,CAAK,aAAa,MAAA,GAAS,IAAA;AAAA,IAC7B;AAEA,IAAA,IAAI,KAAK,cAAA,EAAgB;AACvB,MAAA,IAAA,CAAK,WAAA,CAAY,YAAA,CAAa,eAAA,EAAiB,OAAO,CAAA;AACtD,MAAA,IAAA,CAAK,WAAA,CAAY,gBAAgB,uBAAuB,CAAA;AAAA,IAC1D;AAAA,EACF;AACF","file":"command_palette_controller.js","sourcesContent":["/**\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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 document.removeEventListener(\"keydown\", this.#onKeydown);\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 #onBeforeCache = (): void => {\n this.deactivate({ restoreFocus: false });\n };\n\n /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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/**\n * Headless, highly accessible Command Palette behavior.\n *\n * Markup contract (identifier: `stimeo--command-palette`):\n * <div data-controller=\"stimeo--command-palette\"\n * data-stimeo--command-palette-hotkey-value=\"mod+k\">\n * <div data-stimeo--command-palette-target=\"dialog\" role=\"dialog\"\n * aria-modal=\"true\" aria-label=\"Command palette\"\n * data-action=\"click->stimeo--command-palette#closeOnBackdrop\" hidden>\n * <input data-stimeo--command-palette-target=\"input\" role=\"combobox\"\n * aria-expanded=\"false\" aria-controls=\"cmdk-list\"\n * aria-autocomplete=\"list\" aria-label=\"Search commands\"\n * data-action=\"input->stimeo--command-palette#filter\n * keydown->stimeo--command-palette#onKeydown\" />\n * <ul id=\"cmdk-list\" data-stimeo--command-palette-target=\"list\" role=\"listbox\">\n * <li id=\"cmd-new\" role=\"option\"\n * data-stimeo--command-palette-target=\"option\"\n * data-action=\"click->stimeo--command-palette#selectByClick\">New…</li>\n * <li role=\"option\" data-stimeo--command-palette-target=\"option\"\n * data-disabled=\"true\">Section heading (shown but not selectable)</li>\n * </ul>\n * <p data-stimeo--command-palette-target=\"empty\" hidden>No commands</p>\n * </div>\n * </div>\n *\n * Implements a composite **Dialog (Modal)** + **Combobox** (Listbox options) pattern.\n * A customizable global hotkey (Command/Ctrl + K) toggles it, focus is trapped\n * within the dialog, virtual focus roves via `aria-activedescendant`, and filtering\n * is performed in memory.\n *\n * @remarks\n * Behavior only. The combobox concerns (filtering, virtual focus, selection) live\n * here; the modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by dialog / alert-dialog / drawer). `Escape` closes and\n * `Tab`/`Shift+Tab` cycle focus regardless of which element inside the dialog holds\n * focus (input, close button, …), because the trap listens at the document level.\n * Styling, transitions, and the actual command handlers remain the consumer's.\n */\nexport class CommandPaletteController extends Controller<HTMLElement> {\n static override targets = [\"dialog\", \"input\", \"list\", \"option\", \"empty\"];\n static override values = {\n hotkey: { type: String, default: \"mod+k\" },\n open: { type: Boolean, default: false },\n };\n static actions = [\n \"close\",\n \"closeOnBackdrop\",\n \"filter\",\n \"onKeydown\",\n \"open\",\n \"selectByClick\",\n \"toggle\",\n ] as const;\n static events = [\"select\"] as const;\n\n declare readonly dialogTarget: HTMLElement;\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly emptyTarget: HTMLElement;\n\n declare readonly hasDialogTarget: boolean;\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n declare readonly hasEmptyTarget: boolean;\n\n declare hotkeyValue: string;\n declare openValue: boolean;\n\n /** The index of the currently active option within the visible subset. */\n #activeIndex = -1;\n\n /**\n * Owns the modal side effects (focus trap, scroll lock, background `inert`, focus\n * restore). Escape closes; focus on open goes to the input, and is restored to\n * whatever opened the palette on close.\n */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n initialFocus: () => (this.hasInputTarget ? this.inputTarget : null),\n });\n\n /**\n * Initializes the global hotkey handler and establishes the initial open state.\n *\n * The DOM is the source of truth on reconnect (Turbo cache restore / morph): if\n * the restored snapshot already shows the dialog open, honor that rather than\n * re-deriving from the declarative `open` Value (which would slam a user-opened\n * palette shut). The `open` Value only seeds the initial state of a genuinely\n * fresh render. We normalize to a clean closed baseline first so {@link open}\n * runs its full setup — the {@link FocusTrap} is a fresh instance after a\n * reconnect and must be re-activated.\n */\n override connect(): void {\n document.addEventListener(\"keydown\", this.#onGlobalKeydown);\n const shouldOpen = this.#isOpen || this.openValue;\n this.#resetToClosedState();\n if (shouldOpen) this.open();\n }\n\n /** Tears down the global hotkey listener and reverts the modal side effects. */\n override disconnect(): void {\n document.removeEventListener(\"keydown\", this.#onGlobalKeydown);\n this.#trap.deactivate({ restoreFocus: false });\n this.#resetToClosedState();\n }\n\n /** Toggles the open state of the command palette. */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the palette, traps focus, and shifts focus to the input. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.openValue = true;\n if (this.hasInputTarget) this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#resetFilter();\n this.#trap.activate();\n }\n\n /** Closes the palette and restores focus back to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.#resetToClosedState();\n this.#trap.deactivate();\n }\n\n /** Filters option elements in-memory matching the input value. Bound to input target. */\n filter(): void {\n if (!this.hasInputTarget) return;\n const query = this.inputTarget.value.trim().toLowerCase();\n // Disabled options (e.g. group headings) may still be shown, but they do not\n // count toward the empty state and are never navigable/selectable.\n let hasSelectableMatch = false;\n\n for (const option of this.optionTargets) {\n const searchText = (option.dataset.searchValue || option.textContent || \"\")\n .trim()\n .toLowerCase();\n const matches = searchText.includes(query);\n\n if (matches) {\n option.removeAttribute(\"hidden\");\n if (!this.#isDisabled(option)) hasSelectableMatch = true;\n } else {\n option.setAttribute(\"hidden\", \"true\");\n }\n }\n\n if (this.hasEmptyTarget) {\n this.emptyTarget.hidden = hasSelectableMatch;\n }\n\n this.#setActiveIndex(hasSelectableMatch ? 0 : -1);\n }\n\n /** Selects the clicked option. Bound to option targets. */\n selectByClick(event: MouseEvent): void {\n const target = event.currentTarget as HTMLElement | null;\n if (!target) return;\n const option = target.closest(\"[role='option']\") as HTMLElement | null;\n // Ignore clicks on disabled options (e.g. group headings) so they never fire select.\n if (option && !this.#isDisabled(option)) {\n this.#confirmSelection(option);\n }\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /**\n * Combobox navigation keys (arrows / Home / End / Enter), bound to the input.\n * `Tab` (focus trap) and `Escape` (close) are owned by the {@link FocusTrap} at\n * the document level, so they work no matter which element inside the dialog has\n * focus — not only the input.\n */\n onKeydown(event: KeyboardEvent): void {\n if (!this.#isOpen) return;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.#navigate(1);\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.#navigate(-1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#setActiveIndex(0);\n break;\n case \"End\":\n event.preventDefault();\n this.#setActiveIndex(this.#visibleOptions.length - 1);\n break;\n case \"Enter\": {\n event.preventDefault();\n const activeOption = this.#visibleOptions[this.#activeIndex];\n if (activeOption) this.#confirmSelection(activeOption);\n break;\n }\n }\n }\n\n #navigate(direction: number): void {\n const visible = this.#visibleOptions;\n if (visible.length === 0) return;\n\n let newIndex = this.#activeIndex + direction;\n if (newIndex >= visible.length) newIndex = 0;\n if (newIndex < 0) newIndex = visible.length - 1;\n\n this.#setActiveIndex(newIndex);\n }\n\n #setActiveIndex(index: number): void {\n const visible = this.#visibleOptions;\n this.#activeIndex = index;\n\n visible.forEach((option, i) => {\n if (i === index) {\n option.setAttribute(\"aria-selected\", \"true\");\n option.setAttribute(\"data-active\", \"true\");\n if (this.hasInputTarget) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", option.id || \"\");\n }\n option.scrollIntoView({ block: \"nearest\" });\n } else {\n option.setAttribute(\"aria-selected\", \"false\");\n option.removeAttribute(\"data-active\");\n }\n });\n\n if (index === -1 && this.hasInputTarget) {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n\n #confirmSelection(option: HTMLElement): void {\n const value = option.dataset.value || option.textContent || \"\";\n this.dispatch(\"select\", { detail: { value, option } });\n this.close();\n }\n\n #resetFilter(): void {\n if (this.hasInputTarget) this.inputTarget.value = \"\";\n for (const option of this.optionTargets) {\n option.removeAttribute(\"hidden\");\n }\n if (this.hasEmptyTarget) this.emptyTarget.hidden = true;\n this.#setActiveIndex(0);\n }\n\n get #visibleOptions(): HTMLElement[] {\n // Only options that are both shown and not disabled are navigable/selectable.\n return this.optionTargets.filter(\n (option) => !option.hasAttribute(\"hidden\") && !this.#isDisabled(option),\n );\n }\n\n #isDisabled(option: HTMLElement): boolean {\n return option.dataset.disabled === \"true\";\n }\n\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n\n readonly #onGlobalKeydown = (event: KeyboardEvent): void => {\n const hotkey = this.hotkeyValue.toLowerCase();\n const isMod = hotkey.includes(\"mod+\");\n const key = hotkey.split(\"+\").pop();\n\n if (!key) return;\n\n // \"mod\" accepts *either* Cmd or Ctrl so the documented \"Cmd+K / Ctrl+K\" works\n // on every platform — including Ctrl+K on macOS, not just Cmd+K. Without \"mod\",\n // the hotkey is a bare key that must be pressed with no Cmd/Ctrl held.\n const modPressed = isMod ? event.metaKey || event.ctrlKey : !event.metaKey && !event.ctrlKey;\n const keyMatch = event.key.toLowerCase() === key;\n\n if (modPressed && keyMatch) {\n event.preventDefault();\n this.toggle();\n }\n };\n\n /** Resets transient open state so reconnect starts from a predictable closed snapshot. */\n #resetToClosedState(): void {\n this.#activeIndex = -1;\n this.openValue = false;\n\n if (this.hasDialogTarget) {\n this.dialogTarget.hidden = true;\n }\n\n if (this.hasInputTarget) {\n this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n}\n"]}
@@ -49,6 +49,7 @@ var FocusTrap = class {
49
49
  }
50
50
  if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
51
51
  document.addEventListener("keydown", this.#onKeydown);
52
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
52
53
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
53
54
  }
54
55
  /**
@@ -62,6 +63,7 @@ var FocusTrap = class {
62
63
  if (!this.#activeState) return;
63
64
  this.#activeState = false;
64
65
  document.removeEventListener("keydown", this.#onKeydown);
66
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
65
67
  if (this.#scrollLocked) {
66
68
  document.body.style.overflow = this.#previousBodyOverflow;
67
69
  this.#scrollLocked = false;
@@ -77,6 +79,17 @@ var FocusTrap = class {
77
79
  if (option === void 0) return fallback;
78
80
  return typeof option === "function" ? option() : option;
79
81
  }
82
+ /**
83
+ * Reverts the side effects just before Turbo caches the page snapshot, so an
84
+ * overlay left open does not bake the scroll lock into `body[style]` — a
85
+ * restored page would feed that locked value back into {@link activate} as the
86
+ * baseline, and closing would then never unlock the page. Markup state stays
87
+ * untouched (restore-open designs reopen against a clean baseline), and focus
88
+ * is left alone mid-navigation. The listener lives only while active.
89
+ */
90
+ #onBeforeCache = () => {
91
+ this.deactivate({ restoreFocus: false });
92
+ };
80
93
  /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */
81
94
  #onKeydown = (event) => {
82
95
  if (event.key === "Escape") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/confirm_controller.ts"],"names":[],"mappings":"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","file":"confirm_controller.js","sourcesContent":["/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / drawer).\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). {@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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 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 document.removeEventListener(\"keydown\", this.#onKeydown);\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 /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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/focus_trap.ts","../../src/controllers/confirm_controller.ts"],"names":[],"mappings":"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","file":"confirm_controller.js","sourcesContent":["/**\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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 document.removeEventListener(\"keydown\", this.#onKeydown);\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 #onBeforeCache = (): void => {\n this.deactivate({ restoreFocus: false });\n };\n\n /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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"]}
@@ -49,6 +49,7 @@ var FocusTrap = class {
49
49
  }
50
50
  if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
51
51
  document.addEventListener("keydown", this.#onKeydown);
52
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
52
53
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
53
54
  }
54
55
  /**
@@ -62,6 +63,7 @@ var FocusTrap = class {
62
63
  if (!this.#activeState) return;
63
64
  this.#activeState = false;
64
65
  document.removeEventListener("keydown", this.#onKeydown);
66
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
65
67
  if (this.#scrollLocked) {
66
68
  document.body.style.overflow = this.#previousBodyOverflow;
67
69
  this.#scrollLocked = false;
@@ -77,6 +79,17 @@ var FocusTrap = class {
77
79
  if (option === void 0) return fallback;
78
80
  return typeof option === "function" ? option() : option;
79
81
  }
82
+ /**
83
+ * Reverts the side effects just before Turbo caches the page snapshot, so an
84
+ * overlay left open does not bake the scroll lock into `body[style]` — a
85
+ * restored page would feed that locked value back into {@link activate} as the
86
+ * baseline, and closing would then never unlock the page. Markup state stays
87
+ * untouched (restore-open designs reopen against a clean baseline), and focus
88
+ * is left alone mid-navigation. The listener lives only while active.
89
+ */
90
+ #onBeforeCache = () => {
91
+ this.deactivate({ restoreFocus: false });
92
+ };
80
93
  /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */
81
94
  #onKeydown = (event) => {
82
95
  if (event.key === "Escape") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":"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","file":"dialog_controller.js","sourcesContent":["/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / drawer).\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). {@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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 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 document.removeEventListener(\"keydown\", this.#onKeydown);\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 /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":"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","file":"dialog_controller.js","sourcesContent":["/**\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 closes itself). The trap calls\n * `preventDefault()` before invoking it.\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\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 document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 document.removeEventListener(\"keydown\", this.#onKeydown);\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\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 #onBeforeCache = (): void => {\n this.deactivate({ restoreFocus: false });\n };\n\n /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Escape\") {\n if (this.#options.onEscape) {\n event.preventDefault();\n this.#options.onEscape();\n }\n return;\n }\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/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
@@ -49,6 +49,7 @@ var FocusTrap = class {
49
49
  }
50
50
  if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
51
51
  document.addEventListener("keydown", this.#onKeydown);
52
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
52
53
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
53
54
  }
54
55
  /**
@@ -62,6 +63,7 @@ var FocusTrap = class {
62
63
  if (!this.#activeState) return;
63
64
  this.#activeState = false;
64
65
  document.removeEventListener("keydown", this.#onKeydown);
66
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
65
67
  if (this.#scrollLocked) {
66
68
  document.body.style.overflow = this.#previousBodyOverflow;
67
69
  this.#scrollLocked = false;
@@ -77,6 +79,17 @@ var FocusTrap = class {
77
79
  if (option === void 0) return fallback;
78
80
  return typeof option === "function" ? option() : option;
79
81
  }
82
+ /**
83
+ * Reverts the side effects just before Turbo caches the page snapshot, so an
84
+ * overlay left open does not bake the scroll lock into `body[style]` — a
85
+ * restored page would feed that locked value back into {@link activate} as the
86
+ * baseline, and closing would then never unlock the page. Markup state stays
87
+ * untouched (restore-open designs reopen against a clean baseline), and focus
88
+ * is left alone mid-navigation. The listener lives only while active.
89
+ */
90
+ #onBeforeCache = () => {
91
+ this.deactivate({ restoreFocus: false });
92
+ };
80
93
  /** Handles `Escape` (delegated) and `Tab` (focus trap) while active. */
81
94
  #onKeydown = (event) => {
82
95
  if (event.key === "Escape") {