@adobe/spectrum-wc-core 2.0.0-beta.3 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/components/action-group/ActionGroup.base.js +1 -1
  3. package/dist/components/action-group/ActionGroup.base.js.map +1 -1
  4. package/dist/components/asset/Asset.base.d.ts +89 -10
  5. package/dist/components/asset/Asset.base.js +117 -18
  6. package/dist/components/asset/Asset.base.js.map +1 -1
  7. package/dist/components/asset/Asset.types.d.ts +20 -2
  8. package/dist/components/asset/Asset.types.js +10 -2
  9. package/dist/components/asset/Asset.types.js.map +1 -1
  10. package/dist/components/asset/index.js +3 -3
  11. package/dist/components/dropzone/Dropzone.base.d.ts +1 -1
  12. package/dist/components/dropzone/Dropzone.base.js.map +1 -1
  13. package/dist/components/popover/Popover.base.d.ts +1 -21
  14. package/dist/components/popover/Popover.base.js +35 -59
  15. package/dist/components/popover/Popover.base.js.map +1 -1
  16. package/dist/components/tabs/TabPanel.base.d.ts +1 -1
  17. package/dist/components/tabs/TabPanel.base.js.map +1 -1
  18. package/dist/controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.d.ts +1 -1
  19. package/dist/controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.js.map +1 -1
  20. package/dist/controllers/index.d.ts +2 -1
  21. package/dist/controllers/index.js +2 -1
  22. package/dist/controllers/language-resolution.d.ts +1 -1
  23. package/dist/controllers/language-resolution.js.map +1 -1
  24. package/dist/controllers/trigger-press-controller/index.d.ts +12 -0
  25. package/dist/controllers/trigger-press-controller/index.js +2 -0
  26. package/dist/controllers/trigger-press-controller/src/trigger-press-controller.d.ts +149 -0
  27. package/dist/controllers/trigger-press-controller/src/trigger-press-controller.js +43 -0
  28. package/dist/controllers/trigger-press-controller/src/trigger-press-controller.js.map +1 -0
  29. package/dist/directives/index.d.ts +1 -1
  30. package/dist/element/define-element.js +1 -1
  31. package/dist/element/define-element.js.map +1 -1
  32. package/dist/element/spectrum-element.js +1 -1
  33. package/dist/element/spectrum-element.js.map +1 -1
  34. package/dist/element/version.d.ts +3 -3
  35. package/dist/element/version.js +1 -1
  36. package/dist/element/version.js.map +1 -1
  37. package/dist/utils/resolve-trigger.js.map +1 -1
  38. package/package.json +151 -9
  39. package/CHANGELOG.md +0 -191
@@ -1 +1 @@
1
- {"version":3,"file":"focusgroup-navigation-controller.js","names":[],"sources":["../../../../controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.ts"],"sourcesContent":["/**\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport type { ReactiveController, ReactiveElement } from 'lit';\n\n// ─────────────────────────\n// TYPES\n// ─────────────────────────\n\n/**\n * Spatial mode for arrow-key movement. Aligns with logical axes (inline/block) and a\n * 2D layout mode derived from element geometry.\n *\n * - **horizontal**: Arrow keys on the inline axis move focus (respects `dir`).\n * - **vertical**: Arrow keys on the block axis move focus.\n * - **both**: **ArrowLeft** / **ArrowRight** move along `getItems()` order like **horizontal**\n * (respects `dir`); **ArrowUp** / **ArrowDown** move backward / forward in the same order.\n * - **grid**: Arrow keys move in rows and columns using bounding-rect layout; Ctrl+Home / Ctrl+End\n * jump to the first cell of the first row or the last cell of the last row.\n */\nexport type FocusgroupDirection = 'horizontal' | 'vertical' | 'both' | 'grid';\n\n/**\n * Options for {@link FocusgroupNavigationController}.\n */\nexport type FocusgroupNavigationOptions = {\n /**\n * Returns the current set of items that participate in roving tabindex and\n * directional navigation. Callers typically close over the host (for example\n * querying slotted or shadow DOM children).\n */\n getItems: () => HTMLElement[];\n\n /**\n * Determines which arrow keys move focus and how grid navigation is computed.\n * Use **`both`** when the same linear order should respond to horizontal and vertical arrow keys.\n */\n direction: FocusgroupDirection;\n\n /**\n * When true, arrow keys wrap from the last item to the first (and reverse).\n * Defaults to false.\n */\n wrap?: boolean;\n\n /**\n * When true, restoring focus into the composite (for example with Tab) targets\n * the item that was last focused, if it is still a member of the group.\n * Similar to the default memory behavior described for `focusgroup` in Open UI.\n * Defaults to true.\n */\n memory?: boolean;\n\n /**\n * When true, both natively `disabled` and `aria-disabled=\"true\"` items are\n * skipped for arrow navigation and are not chosen as the roving tab stop.\n * When false (default), disabled items remain in sequence — useful for\n * patterns such as menus where disabled items may still be focusable per\n * APG guidance.\n *\n * **Note:** Regardless of this flag, natively `disabled` elements are never\n * chosen as the roving tab stop (`tabindex=\"0\"`) because they cannot receive\n * browser focus; see {@link applyRovingTabindex}. A future revision may\n * decouple native `disabled` and `aria-disabled` into separate options if\n * component migrations surface the need.\n *\n * Defaults to false.\n */\n skipDisabled?: boolean;\n\n /**\n * Invoked after the active item changes and `tabindex` values are synchronized.\n * The argument is the new active element, or null when the group has no eligible items.\n */\n onActiveItemChange?: (active: HTMLElement | null) => void;\n\n /**\n * When set to a **non-zero** integer, **Page Up** / **Page Down** move focus by that many\n * positions in `getItems()` order for **`horizontal`**, **`vertical`**, and **`both`** modes\n * (respects **`wrap`** the same way as single-step arrows).\n * For **`grid`**, page keys move by that many **rows** (column index is clamped to each row’s\n * length). Omitted, `0`, `NaN`, and non-finite values disable page keys. The sign of the\n * number is ignored; only the magnitude is used.\n */\n pageStep?: number;\n};\n\n// ─────────────────────────\n// CONSTANTS\n// ─────────────────────────\n\n/**\n * Default boolean flags merged with the constructor `options` object.\n *\n * @internal\n */\nconst DEFAULT_OPTIONS = {\n wrap: false,\n memory: true,\n skipDisabled: false,\n} as const;\n\n/**\n * Tolerance in CSS pixels for grouping items into the same grid row when using\n * {@link FocusgroupDirection | `grid`} mode.\n *\n * @internal\n */\nconst GRID_ROW_TOLERANCE_PX = 6;\n\n/**\n * Name of the `CustomEvent` dispatched on the host when the roving tabindex active item changes.\n *\n * The event `bubbles` and is `composed`. Handlers read\n * {@link FocusgroupNavigationActiveChangeDetail} from `event.detail`.\n */\nexport const focusgroupNavigationActiveChange =\n 'swc-focusgroup-navigation-active-change';\n\n/**\n * Describes why the active item changed in a {@link focusgroupNavigationActiveChange} event.\n *\n * - **`keyboard`** — Arrow key, Home, or End navigation moved focus.\n * - **`focus`** — A managed item received DOM focus directly (pointer click or Tab-key entry).\n * - **`refresh`** — {@link FocusgroupNavigationController.refresh} re-parked the roving tab stop.\n * - **`programmatic`** — {@link FocusgroupNavigationController.setActiveItem} or\n * {@link FocusgroupNavigationController.focusFirstItemByTextPrefix} was called directly.\n */\nexport type FocusgroupActiveChangeSource =\n | 'keyboard'\n | 'focus'\n | 'refresh'\n | 'programmatic';\n\n/**\n * `detail` object for the {@link focusgroupNavigationActiveChange} event.\n */\nexport type FocusgroupNavigationActiveChangeDetail = {\n /**\n * Element that now has `tabindex=\"0\"` among managed items, or null when the group is empty.\n */\n activeElement: HTMLElement | null;\n\n /**\n * Why the active item changed. Hosts that implement selection-follows-focus (e.g. automatic\n * activation in a tab list) should react to `'keyboard'` and `'focus'` sources and ignore\n * `'refresh'` and `'programmatic'` to avoid spurious selection changes on mount or when\n * toggling `disabled`.\n */\n source: FocusgroupActiveChangeSource;\n};\n\n/**\n * **FocusgroupNavigation** — implements the roving `tabindex` pattern from the APG\n * keyboard guide and directional navigation similar to the proposed `focusgroup`\n * attribute (Open UI). The exported class name is `FocusgroupNavigationController`.\n *\n * The controller:\n * - Keeps exactly one item in the tab order (`tabindex=\"0\"`) per composite; sets\n * `tabindex=\"-1\"` on other items it manages.\n * - Handles Arrow keys, Home, and End for focus movement (and optionally wrap). **`both`**\n * direction accepts horizontal and vertical arrows on the same `getItems()` sequence.\n * In **`grid`** mode only, **Ctrl+Home** / **Ctrl+End** move to the first cell of the first\n * row or the last cell of the last row (by layout-derived rows).\n * - Optional **`pageStep`**: **Page Up** / **Page Down** move by that many items (linear modes)\n * or rows (**`grid`**).\n * - Optional **`skipDisabled`**: omit **`disabled`** and **`aria-disabled=\"true\"`** items from\n * roving tabindex and arrow navigation.\n * - Supports optional last-focused memory when re-entering via Tab.\n * - Exposes {@link FocusgroupNavigationController.setActiveItem} to choose the roving tab stop\n * without calling `focus()`, and {@link FocusgroupNavigationController.focusFirstItemByTextPrefix}\n * for typeahead-style roving `tabindex` (call {@link FocusgroupNavigationController.getActiveItem}\n * and `focus()` yourself when you want keyboard focus to move). Arrow-key handling calls\n * `setActiveItem` and `focus()` together.\n *\n * Dispatches a bubbling, composed `CustomEvent` named\n * {@link focusgroupNavigationActiveChange} when the active item changes.\n *\n * This is not a browser `focusgroup` implementation; it is a Lit reactive controller\n * for custom elements until native `focusgroup` is available.\n *\n * @example\n * ```typescript\n * class MyToolbar extends LitElement {\n * private readonly navigation = new FocusgroupNavigationController(this, {\n * direction: 'horizontal',\n * wrap: true,\n * getItems: () =>\n * Array.from(this.renderRoot.querySelectorAll<HTMLElement>('button')),\n * });\n *\n * protected override firstUpdated(): void {\n * super.firstUpdated();\n * this.navigation.refresh();\n * }\n * }\n * ```\n *\n * @see https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardnavigationinsidecomponents\n * @see https://open-ui.org/components/scoped-focusgroup.explainer/\n *\n * **Native `focusgroup` (future):** The comment block immediately below this class lists which\n * parts of this file are the most likely candidates for deprecation or deletion once browsers\n * ship built-in focus-group behavior that covers the same cases (especially roving tabindex and\n * arrow-key focus moves). Some options (for example rect-based **grid**, **pageStep**, or\n * **skipDisabled**) may remain useful longer if the platform surface stays narrower.\n */\n// ─────────────────────────────────────────────────────────────────────────────\n// Native `focusgroup` (future) — likely deprecation candidates\n//\n// If/when browsers implement `focusgroup` (or equivalent) with behavior comparable to this\n// controller for your targets, consider removing or shrinking the following areas first:\n//\n// 1. Roving tabindex — `applyRovingTabindex()`, the tabindex portions of `refresh()` and\n// `setActiveItem()`, and assigning `tabIndex` to ineligible raw items.\n//\n// 2. Host keyboard interception — `handleKeydown()`, `hostConnected` / `hostDisconnected`\n// `keydown` listeners, `resolveManagedKeydownTarget()` (shadow retargeting workaround), and\n// navigation helpers: `navigateLinear`, `navigateBothAxes`, `navigateGrid`, `navigatePage`,\n// `navigatePageLinearItems`, `navigatePageGridRows`, `getEffectivePageMagnitude`, plus\n// Home/End and Ctrl+Home/Ctrl+End branches inside `handleKeydown`.\n//\n// 3. JS “memory” for Tab re-entry — `lastFocused`, `handleFocusin` / `handleFocusout` memory\n// paths, and `refresh()`’s preference for `lastFocused` when native group memory replaces\n// this pattern.\n//\n// Often slower to retire (verify against shipped HTML/Open UI behavior): `buildRows` and\n// geometry-based **grid** navigation; **pageStep** (Page Up/Down magnitude); **skipDisabled** and\n// `isDisabledForSkip`; `isNodeWithinHostScope` / `getRawItems` if declarative scoping differs in\n// shadow DOM; `dispatchActiveChange`, `onActiveItemChange`, and the exported event name if\n// products still want a single composed integration hook; `focusFirstItemByTextPrefix` for\n// typeahead roving tabindex (callers focus `getActiveItem()` unless the platform adds an equivalent).\n// `isRtl()` may\n// duplicate or diverge from native axis mapping — revisit when testing RTL with native focusgroup.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class FocusgroupNavigationController implements ReactiveController {\n /**\n * Lit reactive host this controller is attached to.\n */\n private host: ReactiveElement;\n\n /**\n * Effective options (defaults merged with the latest `setOptions` / constructor values).\n */\n private options: FocusgroupNavigationOptions;\n\n /**\n * Capture-phase `keydown` listener reference for removal on disconnect.\n */\n private readonly boundKeydown = this.handleKeydown.bind(this);\n\n /**\n * Capture-phase `focusin` listener reference for removal on disconnect.\n */\n private readonly boundFocusin = this.handleFocusin.bind(this);\n\n /**\n * Capture-phase `focusout` listener reference for removal on disconnect.\n */\n private readonly boundFocusout = this.handleFocusout.bind(this);\n\n /**\n * Cached item for {@link FocusgroupNavigationOptions.memory} when the user moves focus\n * inside or out of the composite. Cleared when that node is no longer returned by\n * `getItems` or when the group becomes empty.\n */\n private lastFocused: HTMLElement | null = null;\n\n /**\n * Tracks the previously dispatched active item so that\n * {@link applyRovingTabindex} only fires the active-change event and\n * {@link FocusgroupNavigationOptions.onActiveItemChange} callback when the\n * active item actually changes.\n */\n private previousActive: HTMLElement | null = null;\n\n /**\n * Guard flag set during keyboard navigation so that the `focusin` triggered\n * by `item.focus()` does not redundantly call {@link applyRovingTabindex}.\n */\n private isNavigating = false;\n\n /**\n * Cached result of {@link getEligibleItems}, populated on first access within\n * a refresh cycle and cleared at the start of each entry point\n * ({@link refresh}, {@link handleFocusin}, {@link handleKeydown}).\n */\n private cachedEligibleItems: HTMLElement[] | null = null;\n\n /**\n * Cached result of {@link buildRows}, populated on first access within a\n * keydown cycle and cleared alongside {@link cachedEligibleItems}.\n */\n private cachedRows: HTMLElement[][] | null = null;\n\n // ─────────────────────────\n // PUBLIC API\n // ─────────────────────────\n\n /**\n * Registers this instance on `host` via `addController` and merges `options` with defaults.\n *\n * @param host - Reactive element that owns the composite (arrow keys and tab order apply within its subtree).\n * @param options - `getItems`, `direction`, and optional behavior flags.\n */\n constructor(host: ReactiveElement, options: FocusgroupNavigationOptions) {\n this.host = host;\n this.options = { ...DEFAULT_OPTIONS, ...options };\n host.addController(this);\n }\n\n /**\n * Merges `partial` into the current options and reapplies roving `tabindex` to the item set.\n *\n * @param partial - Fields to override; omitted keys keep their previous values.\n */\n public setOptions(partial: Partial<FocusgroupNavigationOptions>): void {\n this.options = { ...this.options, ...partial };\n this.refresh();\n }\n\n /**\n * Returns the eligible managed item that currently participates in the sequential focus order\n * (`tabindex=\"0\"`), or null if no eligible item has tab index zero.\n *\n * @returns The active roving item, or null.\n */\n public getActiveItem(): HTMLElement | null {\n for (const el of this.getEligibleItems()) {\n if (el.tabIndex === 0) {\n return el;\n }\n }\n return null;\n }\n\n /**\n * Re-queries `getItems()`, recomputes eligibility, and syncs roving `tabindex`.\n *\n * Call after the item list or item eligibility changes (for example after Lit\n * `updated()` or slot changes). When {@link FocusgroupNavigationOptions.memory} is true,\n * prefers the stored last-focused item if it is still eligible; otherwise keeps the\n * current active item or falls back to the first eligible item.\n */\n public refresh(): void {\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n if (items.length === 0) {\n for (const el of this.getRawItems()) {\n el.tabIndex = -1;\n }\n this.lastFocused = null;\n if (this.previousActive !== null) {\n this.previousActive = null;\n this.dispatchActiveChange(null, 'refresh');\n this.options.onActiveItemChange?.(null);\n }\n return;\n }\n\n const preferred =\n (this.options.memory &&\n this.lastFocused &&\n items.includes(this.lastFocused)\n ? this.lastFocused\n : null) ??\n this.getActiveItem() ??\n items[0];\n\n this.applyRovingTabindex(preferred, 'refresh');\n }\n\n /**\n * Sets roving `tabindex` so `item` is the active tab stop (`tabindex=\"0\"`) and others in the\n * group are `-1`. Does **not** call `focus()`. When {@link FocusgroupNavigationOptions.memory}\n * is true, updates the stored last-focused item so Tab re-entry can target this item.\n *\n * @param item - Item to mark active; must be returned by `getItems` and pass eligibility checks.\n * @returns False if `item` is not in the current eligible item list.\n */\n public setActiveItem(item: HTMLElement): boolean {\n const items = this.getEligibleItems();\n if (!items.includes(item)) {\n return false;\n }\n this.applyRovingTabindex(item, 'programmatic');\n if (this.options.memory) {\n this.lastFocused = item;\n }\n return true;\n }\n\n /**\n * Updates roving `tabindex` so the first **eligible** item (same set as arrow navigation)\n * whose typeahead label starts with `prefix` becomes the active tab stop (`tabindex=\"0\"`).\n * Matching is **case-insensitive**. The label is the first non-empty of: trimmed\n * **`aria-label`**, trimmed text from **`aria-labelledby`** references (in order, space-joined),\n * or trimmed **`textContent`**. Search order matches arrow-key traversal.\n *\n * Does **not** call `focus()`. After this returns `true`, call `focus()` on\n * {@link FocusgroupNavigationController.getActiveItem} (for example `getActiveItem()?.focus()`),\n * often from a **microtask** when the caller runs from a pointer handler so focus is not\n * overwritten by the clicked control.\n *\n * Typical use: menu typeahead; wire `keydown` or `input` at the host and debounce as needed.\n *\n * @param prefix - String to match as a leading substring after `trim`; whitespace-only yields\n * no match and returns `false`.\n * @returns True if a matching item was found and roving tabindex was applied.\n */\n public focusFirstItemByTextPrefix(prefix: string): boolean {\n const trimmed = prefix.trim();\n if (trimmed === '') {\n return false;\n }\n const needle = trimmed.toLowerCase();\n const items = this.getEligibleItems();\n const match = items.find((el) => {\n const label = this.getItemTypeaheadLabel(el).toLowerCase();\n return label.startsWith(needle);\n });\n if (!match) {\n return false;\n }\n this.applyRovingTabindex(match, 'programmatic');\n return true;\n }\n\n /**\n * Lit `ReactiveController` hook: registers capture-phase listeners on `host` and runs\n * an initial {@link refresh}.\n */\n public hostConnected(): void {\n this.previousActive = null;\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n this.host.addEventListener('keydown', this.boundKeydown, true);\n this.host.addEventListener('focusin', this.boundFocusin, true);\n this.host.addEventListener('focusout', this.boundFocusout, true);\n this.refresh();\n }\n\n /**\n * Lit `ReactiveController` hook: removes listeners registered in {@link hostConnected}.\n */\n public hostDisconnected(): void {\n this.host.removeEventListener('keydown', this.boundKeydown, true);\n this.host.removeEventListener('focusin', this.boundFocusin, true);\n this.host.removeEventListener('focusout', this.boundFocusout, true);\n }\n\n // ─────────────────────────\n // IMPLEMENTATION\n // ─────────────────────────\n //\n // Which parts may become redundant under native `focusgroup` is summarized in the\n // “Native `focusgroup` (future)” comment block directly above the class declaration.\n\n /**\n * Resolves writing direction from the computed style of the host element.\n *\n * Uses `getComputedStyle` rather than walking `dir` attributes so that\n * CSS-inherited direction (the 2nd-gen default) is correctly detected.\n *\n * @returns True when horizontal arrow directions should follow RTL semantics.\n */\n private isRtl(): boolean {\n return getComputedStyle(this.host).direction === 'rtl';\n }\n\n /**\n * Whether `node` is the host or reachable from it by walking `parentNode` and\n * `ShadowRoot.host` (so shadow descendants count, including nested shadow roots).\n *\n * `Element.contains()` is not used because it returns false for nodes inside the\n * host's shadow tree, which would drop every item for typical Lit components.\n *\n * @param node - Node to test (may be null).\n * @returns True if `node` is in the host's shadow-inclusive subtree.\n */\n private isNodeWithinHostScope(node: Node | null): boolean {\n if (!node) {\n return false;\n }\n const host = this.host;\n let current: Node | null = node;\n while (current) {\n if (current === host) {\n return true;\n }\n const parent: Node | null = current.parentNode;\n if (parent) {\n current = parent;\n } else if (current instanceof ShadowRoot) {\n current = current.host;\n } else {\n return false;\n }\n }\n return false;\n }\n\n /**\n * Items returned by `getItems` that lie within `host` (shadow-inclusive tree).\n *\n * @returns Candidates before eligibility filtering.\n */\n private getRawItems(): HTMLElement[] {\n return this.options\n .getItems()\n .filter((el) => this.isNodeWithinHostScope(el));\n }\n\n /**\n * {@link getRawItems} filtered by {@link isNavigableItem}.\n *\n * @returns Items that participate in roving tabindex and arrow navigation.\n */\n private getEligibleItems(): HTMLElement[] {\n if (this.cachedEligibleItems) {\n return this.cachedEligibleItems;\n }\n this.cachedEligibleItems = this.getRawItems().filter((el) =>\n this.isNavigableItem(el)\n );\n return this.cachedEligibleItems;\n }\n\n /**\n * {@link buildRows} with per-cycle caching, cleared alongside\n * {@link cachedEligibleItems}.\n *\n * @param items - Eligible items to lay out as a grid.\n * @returns Cached row-major array of rows.\n */\n private getRows(items: HTMLElement[]): HTMLElement[][] {\n if (this.cachedRows) {\n return this.cachedRows;\n }\n this.cachedRows = this.buildRows(items);\n return this.cachedRows;\n }\n\n /**\n * Whether `el` may participate in the focus group (connected, visible, not inert,\n * and not skipped when {@link FocusgroupNavigationOptions.skipDisabled} is true).\n *\n * @param el - Candidate from `getItems`.\n * @returns True if the element counts as navigable for this controller.\n */\n private isNavigableItem(el: HTMLElement): boolean {\n if (!el.isConnected) {\n return false;\n }\n if (el.hasAttribute('inert') || el.closest('[inert]')) {\n return false;\n }\n const style = getComputedStyle(el);\n if (style.visibility === 'hidden' || style.display === 'none') {\n return false;\n }\n if (this.options.skipDisabled && this.isDisabledForSkip(el)) {\n return false;\n }\n return true;\n }\n\n /**\n * Whether `el` should be treated as disabled for {@link FocusgroupNavigationOptions.skipDisabled}.\n *\n * @param el - Element to test.\n * @returns True if the native `disabled` property is true or `aria-disabled` is `\"true\"`.\n */\n private isDisabledForSkip(el: HTMLElement): boolean {\n if ('disabled' in el && (el as HTMLButtonElement).disabled) {\n return true;\n }\n return el.getAttribute('aria-disabled') === 'true';\n }\n\n /**\n * String used for {@link focusFirstItemByTextPrefix}: prefers **`aria-label`**, then text from\n * **`aria-labelledby`** (IDs resolved in the shadow root or document), else **`textContent`**.\n * All branches are trimmed; empty strings fall through to the next source.\n */\n private getItemTypeaheadLabel(el: HTMLElement): string {\n const fromAria = el.getAttribute('aria-label')?.trim();\n if (fromAria) {\n return fromAria;\n }\n const labelledBy = el.getAttribute('aria-labelledby')?.trim();\n if (labelledBy) {\n const root = el.getRootNode();\n const chunks: string[] = [];\n for (const id of labelledBy.split(/\\s+/)) {\n if (!id) {\n continue;\n }\n const ref =\n root instanceof ShadowRoot\n ? (root.getElementById(id) ?? el.ownerDocument.getElementById(id))\n : el.ownerDocument.getElementById(id);\n const t = ref?.textContent?.trim();\n if (t) {\n chunks.push(t);\n }\n }\n const joined = chunks.join(' ').trim();\n if (joined) {\n return joined;\n }\n }\n return el.textContent?.trim() ?? '';\n }\n\n /**\n * Whether `el` is natively disabled and therefore unable to receive focus\n * regardless of its `tabindex` value.\n */\n private isNativelyDisabled(el: HTMLElement): boolean {\n return 'disabled' in el && (el as HTMLButtonElement).disabled === true;\n }\n\n /**\n * Sets `tabindex=\"-1\"` on ineligible raw items, then assigns `tabindex=\"0\"` to\n * `active` (or the first eligible item if `active` is not eligible) and `-1` to the rest.\n *\n * When `skipDisabled` is false, natively disabled items remain in the eligible list\n * for arrow navigation but are never chosen as the roving tab stop because they\n * cannot receive focus. The tab stop falls through to the nearest non-disabled item.\n *\n * Dispatches the active-change event and {@link FocusgroupNavigationOptions.onActiveItemChange}.\n *\n * @param active - Preferred item to mark as the single tab stop when eligible.\n * @param source - Why the active item is changing; included in the dispatched event detail.\n */\n private applyRovingTabindex(\n active: HTMLElement,\n source: FocusgroupActiveChangeSource\n ): void {\n const items = this.getEligibleItems();\n const eligibleSet = new Set(items);\n for (const el of this.getRawItems()) {\n if (!eligibleSet.has(el)) {\n el.tabIndex = -1;\n }\n }\n if (items.length === 0) {\n return;\n }\n\n let safeActive = eligibleSet.has(active) ? active : items[0];\n\n // Natively disabled elements cannot receive focus even with tabindex=\"0\".\n // Fall through to the first non-disabled eligible item so the group\n // remains reachable via Tab.\n //\n // The active-change event is dispatched with the originally requested item\n // (before the fallback), not the fallback tab-stop. This lets consumers\n // such as a tab list in automatic-activation mode inspect the item and skip\n // the selection change when it is disabled — without being misled by the\n // roving tab stop landing on a different element.\n const reportedActive = safeActive;\n if (this.isNativelyDisabled(safeActive)) {\n safeActive =\n items.find((el) => !this.isNativelyDisabled(el)) ?? safeActive;\n }\n\n for (const el of items) {\n if (el === safeActive) {\n el.tabIndex = 0;\n } else {\n el.tabIndex = -1;\n }\n }\n if (reportedActive !== this.previousActive) {\n this.previousActive = reportedActive;\n this.dispatchActiveChange(reportedActive, source);\n this.options.onActiveItemChange?.(safeActive);\n }\n }\n\n /**\n * Dispatches {@link focusgroupNavigationActiveChange} on the reactive host with the given detail.\n *\n * @param activeElement - New active item, or null when clearing selection.\n * @param source - Why the active item changed.\n */\n private dispatchActiveChange(\n activeElement: HTMLElement | null,\n source: FocusgroupActiveChangeSource\n ): void {\n this.host.dispatchEvent(\n new CustomEvent<FocusgroupNavigationActiveChangeDetail>(\n focusgroupNavigationActiveChange,\n {\n bubbles: true,\n composed: true,\n detail: { activeElement, source },\n }\n )\n );\n }\n\n /**\n * Resolves the managed item that actually received focus inside the shadow tree.\n *\n * Same retargeting problem as {@link resolveManagedKeydownTarget}: listeners on\n * the shadow host see `event.target` retargeted to the host when focus lands on a\n * descendant inside the shadow root. Walk `composedPath()` and fall back to\n * `shadowRoot.activeElement` to find the real focused managed item.\n *\n * @param event - Focus event dispatched while focus moves into the composite.\n * @param items - Current eligible items from {@link getEligibleItems}.\n * @returns The managed element that received focus, or null.\n */\n private resolveManagedFocusTarget(\n event: FocusEvent,\n items: HTMLElement[]\n ): HTMLElement | null {\n if (items.length === 0) {\n return null;\n }\n const set = new Set(items);\n for (const node of event.composedPath()) {\n if (!(node instanceof HTMLElement)) {\n continue;\n }\n if (set.has(node)) {\n return node;\n }\n if (node === this.host) {\n break;\n }\n }\n const root = this.host.shadowRoot;\n const active = root?.activeElement;\n if (active instanceof HTMLElement && set.has(active)) {\n return active;\n }\n return null;\n }\n\n /**\n * Capture-phase `focusin` handler: syncs roving `tabindex` when focus moves to a managed item\n * (for example via pointer), and updates memory when enabled.\n *\n * @param event - Focus event whose target may be a group item.\n */\n private handleFocusin(event: FocusEvent): void {\n if (this.isNavigating) {\n return;\n }\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n const target = this.resolveManagedFocusTarget(event, items);\n if (!target) {\n return;\n }\n this.applyRovingTabindex(target, 'focus');\n if (this.options.memory) {\n this.lastFocused = target;\n }\n }\n\n /**\n * Capture-phase `focusout` handler: when focus leaves the host subtree, stores the\n * previous target for {@link FocusgroupNavigationOptions.memory}.\n *\n * @param event - Focus event; `relatedTarget` stays inside the host when moving between items.\n */\n private handleFocusout(event: FocusEvent): void {\n const next = event.relatedTarget;\n if (next instanceof Node && this.isNodeWithinHostScope(next)) {\n return;\n }\n const target = event.target;\n if (\n this.options.memory &&\n target instanceof HTMLElement &&\n this.getRawItems().includes(target)\n ) {\n this.lastFocused = target;\n }\n // When memory is off, reset the roving tab stop to the first eligible\n // item so Tab re-entry always starts from the beginning.\n if (!this.options.memory) {\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n if (items.length > 0) {\n this.applyRovingTabindex(items[0], 'focus');\n }\n }\n }\n\n /**\n * Resolves which managed item should receive arrow, Home, End, or grid Ctrl+Home / Ctrl+End\n * handling for this key event.\n *\n * Listeners on the shadow **host** often see a **retargeted** {@link KeyboardEvent.target}\n * (the host) while focus is on a descendant inside the shadow tree, so matching\n * `event.target` against `getItems()` fails. {@link Event.composedPath} still includes the\n * focused node; we also fall back to {@link ShadowRoot.activeElement} when needed.\n *\n * @param event - Keyboard event dispatched while focus is in this composite.\n * @param items - Current eligible items from {@link getEligibleItems}.\n * @returns The managed element to treat as keydown target, or null.\n */\n private resolveManagedKeydownTarget(\n event: KeyboardEvent,\n items: HTMLElement[]\n ): HTMLElement | null {\n if (items.length === 0) {\n return null;\n }\n const set = new Set(items);\n for (const node of event.composedPath()) {\n if (!(node instanceof HTMLElement)) {\n continue;\n }\n if (set.has(node)) {\n return node;\n }\n if (node === this.host) {\n break;\n }\n }\n const root = this.host.shadowRoot;\n const active = root?.activeElement;\n if (active instanceof HTMLElement && set.has(active)) {\n return active;\n }\n return null;\n }\n\n /**\n * Capture-phase `keydown` handler: arrow keys and Home/End move focus among eligible items\n * when the event target is managed; calls `preventDefault` when handling navigation.\n *\n * When {@link FocusgroupDirection | `direction`} is **`both`**, **ArrowLeft** / **ArrowRight**\n * and **ArrowUp** / **ArrowDown** all participate (see {@link navigateBothAxes}).\n *\n * When {@link FocusgroupDirection | `direction`} is **`grid`**, **Ctrl+Home** focuses the\n * first cell in the first row and **Ctrl+End** focuses the last cell in the last row (from\n * {@link buildRows}); other modifier combinations are ignored except plain Home/End.\n *\n * When {@link FocusgroupNavigationOptions.pageStep} is a non-zero finite number, **Page Up**\n * and **Page Down** are handled before arrow keys (see {@link navigatePage}).\n *\n * @param event - Keyboard event from the focused element inside the host.\n */\n private handleKeydown(event: KeyboardEvent): void {\n if (event.defaultPrevented || event.altKey) {\n return;\n }\n\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n const target = this.resolveManagedKeydownTarget(event, items);\n if (!target) {\n return;\n }\n\n const isGrid = this.options.direction === 'grid';\n const rows = isGrid ? this.getRows(items) : null;\n\n if (\n isGrid &&\n event.ctrlKey &&\n !event.metaKey &&\n (event.key === 'Home' || event.key === 'End')\n ) {\n if (rows!.length > 0) {\n const firstRow = rows![0];\n const lastRow = rows![rows!.length - 1];\n const boundary =\n event.key === 'Home'\n ? (firstRow?.[0] ?? null)\n : (lastRow?.[lastRow.length - 1] ?? null);\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n }\n return;\n }\n\n if (event.ctrlKey || event.metaKey) {\n return;\n }\n\n const pageMagnitude = this.getEffectivePageMagnitude();\n if (\n pageMagnitude !== null &&\n (event.key === 'PageUp' || event.key === 'PageDown')\n ) {\n const pageNext = this.navigatePage(\n items,\n target,\n event.key === 'PageDown' ? pageMagnitude : -pageMagnitude,\n rows\n );\n if (pageNext && pageNext !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(pageNext);\n }\n return;\n }\n\n const rtl = this.isRtl();\n let next: HTMLElement | null = null;\n\n switch (this.options.direction) {\n case 'horizontal':\n next = this.navigateLinear(items, target, event.key, 'horizontal', rtl);\n break;\n case 'vertical':\n next = this.navigateLinear(items, target, event.key, 'vertical', rtl);\n break;\n case 'both':\n next = this.navigateBothAxes(items, target, event.key, rtl);\n break;\n case 'grid':\n next = this.navigateGrid(target, event.key, rtl, rows!);\n break;\n default:\n break;\n }\n\n if (next && next !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(next);\n return;\n }\n\n if (event.key === 'Home' || event.key === 'End') {\n if (isGrid) {\n // APG grid pattern: Home/End scope to the current row.\n // Ctrl+Home/End (entire grid) is handled above.\n const pos = this.findGridIndex(rows!, target);\n if (!pos) {\n return;\n }\n const currentRow = rows![pos.row];\n if (!currentRow?.length) {\n return;\n }\n const boundary =\n event.key === 'Home'\n ? currentRow[0]\n : currentRow[currentRow.length - 1];\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n } else {\n if (items.length === 0) {\n return;\n }\n const boundary =\n event.key === 'Home' ? items[0] : items[items.length - 1];\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n }\n }\n }\n\n /**\n * Applies roving tabindex to `item` and moves DOM focus; used for keyboard navigation only.\n */\n private moveKeyNavigationFocusTo(item: HTMLElement): void {\n this.isNavigating = true;\n try {\n const items = this.getEligibleItems();\n if (items.includes(item)) {\n this.applyRovingTabindex(item, 'keyboard');\n if (this.options.memory) {\n this.lastFocused = item;\n }\n item.focus();\n }\n } finally {\n this.isNavigating = false;\n }\n }\n\n /**\n * Positive step count for {@link FocusgroupNavigationOptions.pageStep}, or null when page keys\n * are disabled.\n */\n private getEffectivePageMagnitude(): number | null {\n const raw = this.options.pageStep;\n if (raw === undefined) {\n return null;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n === 0) {\n return null;\n }\n return Math.abs(n);\n }\n\n /**\n * Target for **Page Up** / **Page Down** when {@link getEffectivePageMagnitude} is set.\n *\n * @param items - Eligible items.\n * @param current - Focused item.\n * @param signedDelta - `+magnitude` for Page Down or `-magnitude` for Page Up (items for\n * linear modes, rows for `grid`).\n */\n private navigatePage(\n items: HTMLElement[],\n current: HTMLElement,\n signedDelta: number,\n rows: HTMLElement[][] | null\n ): HTMLElement | null {\n if (this.options.direction === 'grid') {\n return this.navigatePageGridRows(current, signedDelta, rows!);\n }\n return this.navigatePageLinearItems(items, current, signedDelta);\n }\n\n /**\n * Page Up/Down along `getItems()` order (used for `horizontal`, `vertical`, and `both`).\n */\n private navigatePageLinearItems(\n items: HTMLElement[],\n current: HTMLElement,\n deltaIdx: number\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0 || items.length === 0) {\n return null;\n }\n let nextIdx = idx + deltaIdx;\n if (this.options.wrap) {\n const len = items.length;\n nextIdx = ((nextIdx % len) + len) % len;\n } else {\n nextIdx = Math.max(0, Math.min(items.length - 1, nextIdx));\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Page Up/Down by whole rows in `grid` mode (column clamped per {@link navigateGrid}).\n */\n private navigatePageGridRows(\n current: HTMLElement,\n rowDelta: number,\n grid: HTMLElement[][]\n ): HTMLElement | null {\n if (grid.length === 0) {\n return null;\n }\n const pos = this.findGridIndex(grid, current);\n if (!pos) {\n return null;\n }\n const { row, col } = pos;\n let nextRow = row + rowDelta;\n if (this.options.wrap) {\n const n = grid.length;\n nextRow = ((nextRow % n) + n) % n;\n } else {\n nextRow = Math.max(0, Math.min(grid.length - 1, nextRow));\n }\n const targetRow = grid[nextRow];\n if (!targetRow?.length) {\n return null;\n }\n const clampedCol = Math.min(col, targetRow.length - 1);\n return targetRow[clampedCol] ?? null;\n }\n\n /**\n * Computes the next focus target for linear {@link FocusgroupDirection} modes.\n *\n * @param items - Eligible items in traversal order.\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param mode - `horizontal` (inline axis) or `vertical` (block axis).\n * @param rtl - When true, horizontal Left/Right swap forward/backward.\n * @returns Next item, or null if the key is not a navigation key or movement is blocked.\n */\n private navigateLinear(\n items: HTMLElement[],\n current: HTMLElement,\n key: string,\n mode: 'horizontal' | 'vertical',\n rtl: boolean\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0) {\n return null;\n }\n\n let delta = 0;\n if (mode === 'horizontal') {\n if (key === 'ArrowLeft') {\n delta = rtl ? 1 : -1;\n } else if (key === 'ArrowRight') {\n delta = rtl ? -1 : 1;\n }\n } else {\n if (key === 'ArrowUp') {\n delta = -1;\n } else if (key === 'ArrowDown') {\n delta = 1;\n }\n }\n\n if (delta === 0) {\n return null;\n }\n\n let nextIdx = idx + delta;\n if (this.options.wrap) {\n nextIdx = (nextIdx + items.length) % items.length;\n } else if (nextIdx < 0 || nextIdx >= items.length) {\n return null;\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Computes the next focus target when {@link FocusgroupDirection | `direction`} is **`both`**:\n * inline arrows use the same deltas as {@link navigateLinear} `horizontal` mode; **ArrowUp** /\n * **ArrowDown** step backward / forward in `getItems()` order (not flipped by `dir`).\n *\n * @param items - Eligible items in traversal order.\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param rtl - When true, horizontal Left/Right swap forward/backward.\n * @returns Next item, or null if the key is not handled or movement is blocked.\n */\n private navigateBothAxes(\n items: HTMLElement[],\n current: HTMLElement,\n key: string,\n rtl: boolean\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0) {\n return null;\n }\n\n let delta = 0;\n if (key === 'ArrowLeft') {\n delta = rtl ? 1 : -1;\n } else if (key === 'ArrowRight') {\n delta = rtl ? -1 : 1;\n } else if (key === 'ArrowUp') {\n delta = -1;\n } else if (key === 'ArrowDown') {\n delta = 1;\n }\n\n if (delta === 0) {\n return null;\n }\n\n let nextIdx = idx + delta;\n if (this.options.wrap) {\n nextIdx = (nextIdx + items.length) % items.length;\n } else if (nextIdx < 0 || nextIdx >= items.length) {\n return null;\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Computes the next focus target for `grid` {@link FocusgroupDirection} mode using\n * row clustering and column indices.\n *\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param rtl - When true, horizontal Left/Right swap column direction within a row.\n * @param grid - Pre-built row grid from {@link buildRows}.\n * @returns Next cell item, or null if the key is not handled or movement is blocked.\n */\n private navigateGrid(\n current: HTMLElement,\n key: string,\n rtl: boolean,\n grid: HTMLElement[][]\n ): HTMLElement | null {\n const pos = this.findGridIndex(grid, current);\n if (!pos) {\n return null;\n }\n const { row, col } = pos;\n const rowItems = grid[row] ?? [];\n let nextRow = row;\n let nextCol = col;\n\n switch (key) {\n case 'ArrowLeft':\n nextCol = rtl ? col + 1 : col - 1;\n break;\n case 'ArrowRight':\n nextCol = rtl ? col - 1 : col + 1;\n break;\n case 'ArrowUp':\n nextRow = row - 1;\n break;\n case 'ArrowDown':\n nextRow = row + 1;\n break;\n default:\n return null;\n }\n\n if (key === 'ArrowLeft' || key === 'ArrowRight') {\n if (nextCol >= 0 && nextCol < rowItems.length) {\n return rowItems[nextCol] ?? null;\n }\n if (this.options.wrap && rowItems.length > 0) {\n const wrappedCol = (nextCol + rowItems.length) % rowItems.length;\n return rowItems[wrappedCol] ?? null;\n }\n return null;\n }\n\n if (nextRow < 0 || nextRow >= grid.length) {\n if (this.options.wrap && grid.length > 0) {\n nextRow = (nextRow + grid.length) % grid.length;\n } else {\n return null;\n }\n }\n\n const targetRow = grid[nextRow];\n if (!targetRow?.length) {\n return null;\n }\n const clampedCol = Math.min(col, targetRow.length - 1);\n return targetRow[clampedCol] ?? null;\n }\n\n /**\n * Groups `items` into rows by similar `getBoundingClientRect().top`, then sorts each row by `left`.\n *\n * @param items - Eligible elements to lay out as a grid.\n * @returns Row-major array of rows; each row is left-to-right.\n */\n private buildRows(items: HTMLElement[]): HTMLElement[][] {\n type RowAcc = { top: number; elements: HTMLElement[] };\n const rows: RowAcc[] = [];\n\n for (const el of items) {\n const top = el.getBoundingClientRect().top;\n let row = rows.find(\n (r) => Math.abs(r.top - top) <= GRID_ROW_TOLERANCE_PX\n );\n if (!row) {\n row = { top, elements: [] };\n rows.push(row);\n }\n row.elements.push(el);\n }\n\n rows.sort((a, b) => a.top - b.top);\n return rows.map((r) =>\n r.elements.sort(\n (a, b) =>\n a.getBoundingClientRect().left - b.getBoundingClientRect().left\n )\n );\n }\n\n /**\n * Locates `el` in a row-major grid built by {@link buildRows}.\n *\n * @param grid - Rows of elements.\n * @param el - Element to find.\n * @returns Row and column indices, or null if absent.\n */\n private findGridIndex(\n grid: HTMLElement[][],\n el: HTMLElement\n ): { row: number; col: number } | null {\n for (let r = 0; r < grid.length; r++) {\n const c = grid[r].indexOf(el);\n if (c !== -1) {\n return { row: r, col: c };\n }\n }\n return null;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"focusgroup-navigation-controller.js","names":[],"sources":["../../../../controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.ts"],"sourcesContent":["/**\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport type { ReactiveController, ReactiveElement } from 'lit';\n\n// ─────────────────────────\n// TYPES\n// ─────────────────────────\n\n/**\n * Spatial mode for arrow-key movement. Aligns with logical axes (inline/block) and a\n * 2D layout mode derived from element geometry.\n *\n * - **horizontal**: Arrow keys on the inline axis move focus (respects `dir`).\n * - **vertical**: Arrow keys on the block axis move focus.\n * - **both**: **ArrowLeft** / **ArrowRight** move along `getItems()` order like **horizontal**\n * (respects `dir`); **ArrowUp** / **ArrowDown** move backward / forward in the same order.\n * - **grid**: Arrow keys move in rows and columns using bounding-rect layout; Ctrl+Home / Ctrl+End\n * jump to the first cell of the first row or the last cell of the last row.\n */\nexport type FocusgroupDirection = 'horizontal' | 'vertical' | 'both' | 'grid';\n\n/**\n * Options for {@link FocusgroupNavigationController}.\n */\nexport type FocusgroupNavigationOptions = {\n /**\n * Returns the current set of items that participate in roving tabindex and\n * directional navigation. Callers typically close over the host (for example\n * querying slotted or shadow DOM children).\n */\n getItems: () => HTMLElement[];\n\n /**\n * Determines which arrow keys move focus and how grid navigation is computed.\n * Use **`both`** when the same linear order should respond to horizontal and vertical arrow keys.\n */\n direction: FocusgroupDirection;\n\n /**\n * When true, arrow keys wrap from the last item to the first (and reverse).\n * Defaults to false.\n */\n wrap?: boolean;\n\n /**\n * When true, restoring focus into the composite (for example with Tab) targets\n * the item that was last focused, if it is still a member of the group.\n * Similar to the default memory behavior described for `focusgroup` in Open UI.\n * Defaults to true.\n */\n memory?: boolean;\n\n /**\n * When true, both natively `disabled` and `aria-disabled=\"true\"` items are\n * skipped for arrow navigation and are not chosen as the roving tab stop.\n * When false (default), disabled items remain in sequence — useful for\n * patterns such as menus where disabled items may still be focusable per\n * APG guidance.\n *\n * **Note:** Regardless of this flag, natively `disabled` elements are never\n * chosen as the roving tab stop (`tabindex=\"0\"`) because they cannot receive\n * browser focus; see {@link applyRovingTabindex}. A future revision may\n * decouple native `disabled` and `aria-disabled` into separate options if\n * component migrations surface the need.\n *\n * Defaults to false.\n */\n skipDisabled?: boolean;\n\n /**\n * Invoked after the active item changes and `tabindex` values are synchronized.\n * The argument is the new active element, or null when the group has no eligible items.\n */\n onActiveItemChange?: (active: HTMLElement | null) => void;\n\n /**\n * When set to a **non-zero** integer, **Page Up** / **Page Down** move focus by that many\n * positions in `getItems()` order for **`horizontal`**, **`vertical`**, and **`both`** modes\n * (respects **`wrap`** the same way as single-step arrows).\n * For **`grid`**, page keys move by that many **rows** (column index is clamped to each row’s\n * length). Omitted, `0`, `NaN`, and non-finite values disable page keys. The sign of the\n * number is ignored; only the magnitude is used.\n */\n pageStep?: number;\n};\n\n// ─────────────────────────\n// CONSTANTS\n// ─────────────────────────\n\n/**\n * Default boolean flags merged with the constructor `options` object.\n *\n * @internal\n */\nconst DEFAULT_OPTIONS = {\n wrap: false,\n memory: true,\n skipDisabled: false,\n} as const;\n\n/**\n * Tolerance in CSS pixels for grouping items into the same grid row when using\n * {@link FocusgroupDirection | `grid`} mode.\n *\n * @internal\n */\nconst GRID_ROW_TOLERANCE_PX = 6;\n\n/**\n * Name of the `CustomEvent` dispatched on the host when the roving tabindex active item changes.\n *\n * The event `bubbles` and is `composed`. Handlers read\n * {@link FocusgroupNavigationActiveChangeDetail} from `event.detail`.\n */\nexport const focusgroupNavigationActiveChange =\n 'swc-focusgroup-navigation-active-change';\n\n/**\n * Describes why the active item changed in a {@link focusgroupNavigationActiveChange} event.\n *\n * - **`keyboard`** — Arrow key, Home, or End navigation moved focus.\n * - **`focus`** — A managed item received DOM focus directly (pointer click or Tab-key entry).\n * - **`refresh`** — {@link FocusgroupNavigationController.refresh} re-parked the roving tab stop.\n * - **`programmatic`** — {@link FocusgroupNavigationController.setActiveItem} or\n * {@link FocusgroupNavigationController.focusFirstItemByTextPrefix} was called directly.\n */\nexport type FocusgroupActiveChangeSource =\n | 'keyboard'\n | 'focus'\n | 'refresh'\n | 'programmatic';\n\n/**\n * `detail` object for the {@link focusgroupNavigationActiveChange} event.\n */\nexport type FocusgroupNavigationActiveChangeDetail = {\n /**\n * Element that now has `tabindex=\"0\"` among managed items, or null when the group is empty.\n */\n activeElement: HTMLElement | null;\n\n /**\n * Why the active item changed. Hosts that implement selection-follows-focus (e.g. automatic\n * activation in a tab list) should react to `'keyboard'` and `'focus'` sources and ignore\n * `'refresh'` and `'programmatic'` to avoid spurious selection changes on mount or when\n * toggling `disabled`.\n */\n source: FocusgroupActiveChangeSource;\n};\n\n/**\n * **FocusgroupNavigation** — implements the roving `tabindex` pattern from the APG\n * keyboard guide and directional navigation similar to the proposed `focusgroup`\n * attribute (Open UI). The exported class name is `FocusgroupNavigationController`.\n *\n * The controller:\n * - Keeps exactly one item in the tab order (`tabindex=\"0\"`) per composite; sets\n * `tabindex=\"-1\"` on other items it manages.\n * - Handles Arrow keys, Home, and End for focus movement (and optionally wrap). **`both`**\n * direction accepts horizontal and vertical arrows on the same `getItems()` sequence.\n * In **`grid`** mode only, **Ctrl+Home** / **Ctrl+End** move to the first cell of the first\n * row or the last cell of the last row (by layout-derived rows).\n * - Optional **`pageStep`**: **Page Up** / **Page Down** move by that many items (linear modes)\n * or rows (**`grid`**).\n * - Optional **`skipDisabled`**: omit **`disabled`** and **`aria-disabled=\"true\"`** items from\n * roving tabindex and arrow navigation.\n * - Supports optional last-focused memory when re-entering via Tab.\n * - Exposes {@link FocusgroupNavigationController.setActiveItem} to choose the roving tab stop\n * without calling `focus()`, and {@link FocusgroupNavigationController.focusFirstItemByTextPrefix}\n * for typeahead-style roving `tabindex` (call {@link FocusgroupNavigationController.getActiveItem}\n * and `focus()` yourself when you want keyboard focus to move). Arrow-key handling calls\n * `setActiveItem` and `focus()` together.\n *\n * Dispatches a bubbling, composed `CustomEvent` named\n * {@link focusgroupNavigationActiveChange} when the active item changes.\n *\n * This is not a browser `focusgroup` implementation; it is a Lit reactive controller\n * for custom elements until native `focusgroup` is available.\n *\n * @example\n * ```typescript\n * class MyToolbar extends LitElement {\n * private readonly navigation = new FocusgroupNavigationController(this, {\n * direction: 'horizontal',\n * wrap: true,\n * getItems: () =>\n * Array.from(this.renderRoot.querySelectorAll<HTMLElement>('button')),\n * });\n *\n * protected override firstUpdated(): void {\n * super.firstUpdated();\n * this.navigation.refresh();\n * }\n * }\n * ```\n *\n * @see https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardnavigationinsidecomponents\n * @see https://open-ui.org/components/scoped-focusgroup.explainer/\n *\n * **Native `focusgroup` (future):** The comment block immediately below this class lists which\n * parts of this file are the most likely candidates for deprecation or deletion once browsers\n * ship built-in focus-group behavior that covers the same cases (especially roving tabindex and\n * arrow-key focus moves). Some options (for example rect-based **grid**, **pageStep**, or\n * **skipDisabled**) may remain useful longer if the platform surface stays narrower.\n */\n// ─────────────────────────────────────────────────────────────────────────────\n// Native `focusgroup` (future) — likely deprecation candidates\n//\n// If/when browsers implement `focusgroup` (or equivalent) with behavior comparable to this\n// controller for your targets, consider removing or shrinking the following areas first:\n//\n// 1. Roving tabindex — `applyRovingTabindex()`, the tabindex portions of `refresh()` and\n// `setActiveItem()`, and assigning `tabIndex` to ineligible raw items.\n//\n// 2. Host keyboard interception — `handleKeydown()`, `hostConnected` / `hostDisconnected`\n// `keydown` listeners, `resolveManagedKeydownTarget()` (shadow retargeting workaround), and\n// navigation helpers: `navigateLinear`, `navigateBothAxes`, `navigateGrid`, `navigatePage`,\n// `navigatePageLinearItems`, `navigatePageGridRows`, `getEffectivePageMagnitude`, plus\n// Home/End and Ctrl+Home/Ctrl+End branches inside `handleKeydown`.\n//\n// 3. JS “memory” for Tab re-entry — `lastFocused`, `handleFocusin` / `handleFocusout` memory\n// paths, and `refresh()`’s preference for `lastFocused` when native group memory replaces\n// this pattern.\n//\n// Often slower to retire (verify against shipped HTML/Open UI behavior): `buildRows` and\n// geometry-based **grid** navigation; **pageStep** (Page Up/Down magnitude); **skipDisabled** and\n// `isDisabledForSkip`; `isNodeWithinHostScope` / `getRawItems` if declarative scoping differs in\n// shadow DOM; `dispatchActiveChange`, `onActiveItemChange`, and the exported event name if\n// products still want a single composed integration hook; `focusFirstItemByTextPrefix` for\n// typeahead roving tabindex (callers focus `getActiveItem()` unless the platform adds an equivalent).\n// `isRtl()` may\n// duplicate or diverge from native axis mapping — revisit when testing RTL with native focusgroup.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class FocusgroupNavigationController implements ReactiveController {\n /**\n * Lit reactive host this controller is attached to.\n */\n private host: ReactiveElement;\n\n /**\n * Effective options (defaults merged with the latest `setOptions` / constructor values).\n */\n private options: FocusgroupNavigationOptions;\n\n /**\n * Capture-phase `keydown` listener reference for removal on disconnect.\n */\n private readonly boundKeydown = this.handleKeydown.bind(this);\n\n /**\n * Capture-phase `focusin` listener reference for removal on disconnect.\n */\n private readonly boundFocusin = this.handleFocusin.bind(this);\n\n /**\n * Capture-phase `focusout` listener reference for removal on disconnect.\n */\n private readonly boundFocusout = this.handleFocusout.bind(this);\n\n /**\n * Cached item for {@link FocusgroupNavigationOptions.memory} when the user moves focus\n * inside or out of the composite. Cleared when that node is no longer returned by\n * `getItems` or when the group becomes empty.\n */\n private lastFocused: HTMLElement | null = null;\n\n /**\n * Tracks the previously dispatched active item so that\n * {@link applyRovingTabindex} only fires the active-change event and\n * {@link FocusgroupNavigationOptions.onActiveItemChange} callback when the\n * active item actually changes.\n */\n private previousActive: HTMLElement | null = null;\n\n /**\n * Guard flag set during keyboard navigation so that the `focusin` triggered\n * by `item.focus()` does not redundantly call {@link applyRovingTabindex}.\n */\n private isNavigating = false;\n\n /**\n * Cached result of {@link getEligibleItems}, populated on first access within\n * a refresh cycle and cleared at the start of each entry point\n * ({@link refresh}, {@link handleFocusin}, {@link handleKeydown}).\n */\n private cachedEligibleItems: HTMLElement[] | null = null;\n\n /**\n * Cached result of {@link buildRows}, populated on first access within a\n * keydown cycle and cleared alongside {@link cachedEligibleItems}.\n */\n private cachedRows: HTMLElement[][] | null = null;\n\n // ─────────────────────────\n // PUBLIC API\n // ─────────────────────────\n\n /**\n * Registers this instance on `host` via `addController` and merges `options` with defaults.\n *\n * @param host - Reactive element that owns the composite (arrow keys and tab order apply within its subtree).\n * @param options - `getItems`, `direction`, and optional behavior flags.\n */\n constructor(host: ReactiveElement, options: FocusgroupNavigationOptions) {\n this.host = host;\n this.options = { ...DEFAULT_OPTIONS, ...options };\n host.addController(this);\n }\n\n /**\n * Merges `partial` into the current options and reapplies roving `tabindex` to the item set.\n *\n * @param partial - Fields to override; omitted keys keep their previous values.\n */\n public setOptions(partial: Partial<FocusgroupNavigationOptions>): void {\n this.options = { ...this.options, ...partial };\n this.refresh();\n }\n\n /**\n * Returns the eligible managed item that currently participates in the sequential focus order\n * (`tabindex=\"0\"`), or null if no eligible item has tab index zero.\n *\n * @returns The active roving item, or null.\n */\n public getActiveItem(): HTMLElement | null {\n for (const el of this.getEligibleItems()) {\n if (el.tabIndex === 0) {\n return el;\n }\n }\n return null;\n }\n\n /**\n * Re-queries `getItems()`, recomputes eligibility, and syncs roving `tabindex`.\n *\n * Call after the item list or item eligibility changes (for example after Lit\n * `updated()` or slot changes). When {@link FocusgroupNavigationOptions.memory} is true,\n * prefers the stored last-focused item if it is still eligible; otherwise keeps the\n * current active item or falls back to the first eligible item.\n */\n public refresh(): void {\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n if (items.length === 0) {\n for (const el of this.getRawItems()) {\n el.tabIndex = -1;\n }\n this.lastFocused = null;\n if (this.previousActive !== null) {\n this.previousActive = null;\n this.dispatchActiveChange(null, 'refresh');\n this.options.onActiveItemChange?.(null);\n }\n return;\n }\n\n const preferred =\n (this.options.memory &&\n this.lastFocused &&\n items.includes(this.lastFocused)\n ? this.lastFocused\n : null) ??\n this.getActiveItem() ??\n items[0];\n\n this.applyRovingTabindex(preferred, 'refresh');\n }\n\n /**\n * Sets roving `tabindex` so `item` is the active tab stop (`tabindex=\"0\"`) and others in the\n * group are `-1`. Does **not** call `focus()`. When {@link FocusgroupNavigationOptions.memory}\n * is true, updates the stored last-focused item so Tab re-entry can target this item.\n *\n * @param item - Item to mark active; must be returned by `getItems` and pass eligibility checks.\n * @returns False if `item` is not in the current eligible item list.\n */\n public setActiveItem(item: HTMLElement): boolean {\n const items = this.getEligibleItems();\n if (!items.includes(item)) {\n return false;\n }\n this.applyRovingTabindex(item, 'programmatic');\n if (this.options.memory) {\n this.lastFocused = item;\n }\n return true;\n }\n\n /**\n * Updates roving `tabindex` so the first **eligible** item (same set as arrow navigation)\n * whose typeahead label starts with `prefix` becomes the active tab stop (`tabindex=\"0\"`).\n * Matching is **case-insensitive**. The label is the first non-empty of: trimmed\n * **`aria-label`**, trimmed text from **`aria-labelledby`** references (in order, space-joined),\n * or trimmed **`textContent`**. Search order matches arrow-key traversal.\n *\n * Does **not** call `focus()`. After this returns `true`, call `focus()` on\n * {@link FocusgroupNavigationController.getActiveItem} (for example `getActiveItem()?.focus()`),\n * often from a **microtask** when the caller runs from a pointer handler so focus is not\n * overwritten by the clicked control.\n *\n * Typical use: menu typeahead; wire `keydown` or `input` at the host and debounce as needed.\n *\n * @param prefix - String to match as a leading substring after `trim`; whitespace-only yields\n * no match and returns `false`.\n * @returns True if a matching item was found and roving tabindex was applied.\n */\n public focusFirstItemByTextPrefix(prefix: string): boolean {\n const trimmed = prefix.trim();\n if (trimmed === '') {\n return false;\n }\n const needle = trimmed.toLowerCase();\n const items = this.getEligibleItems();\n const match = items.find((el) => {\n const label = this.getItemTypeaheadLabel(el).toLowerCase();\n return label.startsWith(needle);\n });\n if (!match) {\n return false;\n }\n this.applyRovingTabindex(match, 'programmatic');\n return true;\n }\n\n /**\n * Lit `ReactiveController` hook: registers capture-phase listeners on `host` and runs\n * an initial {@link refresh}.\n */\n public hostConnected(): void {\n this.previousActive = null;\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n this.host.addEventListener('keydown', this.boundKeydown, true);\n this.host.addEventListener('focusin', this.boundFocusin, true);\n this.host.addEventListener('focusout', this.boundFocusout, true);\n this.refresh();\n }\n\n /**\n * Lit `ReactiveController` hook: removes listeners registered in {@link hostConnected}.\n */\n public hostDisconnected(): void {\n this.host.removeEventListener('keydown', this.boundKeydown, true);\n this.host.removeEventListener('focusin', this.boundFocusin, true);\n this.host.removeEventListener('focusout', this.boundFocusout, true);\n }\n\n // ─────────────────────────\n // IMPLEMENTATION\n // ─────────────────────────\n //\n // Which parts may become redundant under native `focusgroup` is summarized in the\n // “Native `focusgroup` (future)” comment block directly above the class declaration.\n\n /**\n * Resolves writing direction from the computed style of the host element.\n *\n * Uses `getComputedStyle` rather than walking `dir` attributes so that\n * CSS-inherited direction (the gen2 default) is correctly detected.\n *\n * @returns True when horizontal arrow directions should follow RTL semantics.\n */\n private isRtl(): boolean {\n return getComputedStyle(this.host).direction === 'rtl';\n }\n\n /**\n * Whether `node` is the host or reachable from it by walking `parentNode` and\n * `ShadowRoot.host` (so shadow descendants count, including nested shadow roots).\n *\n * `Element.contains()` is not used because it returns false for nodes inside the\n * host's shadow tree, which would drop every item for typical Lit components.\n *\n * @param node - Node to test (may be null).\n * @returns True if `node` is in the host's shadow-inclusive subtree.\n */\n private isNodeWithinHostScope(node: Node | null): boolean {\n if (!node) {\n return false;\n }\n const host = this.host;\n let current: Node | null = node;\n while (current) {\n if (current === host) {\n return true;\n }\n const parent: Node | null = current.parentNode;\n if (parent) {\n current = parent;\n } else if (current instanceof ShadowRoot) {\n current = current.host;\n } else {\n return false;\n }\n }\n return false;\n }\n\n /**\n * Items returned by `getItems` that lie within `host` (shadow-inclusive tree).\n *\n * @returns Candidates before eligibility filtering.\n */\n private getRawItems(): HTMLElement[] {\n return this.options\n .getItems()\n .filter((el) => this.isNodeWithinHostScope(el));\n }\n\n /**\n * {@link getRawItems} filtered by {@link isNavigableItem}.\n *\n * @returns Items that participate in roving tabindex and arrow navigation.\n */\n private getEligibleItems(): HTMLElement[] {\n if (this.cachedEligibleItems) {\n return this.cachedEligibleItems;\n }\n this.cachedEligibleItems = this.getRawItems().filter((el) =>\n this.isNavigableItem(el)\n );\n return this.cachedEligibleItems;\n }\n\n /**\n * {@link buildRows} with per-cycle caching, cleared alongside\n * {@link cachedEligibleItems}.\n *\n * @param items - Eligible items to lay out as a grid.\n * @returns Cached row-major array of rows.\n */\n private getRows(items: HTMLElement[]): HTMLElement[][] {\n if (this.cachedRows) {\n return this.cachedRows;\n }\n this.cachedRows = this.buildRows(items);\n return this.cachedRows;\n }\n\n /**\n * Whether `el` may participate in the focus group (connected, visible, not inert,\n * and not skipped when {@link FocusgroupNavigationOptions.skipDisabled} is true).\n *\n * @param el - Candidate from `getItems`.\n * @returns True if the element counts as navigable for this controller.\n */\n private isNavigableItem(el: HTMLElement): boolean {\n if (!el.isConnected) {\n return false;\n }\n if (el.hasAttribute('inert') || el.closest('[inert]')) {\n return false;\n }\n const style = getComputedStyle(el);\n if (style.visibility === 'hidden' || style.display === 'none') {\n return false;\n }\n if (this.options.skipDisabled && this.isDisabledForSkip(el)) {\n return false;\n }\n return true;\n }\n\n /**\n * Whether `el` should be treated as disabled for {@link FocusgroupNavigationOptions.skipDisabled}.\n *\n * @param el - Element to test.\n * @returns True if the native `disabled` property is true or `aria-disabled` is `\"true\"`.\n */\n private isDisabledForSkip(el: HTMLElement): boolean {\n if ('disabled' in el && (el as HTMLButtonElement).disabled) {\n return true;\n }\n return el.getAttribute('aria-disabled') === 'true';\n }\n\n /**\n * String used for {@link focusFirstItemByTextPrefix}: prefers **`aria-label`**, then text from\n * **`aria-labelledby`** (IDs resolved in the shadow root or document), else **`textContent`**.\n * All branches are trimmed; empty strings fall through to the next source.\n */\n private getItemTypeaheadLabel(el: HTMLElement): string {\n const fromAria = el.getAttribute('aria-label')?.trim();\n if (fromAria) {\n return fromAria;\n }\n const labelledBy = el.getAttribute('aria-labelledby')?.trim();\n if (labelledBy) {\n const root = el.getRootNode();\n const chunks: string[] = [];\n for (const id of labelledBy.split(/\\s+/)) {\n if (!id) {\n continue;\n }\n const ref =\n root instanceof ShadowRoot\n ? (root.getElementById(id) ?? el.ownerDocument.getElementById(id))\n : el.ownerDocument.getElementById(id);\n const t = ref?.textContent?.trim();\n if (t) {\n chunks.push(t);\n }\n }\n const joined = chunks.join(' ').trim();\n if (joined) {\n return joined;\n }\n }\n return el.textContent?.trim() ?? '';\n }\n\n /**\n * Whether `el` is natively disabled and therefore unable to receive focus\n * regardless of its `tabindex` value.\n */\n private isNativelyDisabled(el: HTMLElement): boolean {\n return 'disabled' in el && (el as HTMLButtonElement).disabled === true;\n }\n\n /**\n * Sets `tabindex=\"-1\"` on ineligible raw items, then assigns `tabindex=\"0\"` to\n * `active` (or the first eligible item if `active` is not eligible) and `-1` to the rest.\n *\n * When `skipDisabled` is false, natively disabled items remain in the eligible list\n * for arrow navigation but are never chosen as the roving tab stop because they\n * cannot receive focus. The tab stop falls through to the nearest non-disabled item.\n *\n * Dispatches the active-change event and {@link FocusgroupNavigationOptions.onActiveItemChange}.\n *\n * @param active - Preferred item to mark as the single tab stop when eligible.\n * @param source - Why the active item is changing; included in the dispatched event detail.\n */\n private applyRovingTabindex(\n active: HTMLElement,\n source: FocusgroupActiveChangeSource\n ): void {\n const items = this.getEligibleItems();\n const eligibleSet = new Set(items);\n for (const el of this.getRawItems()) {\n if (!eligibleSet.has(el)) {\n el.tabIndex = -1;\n }\n }\n if (items.length === 0) {\n return;\n }\n\n let safeActive = eligibleSet.has(active) ? active : items[0];\n\n // Natively disabled elements cannot receive focus even with tabindex=\"0\".\n // Fall through to the first non-disabled eligible item so the group\n // remains reachable via Tab.\n //\n // The active-change event is dispatched with the originally requested item\n // (before the fallback), not the fallback tab-stop. This lets consumers\n // such as a tab list in automatic-activation mode inspect the item and skip\n // the selection change when it is disabled — without being misled by the\n // roving tab stop landing on a different element.\n const reportedActive = safeActive;\n if (this.isNativelyDisabled(safeActive)) {\n safeActive =\n items.find((el) => !this.isNativelyDisabled(el)) ?? safeActive;\n }\n\n for (const el of items) {\n if (el === safeActive) {\n el.tabIndex = 0;\n } else {\n el.tabIndex = -1;\n }\n }\n if (reportedActive !== this.previousActive) {\n this.previousActive = reportedActive;\n this.dispatchActiveChange(reportedActive, source);\n this.options.onActiveItemChange?.(safeActive);\n }\n }\n\n /**\n * Dispatches {@link focusgroupNavigationActiveChange} on the reactive host with the given detail.\n *\n * @param activeElement - New active item, or null when clearing selection.\n * @param source - Why the active item changed.\n */\n private dispatchActiveChange(\n activeElement: HTMLElement | null,\n source: FocusgroupActiveChangeSource\n ): void {\n this.host.dispatchEvent(\n new CustomEvent<FocusgroupNavigationActiveChangeDetail>(\n focusgroupNavigationActiveChange,\n {\n bubbles: true,\n composed: true,\n detail: { activeElement, source },\n }\n )\n );\n }\n\n /**\n * Resolves the managed item that actually received focus inside the shadow tree.\n *\n * Same retargeting problem as {@link resolveManagedKeydownTarget}: listeners on\n * the shadow host see `event.target` retargeted to the host when focus lands on a\n * descendant inside the shadow root. Walk `composedPath()` and fall back to\n * `shadowRoot.activeElement` to find the real focused managed item.\n *\n * @param event - Focus event dispatched while focus moves into the composite.\n * @param items - Current eligible items from {@link getEligibleItems}.\n * @returns The managed element that received focus, or null.\n */\n private resolveManagedFocusTarget(\n event: FocusEvent,\n items: HTMLElement[]\n ): HTMLElement | null {\n if (items.length === 0) {\n return null;\n }\n const set = new Set(items);\n for (const node of event.composedPath()) {\n if (!(node instanceof HTMLElement)) {\n continue;\n }\n if (set.has(node)) {\n return node;\n }\n if (node === this.host) {\n break;\n }\n }\n const root = this.host.shadowRoot;\n const active = root?.activeElement;\n if (active instanceof HTMLElement && set.has(active)) {\n return active;\n }\n return null;\n }\n\n /**\n * Capture-phase `focusin` handler: syncs roving `tabindex` when focus moves to a managed item\n * (for example via pointer), and updates memory when enabled.\n *\n * @param event - Focus event whose target may be a group item.\n */\n private handleFocusin(event: FocusEvent): void {\n if (this.isNavigating) {\n return;\n }\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n const target = this.resolveManagedFocusTarget(event, items);\n if (!target) {\n return;\n }\n this.applyRovingTabindex(target, 'focus');\n if (this.options.memory) {\n this.lastFocused = target;\n }\n }\n\n /**\n * Capture-phase `focusout` handler: when focus leaves the host subtree, stores the\n * previous target for {@link FocusgroupNavigationOptions.memory}.\n *\n * @param event - Focus event; `relatedTarget` stays inside the host when moving between items.\n */\n private handleFocusout(event: FocusEvent): void {\n const next = event.relatedTarget;\n if (next instanceof Node && this.isNodeWithinHostScope(next)) {\n return;\n }\n const target = event.target;\n if (\n this.options.memory &&\n target instanceof HTMLElement &&\n this.getRawItems().includes(target)\n ) {\n this.lastFocused = target;\n }\n // When memory is off, reset the roving tab stop to the first eligible\n // item so Tab re-entry always starts from the beginning.\n if (!this.options.memory) {\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n if (items.length > 0) {\n this.applyRovingTabindex(items[0], 'focus');\n }\n }\n }\n\n /**\n * Resolves which managed item should receive arrow, Home, End, or grid Ctrl+Home / Ctrl+End\n * handling for this key event.\n *\n * Listeners on the shadow **host** often see a **retargeted** {@link KeyboardEvent.target}\n * (the host) while focus is on a descendant inside the shadow tree, so matching\n * `event.target` against `getItems()` fails. {@link Event.composedPath} still includes the\n * focused node; we also fall back to {@link ShadowRoot.activeElement} when needed.\n *\n * @param event - Keyboard event dispatched while focus is in this composite.\n * @param items - Current eligible items from {@link getEligibleItems}.\n * @returns The managed element to treat as keydown target, or null.\n */\n private resolveManagedKeydownTarget(\n event: KeyboardEvent,\n items: HTMLElement[]\n ): HTMLElement | null {\n if (items.length === 0) {\n return null;\n }\n const set = new Set(items);\n for (const node of event.composedPath()) {\n if (!(node instanceof HTMLElement)) {\n continue;\n }\n if (set.has(node)) {\n return node;\n }\n if (node === this.host) {\n break;\n }\n }\n const root = this.host.shadowRoot;\n const active = root?.activeElement;\n if (active instanceof HTMLElement && set.has(active)) {\n return active;\n }\n return null;\n }\n\n /**\n * Capture-phase `keydown` handler: arrow keys and Home/End move focus among eligible items\n * when the event target is managed; calls `preventDefault` when handling navigation.\n *\n * When {@link FocusgroupDirection | `direction`} is **`both`**, **ArrowLeft** / **ArrowRight**\n * and **ArrowUp** / **ArrowDown** all participate (see {@link navigateBothAxes}).\n *\n * When {@link FocusgroupDirection | `direction`} is **`grid`**, **Ctrl+Home** focuses the\n * first cell in the first row and **Ctrl+End** focuses the last cell in the last row (from\n * {@link buildRows}); other modifier combinations are ignored except plain Home/End.\n *\n * When {@link FocusgroupNavigationOptions.pageStep} is a non-zero finite number, **Page Up**\n * and **Page Down** are handled before arrow keys (see {@link navigatePage}).\n *\n * @param event - Keyboard event from the focused element inside the host.\n */\n private handleKeydown(event: KeyboardEvent): void {\n if (event.defaultPrevented || event.altKey) {\n return;\n }\n\n this.cachedEligibleItems = null;\n this.cachedRows = null;\n const items = this.getEligibleItems();\n const target = this.resolveManagedKeydownTarget(event, items);\n if (!target) {\n return;\n }\n\n const isGrid = this.options.direction === 'grid';\n const rows = isGrid ? this.getRows(items) : null;\n\n if (\n isGrid &&\n event.ctrlKey &&\n !event.metaKey &&\n (event.key === 'Home' || event.key === 'End')\n ) {\n if (rows!.length > 0) {\n const firstRow = rows![0];\n const lastRow = rows![rows!.length - 1];\n const boundary =\n event.key === 'Home'\n ? (firstRow?.[0] ?? null)\n : (lastRow?.[lastRow.length - 1] ?? null);\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n }\n return;\n }\n\n if (event.ctrlKey || event.metaKey) {\n return;\n }\n\n const pageMagnitude = this.getEffectivePageMagnitude();\n if (\n pageMagnitude !== null &&\n (event.key === 'PageUp' || event.key === 'PageDown')\n ) {\n const pageNext = this.navigatePage(\n items,\n target,\n event.key === 'PageDown' ? pageMagnitude : -pageMagnitude,\n rows\n );\n if (pageNext && pageNext !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(pageNext);\n }\n return;\n }\n\n const rtl = this.isRtl();\n let next: HTMLElement | null = null;\n\n switch (this.options.direction) {\n case 'horizontal':\n next = this.navigateLinear(items, target, event.key, 'horizontal', rtl);\n break;\n case 'vertical':\n next = this.navigateLinear(items, target, event.key, 'vertical', rtl);\n break;\n case 'both':\n next = this.navigateBothAxes(items, target, event.key, rtl);\n break;\n case 'grid':\n next = this.navigateGrid(target, event.key, rtl, rows!);\n break;\n default:\n break;\n }\n\n if (next && next !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(next);\n return;\n }\n\n if (event.key === 'Home' || event.key === 'End') {\n if (isGrid) {\n // APG grid pattern: Home/End scope to the current row.\n // Ctrl+Home/End (entire grid) is handled above.\n const pos = this.findGridIndex(rows!, target);\n if (!pos) {\n return;\n }\n const currentRow = rows![pos.row];\n if (!currentRow?.length) {\n return;\n }\n const boundary =\n event.key === 'Home'\n ? currentRow[0]\n : currentRow[currentRow.length - 1];\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n } else {\n if (items.length === 0) {\n return;\n }\n const boundary =\n event.key === 'Home' ? items[0] : items[items.length - 1];\n if (boundary && boundary !== target) {\n event.preventDefault();\n this.moveKeyNavigationFocusTo(boundary);\n }\n }\n }\n }\n\n /**\n * Applies roving tabindex to `item` and moves DOM focus; used for keyboard navigation only.\n */\n private moveKeyNavigationFocusTo(item: HTMLElement): void {\n this.isNavigating = true;\n try {\n const items = this.getEligibleItems();\n if (items.includes(item)) {\n this.applyRovingTabindex(item, 'keyboard');\n if (this.options.memory) {\n this.lastFocused = item;\n }\n item.focus();\n }\n } finally {\n this.isNavigating = false;\n }\n }\n\n /**\n * Positive step count for {@link FocusgroupNavigationOptions.pageStep}, or null when page keys\n * are disabled.\n */\n private getEffectivePageMagnitude(): number | null {\n const raw = this.options.pageStep;\n if (raw === undefined) {\n return null;\n }\n const n = Math.trunc(Number(raw));\n if (!Number.isFinite(n) || n === 0) {\n return null;\n }\n return Math.abs(n);\n }\n\n /**\n * Target for **Page Up** / **Page Down** when {@link getEffectivePageMagnitude} is set.\n *\n * @param items - Eligible items.\n * @param current - Focused item.\n * @param signedDelta - `+magnitude` for Page Down or `-magnitude` for Page Up (items for\n * linear modes, rows for `grid`).\n */\n private navigatePage(\n items: HTMLElement[],\n current: HTMLElement,\n signedDelta: number,\n rows: HTMLElement[][] | null\n ): HTMLElement | null {\n if (this.options.direction === 'grid') {\n return this.navigatePageGridRows(current, signedDelta, rows!);\n }\n return this.navigatePageLinearItems(items, current, signedDelta);\n }\n\n /**\n * Page Up/Down along `getItems()` order (used for `horizontal`, `vertical`, and `both`).\n */\n private navigatePageLinearItems(\n items: HTMLElement[],\n current: HTMLElement,\n deltaIdx: number\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0 || items.length === 0) {\n return null;\n }\n let nextIdx = idx + deltaIdx;\n if (this.options.wrap) {\n const len = items.length;\n nextIdx = ((nextIdx % len) + len) % len;\n } else {\n nextIdx = Math.max(0, Math.min(items.length - 1, nextIdx));\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Page Up/Down by whole rows in `grid` mode (column clamped per {@link navigateGrid}).\n */\n private navigatePageGridRows(\n current: HTMLElement,\n rowDelta: number,\n grid: HTMLElement[][]\n ): HTMLElement | null {\n if (grid.length === 0) {\n return null;\n }\n const pos = this.findGridIndex(grid, current);\n if (!pos) {\n return null;\n }\n const { row, col } = pos;\n let nextRow = row + rowDelta;\n if (this.options.wrap) {\n const n = grid.length;\n nextRow = ((nextRow % n) + n) % n;\n } else {\n nextRow = Math.max(0, Math.min(grid.length - 1, nextRow));\n }\n const targetRow = grid[nextRow];\n if (!targetRow?.length) {\n return null;\n }\n const clampedCol = Math.min(col, targetRow.length - 1);\n return targetRow[clampedCol] ?? null;\n }\n\n /**\n * Computes the next focus target for linear {@link FocusgroupDirection} modes.\n *\n * @param items - Eligible items in traversal order.\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param mode - `horizontal` (inline axis) or `vertical` (block axis).\n * @param rtl - When true, horizontal Left/Right swap forward/backward.\n * @returns Next item, or null if the key is not a navigation key or movement is blocked.\n */\n private navigateLinear(\n items: HTMLElement[],\n current: HTMLElement,\n key: string,\n mode: 'horizontal' | 'vertical',\n rtl: boolean\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0) {\n return null;\n }\n\n let delta = 0;\n if (mode === 'horizontal') {\n if (key === 'ArrowLeft') {\n delta = rtl ? 1 : -1;\n } else if (key === 'ArrowRight') {\n delta = rtl ? -1 : 1;\n }\n } else {\n if (key === 'ArrowUp') {\n delta = -1;\n } else if (key === 'ArrowDown') {\n delta = 1;\n }\n }\n\n if (delta === 0) {\n return null;\n }\n\n let nextIdx = idx + delta;\n if (this.options.wrap) {\n nextIdx = (nextIdx + items.length) % items.length;\n } else if (nextIdx < 0 || nextIdx >= items.length) {\n return null;\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Computes the next focus target when {@link FocusgroupDirection | `direction`} is **`both`**:\n * inline arrows use the same deltas as {@link navigateLinear} `horizontal` mode; **ArrowUp** /\n * **ArrowDown** step backward / forward in `getItems()` order (not flipped by `dir`).\n *\n * @param items - Eligible items in traversal order.\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param rtl - When true, horizontal Left/Right swap forward/backward.\n * @returns Next item, or null if the key is not handled or movement is blocked.\n */\n private navigateBothAxes(\n items: HTMLElement[],\n current: HTMLElement,\n key: string,\n rtl: boolean\n ): HTMLElement | null {\n const idx = items.indexOf(current);\n if (idx < 0) {\n return null;\n }\n\n let delta = 0;\n if (key === 'ArrowLeft') {\n delta = rtl ? 1 : -1;\n } else if (key === 'ArrowRight') {\n delta = rtl ? -1 : 1;\n } else if (key === 'ArrowUp') {\n delta = -1;\n } else if (key === 'ArrowDown') {\n delta = 1;\n }\n\n if (delta === 0) {\n return null;\n }\n\n let nextIdx = idx + delta;\n if (this.options.wrap) {\n nextIdx = (nextIdx + items.length) % items.length;\n } else if (nextIdx < 0 || nextIdx >= items.length) {\n return null;\n }\n return items[nextIdx] ?? null;\n }\n\n /**\n * Computes the next focus target for `grid` {@link FocusgroupDirection} mode using\n * row clustering and column indices.\n *\n * @param current - Currently focused item.\n * @param key - `KeyboardEvent.key` value.\n * @param rtl - When true, horizontal Left/Right swap column direction within a row.\n * @param grid - Pre-built row grid from {@link buildRows}.\n * @returns Next cell item, or null if the key is not handled or movement is blocked.\n */\n private navigateGrid(\n current: HTMLElement,\n key: string,\n rtl: boolean,\n grid: HTMLElement[][]\n ): HTMLElement | null {\n const pos = this.findGridIndex(grid, current);\n if (!pos) {\n return null;\n }\n const { row, col } = pos;\n const rowItems = grid[row] ?? [];\n let nextRow = row;\n let nextCol = col;\n\n switch (key) {\n case 'ArrowLeft':\n nextCol = rtl ? col + 1 : col - 1;\n break;\n case 'ArrowRight':\n nextCol = rtl ? col - 1 : col + 1;\n break;\n case 'ArrowUp':\n nextRow = row - 1;\n break;\n case 'ArrowDown':\n nextRow = row + 1;\n break;\n default:\n return null;\n }\n\n if (key === 'ArrowLeft' || key === 'ArrowRight') {\n if (nextCol >= 0 && nextCol < rowItems.length) {\n return rowItems[nextCol] ?? null;\n }\n if (this.options.wrap && rowItems.length > 0) {\n const wrappedCol = (nextCol + rowItems.length) % rowItems.length;\n return rowItems[wrappedCol] ?? null;\n }\n return null;\n }\n\n if (nextRow < 0 || nextRow >= grid.length) {\n if (this.options.wrap && grid.length > 0) {\n nextRow = (nextRow + grid.length) % grid.length;\n } else {\n return null;\n }\n }\n\n const targetRow = grid[nextRow];\n if (!targetRow?.length) {\n return null;\n }\n const clampedCol = Math.min(col, targetRow.length - 1);\n return targetRow[clampedCol] ?? null;\n }\n\n /**\n * Groups `items` into rows by similar `getBoundingClientRect().top`, then sorts each row by `left`.\n *\n * @param items - Eligible elements to lay out as a grid.\n * @returns Row-major array of rows; each row is left-to-right.\n */\n private buildRows(items: HTMLElement[]): HTMLElement[][] {\n type RowAcc = { top: number; elements: HTMLElement[] };\n const rows: RowAcc[] = [];\n\n for (const el of items) {\n const top = el.getBoundingClientRect().top;\n let row = rows.find(\n (r) => Math.abs(r.top - top) <= GRID_ROW_TOLERANCE_PX\n );\n if (!row) {\n row = { top, elements: [] };\n rows.push(row);\n }\n row.elements.push(el);\n }\n\n rows.sort((a, b) => a.top - b.top);\n return rows.map((r) =>\n r.elements.sort(\n (a, b) =>\n a.getBoundingClientRect().left - b.getBoundingClientRect().left\n )\n );\n }\n\n /**\n * Locates `el` in a row-major grid built by {@link buildRows}.\n *\n * @param grid - Rows of elements.\n * @param el - Element to find.\n * @returns Row and column indices, or null if absent.\n */\n private findGridIndex(\n grid: HTMLElement[][],\n el: HTMLElement\n ): { row: number; col: number } | null {\n for (let r = 0; r < grid.length; r++) {\n const c = grid[r].indexOf(el);\n if (c !== -1) {\n return { row: r, col: c };\n }\n }\n return null;\n }\n}\n"],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  /**
13
- * Public exports for Lit reactive controllers shared across 2nd-gen packages.
13
+ * Public exports for Lit reactive controllers shared across gen2 packages.
14
14
  */
15
15
  export { ColorController, type Color, type ColorTypes, } from './color-controller/index.js';
16
16
  export { DragAndDropController, type DragAndDropControllerOptions, type DragLeaveSnapshot, } from './drag-and-drop-controller/index.js';
@@ -24,3 +24,4 @@ export { ALL_PLACEMENTS, fromFloatingPlacement, PlacementController, toFloatingP
24
24
  export { SlotAttributePropagationController, type SlotAttributePropagationControllerOptions, } from './slot-attribute-propagation-controller/index.js';
25
25
  export { SlotPresenceController } from './slot-presence-controller/index.js';
26
26
  export { SlotTextController, type SlotTextConfig, } from './slot-text-controller/index.js';
27
+ export { TriggerPressController, type TriggerPressControllerOptions, } from './trigger-press-controller/index.js';
@@ -15,4 +15,5 @@ import "./color-controller/index.js";
15
15
  import { FocusgroupNavigationController as p, focusgroupNavigationActiveChange as m } from "./focusgroup-navigation-controller/src/focusgroup-navigation-controller.js";
16
16
  import { LiveSelectionController as h } from "./live-selection-controller/src/live-selection-controller.js";
17
17
  import { PageScrollLockController as g } from "./page-scroll-lock-controller/src/page-scroll-lock.js";
18
- export { o as ALL_PLACEMENTS, f as ColorController, n as DragAndDropController, p as FocusgroupNavigationController, r as HoverController, c as LanguageResolutionController, h as LiveSelectionController, g as PageScrollLockController, d as PendingController, s as PlacementController, e as SlotAttributePropagationController, u as SlotPresenceController, t as SlotTextController, m as focusgroupNavigationActiveChange, i as fromFloatingPlacement, l as languageResolverUpdatedSymbol, a as toFloatingPlacement };
18
+ import { TriggerPressController as _ } from "./trigger-press-controller/src/trigger-press-controller.js";
19
+ export { o as ALL_PLACEMENTS, f as ColorController, n as DragAndDropController, p as FocusgroupNavigationController, r as HoverController, c as LanguageResolutionController, h as LiveSelectionController, g as PageScrollLockController, d as PendingController, s as PlacementController, e as SlotAttributePropagationController, u as SlotPresenceController, t as SlotTextController, _ as TriggerPressController, m as focusgroupNavigationActiveChange, i as fromFloatingPlacement, l as languageResolverUpdatedSymbol, a as toFloatingPlacement };
@@ -28,7 +28,7 @@ export declare const languageResolverUpdatedSymbol: unique symbol;
28
28
  * the controller updates and the host re-renders (e.g. aria-valuetext reformats)
29
29
  * - Validates locale support using Intl API and falls back to `'en-US'` if unsupported
30
30
  *
31
- * In 2nd-gen there is no sp-theme language provider, so live updates come from
31
+ * In gen2 there is no sp-theme language provider, so live updates come from
32
32
  * `<html lang>` changes. Apps that support locale switching should set
33
33
  * `document.documentElement.lang` when the locale changes; the controller will
34
34
  * pick it up and trigger updates.
@@ -1 +1 @@
1
- {"version":3,"file":"language-resolution.js","names":[],"sources":["../../controllers/language-resolution.ts"],"sourcesContent":["/**\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport type { ReactiveController, ReactiveElement } from 'lit';\n\n// TODO: Update this when theme is migrated to 2nd-gen\ntype ProvideLang = {\n callback: (lang: string, unsubscribe: () => void) => void;\n};\n\n/**\n * Symbol used to track language resolver updates in reactive element lifecycle.\n * When the language context changes, components use this symbol to trigger updates\n * to locale-dependent content (e.g., formatted dates, numbers, currency).\n *\n * @example\n * ```typescript\n * protected override updated(changes: PropertyValues): void {\n * if (changes.has(languageResolverUpdatedSymbol)) {\n * // Re-render locale-dependent content\n * this.setAttribute('aria-valuetext', this.formatProgress());\n * }\n * }\n * ```\n */\nexport const languageResolverUpdatedSymbol = Symbol(\n 'language resolver updated'\n);\n\n// ────────────────────────────────────────────\n// Shared <html lang> observer (singleton)\n// ────────────────────────────────────────────\n//\n// Instead of each controller instance creating its own MutationObserver on\n// document.documentElement, a single module-scoped observer fans out to every\n// registered callback. The observer is created lazily when the first controller\n// connects and torn down automatically when the last one disconnects.\n\ntype LangChangeListener = () => void;\n\nconst listeners = new Set<LangChangeListener>();\nlet sharedObserver: MutationObserver | undefined;\n\nfunction addLangListener(listener: LangChangeListener): () => void {\n listeners.add(listener);\n\n if (!sharedObserver) {\n sharedObserver = new MutationObserver(() => {\n for (const cb of listeners) {\n cb();\n }\n });\n sharedObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['lang'],\n });\n }\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n sharedObserver?.disconnect();\n sharedObserver = undefined;\n }\n };\n}\n\n/**\n * A reactive controller that manages language/locale resolution for components.\n *\n * This controller:\n * - Gets initial language from `<html lang>`, then `navigator.language`, then `'en-US'`\n * - Optionally subscribes to a provider (e.g. 1st-gen `<sp-theme>`) via the\n * `sp-language-context` event; if something up the tree handles it and calls the\n * callback, that becomes the source of truth for live updates\n * - Observes `<html lang>` attribute changes via a shared singleton observer so that\n * when the document language changes at runtime (e.g. app-level locale switching),\n * the controller updates and the host re-renders (e.g. aria-valuetext reformats)\n * - Validates locale support using Intl API and falls back to `'en-US'` if unsupported\n *\n * In 2nd-gen there is no sp-theme language provider, so live updates come from\n * `<html lang>` changes. Apps that support locale switching should set\n * `document.documentElement.lang` when the locale changes; the controller will\n * pick it up and trigger updates.\n *\n * Components using this controller can access the current language via the `language`\n * property and will automatically re-render when the language context changes.\n *\n * @example\n * ```typescript\n * class MyComponent extends SpectrumElement {\n * private languageResolver = new LanguageResolutionController(this);\n *\n * protected override updated(changes: PropertyValues): void {\n * if (changes.has(languageResolverUpdatedSymbol)) {\n * // Update locale-dependent formatting\n * this.formattedValue = new Intl.NumberFormat(\n * this.languageResolver.language\n * ).format(this.value);\n * }\n * }\n * }\n * ```\n */\nexport class LanguageResolutionController implements ReactiveController {\n private host: ReactiveElement;\n\n /**\n * The currently resolved language/locale code (e.g., 'en-US', 'fr-FR').\n * Defaults to document language, browser language, or 'en-US'.\n */\n language = this.getDocumentLanguage();\n\n /** Unsubscribe from the sp-language-context provider (if any). */\n private unsubscribe?: () => void;\n\n /** Unsubscribe from the shared <html lang> observer. */\n private removeLangListener?: () => void;\n\n constructor(host: ReactiveElement) {\n this.host = host;\n this.host.addController(this);\n }\n\n /**\n * Reads language from document and validates. Used for initial value and\n * when syncing from `<html lang>` changes.\n */\n private getDocumentLanguage(): string {\n const raw = document.documentElement.lang || navigator.language || 'en-US';\n try {\n Intl.DateTimeFormat.supportedLocalesOf([raw]);\n return raw;\n } catch {\n return 'en-US';\n }\n }\n\n public hostConnected(): void {\n this.resolveLanguage();\n this.removeLangListener = addLangListener(this.handleLangChange.bind(this));\n }\n\n public hostDisconnected(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.removeLangListener?.();\n this.removeLangListener = undefined;\n }\n\n /**\n * Called by the shared observer when `<html lang>` changes.\n * Skipped when a provider (e.g. sp-theme) is the source of truth.\n */\n private handleLangChange(): void {\n if (this.unsubscribe) {\n return;\n }\n const next = this.getDocumentLanguage();\n if (next === this.language) {\n return;\n }\n const previous = this.language;\n this.language = next;\n this.host.requestUpdate(languageResolverUpdatedSymbol, previous);\n }\n\n /**\n * Resolves the language: syncs from document, then queries for a provider\n * (e.g. sp-theme) via 'sp-language-context'. If a provider calls the\n * callback, it becomes the source of truth until disconnected.\n *\n * @private\n */\n private resolveLanguage(): void {\n this.language = this.getDocumentLanguage();\n const queryThemeEvent = new CustomEvent<ProvideLang>(\n 'sp-language-context',\n {\n bubbles: true,\n composed: true,\n detail: {\n callback: (lang: string, unsubscribe: () => void) => {\n const previous = this.language;\n this.language = lang;\n this.unsubscribe = unsubscribe;\n this.host.requestUpdate(languageResolverUpdatedSymbol, previous);\n },\n },\n cancelable: true,\n }\n );\n this.host.dispatchEvent(queryThemeEvent);\n }\n}\n"],"mappings":";AAkCA,IAAa,IAAgC,OAC3C,4BACD,EAaK,oBAAY,IAAI,KAAyB,EAC3C;AAEJ,SAAS,EAAgB,GAA0C;AAejE,QAdA,EAAU,IAAI,EAAS,EAElB,MACH,IAAiB,IAAI,uBAAuB;AAC1C,OAAK,IAAM,KAAM,EACf,IAAI;GAEN,EACF,EAAe,QAAQ,SAAS,iBAAiB;EAC/C,YAAY;EACZ,iBAAiB,CAAC,OAAO;EAC1B,CAAC,SAGS;AAEX,EADA,EAAU,OAAO,EAAS,EACtB,EAAU,SAAS,MACrB,KAAA,QAAA,EAAgB,YAAY,EAC5B,IAAiB,KAAA;;;AA0CvB,IAAa,IAAb,MAAwE;CAetE,YAAY,GAAuB;AAEjC,kBAVS,KAAK,qBAAqB,EASnC,KAAK,OAAO,GACZ,KAAK,KAAK,cAAc,KAAK;;CAO/B,sBAAsC;EACpC,IAAM,IAAM,SAAS,gBAAgB,QAAQ,UAAU,YAAY;AACnE,MAAI;AAEF,UADA,KAAK,eAAe,mBAAmB,CAAC,EAAI,CAAC,EACtC;cACD;AACN,UAAO;;;CAIX,gBAA6B;AAE3B,EADA,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAAgB,KAAK,iBAAiB,KAAK,KAAK,CAAC;;CAG7E,mBAAgC;;AAI9B,GAHA,IAAA,KAAK,gBAAA,QAAA,EAAA,KAAA,KAAe,EACpB,KAAK,cAAc,KAAA,IACnB,IAAA,KAAK,uBAAA,QAAA,EAAA,KAAA,KAAsB,EAC3B,KAAK,qBAAqB,KAAA;;CAO5B,mBAAiC;AAC/B,MAAI,KAAK,YACP;EAEF,IAAM,IAAO,KAAK,qBAAqB;AACvC,MAAI,MAAS,KAAK,SAChB;EAEF,IAAM,IAAW,KAAK;AAEtB,EADA,KAAK,WAAW,GAChB,KAAK,KAAK,cAAc,GAA+B,EAAS;;CAUlE,kBAAgC;AAC9B,OAAK,WAAW,KAAK,qBAAqB;EAC1C,IAAM,IAAkB,IAAI,YAC1B,uBACA;GACE,SAAS;GACT,UAAU;GACV,QAAQ,EACN,WAAW,GAAc,MAA4B;IACnD,IAAM,IAAW,KAAK;AAGtB,IAFA,KAAK,WAAW,GAChB,KAAK,cAAc,GACnB,KAAK,KAAK,cAAc,GAA+B,EAAS;MAEnE;GACD,YAAY;GACb,CACF;AACD,OAAK,KAAK,cAAc,EAAgB"}
1
+ {"version":3,"file":"language-resolution.js","names":[],"sources":["../../controllers/language-resolution.ts"],"sourcesContent":["/**\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport type { ReactiveController, ReactiveElement } from 'lit';\n\n// TODO: Update this when theme is migrated to gen2\ntype ProvideLang = {\n callback: (lang: string, unsubscribe: () => void) => void;\n};\n\n/**\n * Symbol used to track language resolver updates in reactive element lifecycle.\n * When the language context changes, components use this symbol to trigger updates\n * to locale-dependent content (e.g., formatted dates, numbers, currency).\n *\n * @example\n * ```typescript\n * protected override updated(changes: PropertyValues): void {\n * if (changes.has(languageResolverUpdatedSymbol)) {\n * // Re-render locale-dependent content\n * this.setAttribute('aria-valuetext', this.formatProgress());\n * }\n * }\n * ```\n */\nexport const languageResolverUpdatedSymbol = Symbol(\n 'language resolver updated'\n);\n\n// ────────────────────────────────────────────\n// Shared <html lang> observer (singleton)\n// ────────────────────────────────────────────\n//\n// Instead of each controller instance creating its own MutationObserver on\n// document.documentElement, a single module-scoped observer fans out to every\n// registered callback. The observer is created lazily when the first controller\n// connects and torn down automatically when the last one disconnects.\n\ntype LangChangeListener = () => void;\n\nconst listeners = new Set<LangChangeListener>();\nlet sharedObserver: MutationObserver | undefined;\n\nfunction addLangListener(listener: LangChangeListener): () => void {\n listeners.add(listener);\n\n if (!sharedObserver) {\n sharedObserver = new MutationObserver(() => {\n for (const cb of listeners) {\n cb();\n }\n });\n sharedObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['lang'],\n });\n }\n\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n sharedObserver?.disconnect();\n sharedObserver = undefined;\n }\n };\n}\n\n/**\n * A reactive controller that manages language/locale resolution for components.\n *\n * This controller:\n * - Gets initial language from `<html lang>`, then `navigator.language`, then `'en-US'`\n * - Optionally subscribes to a provider (e.g. 1st-gen `<sp-theme>`) via the\n * `sp-language-context` event; if something up the tree handles it and calls the\n * callback, that becomes the source of truth for live updates\n * - Observes `<html lang>` attribute changes via a shared singleton observer so that\n * when the document language changes at runtime (e.g. app-level locale switching),\n * the controller updates and the host re-renders (e.g. aria-valuetext reformats)\n * - Validates locale support using Intl API and falls back to `'en-US'` if unsupported\n *\n * In gen2 there is no sp-theme language provider, so live updates come from\n * `<html lang>` changes. Apps that support locale switching should set\n * `document.documentElement.lang` when the locale changes; the controller will\n * pick it up and trigger updates.\n *\n * Components using this controller can access the current language via the `language`\n * property and will automatically re-render when the language context changes.\n *\n * @example\n * ```typescript\n * class MyComponent extends SpectrumElement {\n * private languageResolver = new LanguageResolutionController(this);\n *\n * protected override updated(changes: PropertyValues): void {\n * if (changes.has(languageResolverUpdatedSymbol)) {\n * // Update locale-dependent formatting\n * this.formattedValue = new Intl.NumberFormat(\n * this.languageResolver.language\n * ).format(this.value);\n * }\n * }\n * }\n * ```\n */\nexport class LanguageResolutionController implements ReactiveController {\n private host: ReactiveElement;\n\n /**\n * The currently resolved language/locale code (e.g., 'en-US', 'fr-FR').\n * Defaults to document language, browser language, or 'en-US'.\n */\n language = this.getDocumentLanguage();\n\n /** Unsubscribe from the sp-language-context provider (if any). */\n private unsubscribe?: () => void;\n\n /** Unsubscribe from the shared <html lang> observer. */\n private removeLangListener?: () => void;\n\n constructor(host: ReactiveElement) {\n this.host = host;\n this.host.addController(this);\n }\n\n /**\n * Reads language from document and validates. Used for initial value and\n * when syncing from `<html lang>` changes.\n */\n private getDocumentLanguage(): string {\n const raw = document.documentElement.lang || navigator.language || 'en-US';\n try {\n Intl.DateTimeFormat.supportedLocalesOf([raw]);\n return raw;\n } catch {\n return 'en-US';\n }\n }\n\n public hostConnected(): void {\n this.resolveLanguage();\n this.removeLangListener = addLangListener(this.handleLangChange.bind(this));\n }\n\n public hostDisconnected(): void {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.removeLangListener?.();\n this.removeLangListener = undefined;\n }\n\n /**\n * Called by the shared observer when `<html lang>` changes.\n * Skipped when a provider (e.g. sp-theme) is the source of truth.\n */\n private handleLangChange(): void {\n if (this.unsubscribe) {\n return;\n }\n const next = this.getDocumentLanguage();\n if (next === this.language) {\n return;\n }\n const previous = this.language;\n this.language = next;\n this.host.requestUpdate(languageResolverUpdatedSymbol, previous);\n }\n\n /**\n * Resolves the language: syncs from document, then queries for a provider\n * (e.g. sp-theme) via 'sp-language-context'. If a provider calls the\n * callback, it becomes the source of truth until disconnected.\n *\n * @private\n */\n private resolveLanguage(): void {\n this.language = this.getDocumentLanguage();\n const queryThemeEvent = new CustomEvent<ProvideLang>(\n 'sp-language-context',\n {\n bubbles: true,\n composed: true,\n detail: {\n callback: (lang: string, unsubscribe: () => void) => {\n const previous = this.language;\n this.language = lang;\n this.unsubscribe = unsubscribe;\n this.host.requestUpdate(languageResolverUpdatedSymbol, previous);\n },\n },\n cancelable: true,\n }\n );\n this.host.dispatchEvent(queryThemeEvent);\n }\n}\n"],"mappings":";AAkCA,IAAa,IAAgC,OAC3C,4BACD,EAaK,oBAAY,IAAI,KAAyB,EAC3C;AAEJ,SAAS,EAAgB,GAA0C;AAejE,QAdA,EAAU,IAAI,EAAS,EAElB,MACH,IAAiB,IAAI,uBAAuB;AAC1C,OAAK,IAAM,KAAM,EACf,IAAI;GAEN,EACF,EAAe,QAAQ,SAAS,iBAAiB;EAC/C,YAAY;EACZ,iBAAiB,CAAC,OAAO;EAC1B,CAAC,SAGS;AAEX,EADA,EAAU,OAAO,EAAS,EACtB,EAAU,SAAS,MACrB,KAAA,QAAA,EAAgB,YAAY,EAC5B,IAAiB,KAAA;;;AA0CvB,IAAa,IAAb,MAAwE;CAetE,YAAY,GAAuB;AAEjC,kBAVS,KAAK,qBAAqB,EASnC,KAAK,OAAO,GACZ,KAAK,KAAK,cAAc,KAAK;;CAO/B,sBAAsC;EACpC,IAAM,IAAM,SAAS,gBAAgB,QAAQ,UAAU,YAAY;AACnE,MAAI;AAEF,UADA,KAAK,eAAe,mBAAmB,CAAC,EAAI,CAAC,EACtC;cACD;AACN,UAAO;;;CAIX,gBAA6B;AAE3B,EADA,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAAgB,KAAK,iBAAiB,KAAK,KAAK,CAAC;;CAG7E,mBAAgC;;AAI9B,GAHA,IAAA,KAAK,gBAAA,QAAA,EAAA,KAAA,KAAe,EACpB,KAAK,cAAc,KAAA,IACnB,IAAA,KAAK,uBAAA,QAAA,EAAA,KAAA,KAAsB,EAC3B,KAAK,qBAAqB,KAAA;;CAO5B,mBAAiC;AAC/B,MAAI,KAAK,YACP;EAEF,IAAM,IAAO,KAAK,qBAAqB;AACvC,MAAI,MAAS,KAAK,SAChB;EAEF,IAAM,IAAW,KAAK;AAEtB,EADA,KAAK,WAAW,GAChB,KAAK,KAAK,cAAc,GAA+B,EAAS;;CAUlE,kBAAgC;AAC9B,OAAK,WAAW,KAAK,qBAAqB;EAC1C,IAAM,IAAkB,IAAI,YAC1B,uBACA;GACE,SAAS;GACT,UAAU;GACV,QAAQ,EACN,WAAW,GAAc,MAA4B;IACnD,IAAM,IAAW,KAAK;AAGtB,IAFA,KAAK,WAAW,GAChB,KAAK,cAAc,GACnB,KAAK,KAAK,cAAc,GAA+B,EAAS;MAEnE;GACD,YAAY;GACb,CACF;AACD,OAAK,KAAK,cAAc,EAAgB"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ export { TriggerPressController, type TriggerPressControllerOptions, } from './src/trigger-press-controller.js';
@@ -0,0 +1,2 @@
1
+ import { TriggerPressController as e } from "./src/trigger-press-controller.js";
2
+ export { e as TriggerPressController };
@@ -0,0 +1,149 @@
1
+ import { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * Options for {@link TriggerPressController.attach}.
4
+ */
5
+ export interface TriggerPressControllerOptions {
6
+ /**
7
+ * Called on a trigger click that is not consumed as a same-gesture reopen
8
+ * guard (see {@link TriggerPressController} for when a click is
9
+ * consumed instead). Typically flips the host's own `open` property.
10
+ */
11
+ onToggle: () => void;
12
+ }
13
+ /**
14
+ * **TriggerPressController** — click-to-toggle wiring for a trigger that
15
+ * opens a native light-dismissible surface (`popover="auto"`, a non-modal
16
+ * `<dialog>`, or anything else the platform can close on its own), without
17
+ * the surface reopening on the same click that was meant to close it.
18
+ *
19
+ * ### The bug this prevents
20
+ *
21
+ * A naive click-to-toggle handler (`trigger.onclick = () => (open = !open)`)
22
+ * looks correct, but breaks the moment the surface can close itself: pressing
23
+ * the trigger again while the surface is open is, from the surface's own
24
+ * perspective, a press *outside* it. The platform's light-dismiss closes the
25
+ * surface **before** the trailing `click` event fires on the trigger. By the
26
+ * time that `click` handler runs, `open` already reads `false` (synced down
27
+ * by the dismissal), so `!open` flips it back to `true` — the surface
28
+ * silently reopens on the very click that should have closed it.
29
+ *
30
+ * ### How it's fixed
31
+ *
32
+ * A capturing `pointerdown`/`touchstart` listener on the trigger opens a
33
+ * short "gesture window" before the platform gets a chance to light-dismiss.
34
+ * The consumer calls {@link TriggerPressController.noteNativeDismiss}
35
+ * from its own native-close handler (for example a `popover` element's
36
+ * `beforetoggle` reaction) whenever it observes a close that was **not**
37
+ * driven by its own `open` setter. If that close lands inside the gesture
38
+ * window, the controller remembers it, and the trailing `click` — read by
39
+ * this controller's own listener — is consumed instead of calling
40
+ * {@link TriggerPressControllerOptions.onToggle}. A close observed
41
+ * outside the gesture window (Escape, an outside click elsewhere, a
42
+ * programmatic close) is not affected.
43
+ *
44
+ * This is a JS-only fix because a custom element's shadow-internal surface
45
+ * cannot be associated with an external trigger via the native
46
+ * `popovertarget` attribute (that attribute cannot reach across shadow
47
+ * boundaries), so the platform has no way to know the trigger and the
48
+ * surface belong together.
49
+ *
50
+ * ### Usage
51
+ *
52
+ * Construct once per host, then call {@link TriggerPressController.attach}
53
+ * every time the resolved trigger element changes (attaching the same
54
+ * element again is a no-op for the listeners, but still updates the
55
+ * {@link TriggerPressControllerOptions.onToggle} callback). Call
56
+ * {@link TriggerPressController.noteNativeDismiss} from the surface's own
57
+ * native-close reaction, guarded by whatever the host already uses to tell a
58
+ * genuine native dismissal apart from its own programmatic close (typically:
59
+ * the close reaction fires while the host's `open` property is still `true`).
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * class MyToggle extends LitElement {
64
+ * @property({ type: Boolean, reflect: true }) open = false;
65
+ *
66
+ * private readonly pressGuard = new TriggerPressController(this);
67
+ *
68
+ * private wireTrigger(trigger: HTMLElement | null): void {
69
+ * this.pressGuard.attach(trigger, {
70
+ * onToggle: () => (this.open = !this.open),
71
+ * });
72
+ * }
73
+ *
74
+ * // Bound to the surface's `beforetoggle` in render().
75
+ * private onBeforeToggle = (event: ToggleEvent): void => {
76
+ * if (event.newState === 'open') {
77
+ * return;
78
+ * }
79
+ * if (this.open) {
80
+ * // Not already closed via our own setter, so this is a genuine
81
+ * // native dismissal (Escape, outside click, ...).
82
+ * this.pressGuard.noteNativeDismiss();
83
+ * }
84
+ * this.open = false;
85
+ * };
86
+ * }
87
+ * ```
88
+ */
89
+ export declare class TriggerPressController implements ReactiveController {
90
+ /** The element currently carrying the press/click listeners. */
91
+ private trigger;
92
+ private options;
93
+ /**
94
+ * True between a trigger press start (`pointerdown`/`touchstart`) and its
95
+ * `click`, so a native dismissal observed inside that window is attributed
96
+ * to the press.
97
+ */
98
+ private pointerActive;
99
+ /** Cancels the pending press-end listeners armed in {@link onPressStart}. */
100
+ private pressEndAbort;
101
+ /**
102
+ * Set by {@link noteNativeDismiss} when a native dismissal is observed
103
+ * while {@link pointerActive}, so the trailing `click` of that same
104
+ * gesture is read as the close rather than a reopen.
105
+ */
106
+ private dismissedByPress;
107
+ /**
108
+ * Registers this controller on `host` via `addController`.
109
+ *
110
+ * @param host - Reactive element whose trigger this controller wires.
111
+ */
112
+ constructor(host: ReactiveControllerHost);
113
+ /**
114
+ * Wires (or rewires) the click-to-toggle and press-tracking listeners.
115
+ *
116
+ * A no-op for the listeners themselves when `trigger` is the same element
117
+ * already attached — safe to call on every re-resolution of the trigger
118
+ * (for example from a Lit `updated()` reacting to `for`/`triggerElement`
119
+ * changes) without needing to compare against the previous element
120
+ * yourself. `options` is always updated, even when `trigger` is unchanged.
121
+ *
122
+ * @param trigger - Element to wire, or `null` to only detach.
123
+ * @param options - Callback invoked on an un-consumed click.
124
+ */
125
+ attach(trigger: HTMLElement | null, options: TriggerPressControllerOptions): void;
126
+ /**
127
+ * Removes the currently-wired listeners (if any) and resets gesture state.
128
+ * Safe to call multiple times. Also called automatically by
129
+ * {@link hostDisconnected}.
130
+ */
131
+ detach(): void;
132
+ /**
133
+ * Call from the surface's own native-close reaction when the close was
134
+ * **not** driven by the host's own `open` setter (a genuine native
135
+ * dismissal: Escape, an outside click, or the platform closing it for any
136
+ * other reason). No-op unless a press on the attached trigger is currently
137
+ * in flight; that gesture window is the only time a dismissal needs to be
138
+ * correlated with the trailing click.
139
+ */
140
+ noteNativeDismiss(): void;
141
+ /**
142
+ * Lit `ReactiveController` hook: tears down listeners when the host
143
+ * disconnects.
144
+ */
145
+ hostDisconnected(): void;
146
+ private readonly onPressStart;
147
+ private readonly onPressEnd;
148
+ private readonly onClick;
149
+ }
@@ -0,0 +1,43 @@
1
+ //#region controllers/trigger-press-controller/src/trigger-press-controller.ts
2
+ var e = class {
3
+ constructor(e) {
4
+ this.trigger = null, this.options = null, this.pointerActive = !1, this.pressEndAbort = null, this.dismissedByPress = !1, this.onPressStart = () => {
5
+ var e;
6
+ this.pointerActive = !0, (e = this.pressEndAbort) == null || e.abort(), this.pressEndAbort = new AbortController();
7
+ let { signal: t } = this.pressEndAbort;
8
+ document.addEventListener("pointerup", this.onPressEnd, {
9
+ capture: !0,
10
+ once: !0,
11
+ signal: t
12
+ }), document.addEventListener("pointercancel", this.onPressEnd, {
13
+ capture: !0,
14
+ once: !0,
15
+ signal: t
16
+ });
17
+ }, this.onPressEnd = (e) => {
18
+ var t;
19
+ (t = this.pressEndAbort) == null || t.abort(), !(e.type === "pointerup" && this.trigger !== null && e.composedPath().includes(this.trigger)) && (this.pointerActive = !1, this.dismissedByPress = !1);
20
+ }, this.onClick = () => {
21
+ var e;
22
+ let t = this.dismissedByPress;
23
+ this.pointerActive = !1, this.dismissedByPress = !1, !t && ((e = this.options) == null || e.onToggle());
24
+ }, e.addController(this);
25
+ }
26
+ attach(e, t) {
27
+ this.options = t, e !== this.trigger && (this.detach(), this.trigger = e, e && (e.addEventListener("pointerdown", this.onPressStart, { capture: !0 }), e.addEventListener("touchstart", this.onPressStart, { capture: !0 }), e.addEventListener("click", this.onClick)));
28
+ }
29
+ detach() {
30
+ var e, t, n, r;
31
+ (e = this.trigger) == null || e.removeEventListener("pointerdown", this.onPressStart, { capture: !0 }), (t = this.trigger) == null || t.removeEventListener("touchstart", this.onPressStart, { capture: !0 }), (n = this.trigger) == null || n.removeEventListener("click", this.onClick), this.trigger = null, (r = this.pressEndAbort) == null || r.abort(), this.pressEndAbort = null, this.pointerActive = !1, this.dismissedByPress = !1;
32
+ }
33
+ noteNativeDismiss() {
34
+ this.pointerActive && (this.dismissedByPress = !0);
35
+ }
36
+ hostDisconnected() {
37
+ this.detach();
38
+ }
39
+ };
40
+ //#endregion
41
+ export { e as TriggerPressController };
42
+
43
+ //# sourceMappingURL=trigger-press-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trigger-press-controller.js","names":[],"sources":["../../../../controllers/trigger-press-controller/src/trigger-press-controller.ts"],"sourcesContent":["/**\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport type { ReactiveController, ReactiveControllerHost } from 'lit';\n\n/**\n * Options for {@link TriggerPressController.attach}.\n */\nexport interface TriggerPressControllerOptions {\n /**\n * Called on a trigger click that is not consumed as a same-gesture reopen\n * guard (see {@link TriggerPressController} for when a click is\n * consumed instead). Typically flips the host's own `open` property.\n */\n onToggle: () => void;\n}\n\n/**\n * **TriggerPressController** — click-to-toggle wiring for a trigger that\n * opens a native light-dismissible surface (`popover=\"auto\"`, a non-modal\n * `<dialog>`, or anything else the platform can close on its own), without\n * the surface reopening on the same click that was meant to close it.\n *\n * ### The bug this prevents\n *\n * A naive click-to-toggle handler (`trigger.onclick = () => (open = !open)`)\n * looks correct, but breaks the moment the surface can close itself: pressing\n * the trigger again while the surface is open is, from the surface's own\n * perspective, a press *outside* it. The platform's light-dismiss closes the\n * surface **before** the trailing `click` event fires on the trigger. By the\n * time that `click` handler runs, `open` already reads `false` (synced down\n * by the dismissal), so `!open` flips it back to `true` — the surface\n * silently reopens on the very click that should have closed it.\n *\n * ### How it's fixed\n *\n * A capturing `pointerdown`/`touchstart` listener on the trigger opens a\n * short \"gesture window\" before the platform gets a chance to light-dismiss.\n * The consumer calls {@link TriggerPressController.noteNativeDismiss}\n * from its own native-close handler (for example a `popover` element's\n * `beforetoggle` reaction) whenever it observes a close that was **not**\n * driven by its own `open` setter. If that close lands inside the gesture\n * window, the controller remembers it, and the trailing `click` — read by\n * this controller's own listener — is consumed instead of calling\n * {@link TriggerPressControllerOptions.onToggle}. A close observed\n * outside the gesture window (Escape, an outside click elsewhere, a\n * programmatic close) is not affected.\n *\n * This is a JS-only fix because a custom element's shadow-internal surface\n * cannot be associated with an external trigger via the native\n * `popovertarget` attribute (that attribute cannot reach across shadow\n * boundaries), so the platform has no way to know the trigger and the\n * surface belong together.\n *\n * ### Usage\n *\n * Construct once per host, then call {@link TriggerPressController.attach}\n * every time the resolved trigger element changes (attaching the same\n * element again is a no-op for the listeners, but still updates the\n * {@link TriggerPressControllerOptions.onToggle} callback). Call\n * {@link TriggerPressController.noteNativeDismiss} from the surface's own\n * native-close reaction, guarded by whatever the host already uses to tell a\n * genuine native dismissal apart from its own programmatic close (typically:\n * the close reaction fires while the host's `open` property is still `true`).\n *\n * @example\n * ```typescript\n * class MyToggle extends LitElement {\n * @property({ type: Boolean, reflect: true }) open = false;\n *\n * private readonly pressGuard = new TriggerPressController(this);\n *\n * private wireTrigger(trigger: HTMLElement | null): void {\n * this.pressGuard.attach(trigger, {\n * onToggle: () => (this.open = !this.open),\n * });\n * }\n *\n * // Bound to the surface's `beforetoggle` in render().\n * private onBeforeToggle = (event: ToggleEvent): void => {\n * if (event.newState === 'open') {\n * return;\n * }\n * if (this.open) {\n * // Not already closed via our own setter, so this is a genuine\n * // native dismissal (Escape, outside click, ...).\n * this.pressGuard.noteNativeDismiss();\n * }\n * this.open = false;\n * };\n * }\n * ```\n */\nexport class TriggerPressController implements ReactiveController {\n /** The element currently carrying the press/click listeners. */\n private trigger: HTMLElement | null = null;\n\n private options: TriggerPressControllerOptions | null = null;\n\n /**\n * True between a trigger press start (`pointerdown`/`touchstart`) and its\n * `click`, so a native dismissal observed inside that window is attributed\n * to the press.\n */\n private pointerActive = false;\n\n /** Cancels the pending press-end listeners armed in {@link onPressStart}. */\n private pressEndAbort: AbortController | null = null;\n\n /**\n * Set by {@link noteNativeDismiss} when a native dismissal is observed\n * while {@link pointerActive}, so the trailing `click` of that same\n * gesture is read as the close rather than a reopen.\n */\n private dismissedByPress = false;\n\n /**\n * Registers this controller on `host` via `addController`.\n *\n * @param host - Reactive element whose trigger this controller wires.\n */\n constructor(host: ReactiveControllerHost) {\n host.addController(this);\n }\n\n /**\n * Wires (or rewires) the click-to-toggle and press-tracking listeners.\n *\n * A no-op for the listeners themselves when `trigger` is the same element\n * already attached — safe to call on every re-resolution of the trigger\n * (for example from a Lit `updated()` reacting to `for`/`triggerElement`\n * changes) without needing to compare against the previous element\n * yourself. `options` is always updated, even when `trigger` is unchanged.\n *\n * @param trigger - Element to wire, or `null` to only detach.\n * @param options - Callback invoked on an un-consumed click.\n */\n public attach(\n trigger: HTMLElement | null,\n options: TriggerPressControllerOptions\n ): void {\n this.options = options;\n if (trigger === this.trigger) {\n return;\n }\n this.detach();\n this.trigger = trigger;\n if (trigger) {\n trigger.addEventListener('pointerdown', this.onPressStart, {\n capture: true,\n });\n trigger.addEventListener('touchstart', this.onPressStart, {\n capture: true,\n });\n trigger.addEventListener('click', this.onClick);\n }\n }\n\n /**\n * Removes the currently-wired listeners (if any) and resets gesture state.\n * Safe to call multiple times. Also called automatically by\n * {@link hostDisconnected}.\n */\n public detach(): void {\n this.trigger?.removeEventListener('pointerdown', this.onPressStart, {\n capture: true,\n });\n this.trigger?.removeEventListener('touchstart', this.onPressStart, {\n capture: true,\n });\n this.trigger?.removeEventListener('click', this.onClick);\n this.trigger = null;\n this.pressEndAbort?.abort();\n this.pressEndAbort = null;\n this.pointerActive = false;\n this.dismissedByPress = false;\n }\n\n /**\n * Call from the surface's own native-close reaction when the close was\n * **not** driven by the host's own `open` setter (a genuine native\n * dismissal: Escape, an outside click, or the platform closing it for any\n * other reason). No-op unless a press on the attached trigger is currently\n * in flight; that gesture window is the only time a dismissal needs to be\n * correlated with the trailing click.\n */\n public noteNativeDismiss(): void {\n if (this.pointerActive) {\n this.dismissedByPress = true;\n }\n }\n\n /**\n * Lit `ReactiveController` hook: tears down listeners when the host\n * disconnects.\n */\n public hostDisconnected(): void {\n this.detach();\n }\n\n // ─────────────────────────\n // IMPLEMENTATION\n // ─────────────────────────\n\n // Capture phase, before a native light-dismiss (which runs on the same\n // press) gets a chance to close the surface first.\n private readonly onPressStart = (): void => {\n this.pointerActive = true;\n // A touch gesture fires both `pointerdown` and `touchstart` on the same\n // trigger, so this runs twice per press. Without aborting the previous\n // controller first, the second `addEventListener` below is a no-op (the\n // DOM dedupes on type/listener/capture, ignoring the new `signal`), so\n // `pressEndAbort` would end up pointing at a controller with nothing\n // attached to it while the real listeners stay tied to the first one.\n this.pressEndAbort?.abort();\n // Catches a press that ends without a click (drag off the trigger, or a\n // cancelled gesture), which would otherwise leave the flag stuck true.\n this.pressEndAbort = new AbortController();\n const { signal } = this.pressEndAbort;\n document.addEventListener('pointerup', this.onPressEnd, {\n capture: true,\n once: true,\n signal,\n });\n document.addEventListener('pointercancel', this.onPressEnd, {\n capture: true,\n once: true,\n signal,\n });\n };\n\n private readonly onPressEnd = (event: PointerEvent): void => {\n this.pressEndAbort?.abort();\n // A same-target pointerup still gets its own click, which reads and\n // clears `dismissedByPress` itself; clearing it here too would race that\n // read. Any other press end (pointercancel, or a pointerup that lands off\n // the trigger) means no click is coming for this gesture, so both flags\n // are cleared here instead — otherwise a dismissal noted during a press\n // that never resolves to a click would incorrectly consume the next,\n // unrelated click on the trigger.\n if (\n event.type === 'pointerup' &&\n this.trigger !== null &&\n event.composedPath().includes(this.trigger)\n ) {\n return;\n }\n this.pointerActive = false;\n this.dismissedByPress = false;\n };\n\n // If a native dismissal already consumed this gesture (recorded via\n // `noteNativeDismiss`), consume this click so it does not toggle back\n // open; otherwise call the consumer's own toggle.\n private readonly onClick = (): void => {\n const dismissedByThisGesture = this.dismissedByPress;\n this.pointerActive = false;\n this.dismissedByPress = false;\n if (dismissedByThisGesture) {\n return;\n }\n this.options?.onToggle();\n };\n}\n"],"mappings":";AAsGA,IAAa,IAAb,MAAkE;CA4BhE,YAAY,GAA8B;AACxC,iBA3BoC,qBAEkB,2BAOhC,yBAGwB,8BAOrB,8BA4FiB;;AAW1C,GAVA,KAAK,gBAAgB,KAOrB,IAAA,KAAK,kBAAA,QAAA,EAAe,OAAO,EAG3B,KAAK,gBAAgB,IAAI,iBAAiB;GAC1C,IAAM,EAAE,cAAW,KAAK;AAMxB,GALA,SAAS,iBAAiB,aAAa,KAAK,YAAY;IACtD,SAAS;IACT,MAAM;IACN;IACD,CAAC,EACF,SAAS,iBAAiB,iBAAiB,KAAK,YAAY;IAC1D,SAAS;IACT,MAAM;IACN;IACD,CAAC;wBAG2B,MAA8B;;AAC3D,IAAA,IAAA,KAAK,kBAAA,QAAA,EAAe,OAAO,EASzB,IAAM,SAAS,eACf,KAAK,YAAY,QACjB,EAAM,cAAc,CAAC,SAAS,KAAK,QAAQ,MAI7C,KAAK,gBAAgB,IACrB,KAAK,mBAAmB;0BAMa;;GACrC,IAAM,IAAyB,KAAK;AACpC,QAAK,gBAAgB,IACrB,KAAK,mBAAmB,IACpB,QAGJ,IAAA,KAAK,YAAA,QAAA,EAAS,UAAU;KA3IxB,EAAK,cAAc,KAAK;;CAe1B,OACE,GACA,GACM;AACN,OAAK,UAAU,GACX,MAAY,KAAK,YAGrB,KAAK,QAAQ,EACb,KAAK,UAAU,GACX,MACF,EAAQ,iBAAiB,eAAe,KAAK,cAAc,EACzD,SAAS,IACV,CAAC,EACF,EAAQ,iBAAiB,cAAc,KAAK,cAAc,EACxD,SAAS,IACV,CAAC,EACF,EAAQ,iBAAiB,SAAS,KAAK,QAAQ;;CASnD,SAAsB;;AAYpB,GAXA,IAAA,KAAK,YAAA,QAAA,EAAS,oBAAoB,eAAe,KAAK,cAAc,EAClE,SAAS,IACV,CAAC,GACF,IAAA,KAAK,YAAA,QAAA,EAAS,oBAAoB,cAAc,KAAK,cAAc,EACjE,SAAS,IACV,CAAC,GACF,IAAA,KAAK,YAAA,QAAA,EAAS,oBAAoB,SAAS,KAAK,QAAQ,EACxD,KAAK,UAAU,OACf,IAAA,KAAK,kBAAA,QAAA,EAAe,OAAO,EAC3B,KAAK,gBAAgB,MACrB,KAAK,gBAAgB,IACrB,KAAK,mBAAmB;;CAW1B,oBAAiC;AAC/B,EAAI,KAAK,kBACP,KAAK,mBAAmB;;CAQ5B,mBAAgC;AAC9B,OAAK,QAAQ"}
@@ -10,6 +10,6 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  /**
13
- * Public exports for render-only Lit template directives shared across 2nd-gen packages.
13
+ * Public exports for render-only Lit template directives shared across gen2 packages.
14
14
  */
15
15
  export { renderPendingSpinner, type PendingSpinnerResult, } from './pending-spinner/index.js';