@adobe/spectrum-wc-core 2.0.0-beta.2 → 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.
- package/README.md +1 -1
- package/dist/components/accordion/Accordion.base.js +28 -22
- package/dist/components/accordion/Accordion.base.js.map +1 -1
- package/dist/components/accordion/AccordionItem.base.d.ts +16 -6
- package/dist/components/accordion/AccordionItem.base.js +6 -3
- package/dist/components/accordion/AccordionItem.base.js.map +1 -1
- package/dist/components/action-group/ActionGroup.base.d.ts +108 -0
- package/dist/components/action-group/ActionGroup.base.js +53 -0
- package/dist/components/action-group/ActionGroup.base.js.map +1 -0
- package/dist/components/action-group/ActionGroup.types.d.ts +17 -0
- package/dist/components/action-group/ActionGroup.types.js +12 -0
- package/dist/components/action-group/ActionGroup.types.js.map +1 -0
- package/dist/components/action-group/index.d.ts +13 -0
- package/dist/components/action-group/index.js +3 -0
- package/dist/components/alert-banner/AlertBanner.base.js +11 -10
- package/dist/components/alert-banner/AlertBanner.base.js.map +1 -1
- package/dist/components/asset/Asset.base.d.ts +89 -10
- package/dist/components/asset/Asset.base.js +117 -12
- package/dist/components/asset/Asset.base.js.map +1 -1
- package/dist/components/asset/Asset.types.d.ts +20 -2
- package/dist/components/asset/Asset.types.js +10 -2
- package/dist/components/asset/Asset.types.js.map +1 -1
- package/dist/components/asset/index.js +3 -3
- package/dist/components/avatar/Avatar.base.js +18 -22
- package/dist/components/avatar/Avatar.base.js.map +1 -1
- package/dist/components/badge/Badge.base.d.ts +24 -8
- package/dist/components/badge/Badge.base.js +22 -17
- package/dist/components/badge/Badge.base.js.map +1 -1
- package/dist/components/button/Button.base.d.ts +20 -10
- package/dist/components/button/Button.base.js +17 -12
- package/dist/components/button/Button.base.js.map +1 -1
- package/dist/components/button-group/ButtonGroup.base.js +19 -12
- package/dist/components/button-group/ButtonGroup.base.js.map +1 -1
- package/dist/components/card/Card.base.d.ts +95 -46
- package/dist/components/card/Card.base.js +71 -23
- package/dist/components/card/Card.base.js.map +1 -1
- package/dist/components/card/Card.types.d.ts +1 -0
- package/dist/components/card/Card.types.js +2 -2
- package/dist/components/card/Card.types.js.map +1 -1
- package/dist/components/card/index.js +3 -3
- package/dist/components/divider/Divider.base.js +13 -8
- package/dist/components/divider/Divider.base.js.map +1 -1
- package/dist/components/dropzone/Dropzone.base.d.ts +74 -0
- package/dist/components/dropzone/Dropzone.base.js +82 -0
- package/dist/components/dropzone/Dropzone.base.js.map +1 -0
- package/dist/components/dropzone/Dropzone.types.d.ts +43 -0
- package/dist/components/dropzone/Dropzone.types.js +15 -0
- package/dist/components/dropzone/Dropzone.types.js.map +1 -0
- package/dist/components/dropzone/index.d.ts +13 -0
- package/dist/components/dropzone/index.js +3 -0
- package/dist/components/icon/Icon.base.d.ts +11 -7
- package/dist/components/icon/Icon.base.js +13 -20
- package/dist/components/icon/Icon.base.js.map +1 -1
- package/dist/components/illustrated-message/IllustratedMessage.base.d.ts +22 -8
- package/dist/components/illustrated-message/IllustratedMessage.base.js +33 -20
- package/dist/components/illustrated-message/IllustratedMessage.base.js.map +1 -1
- package/dist/components/infield-button/InfieldButton.base.d.ts +28 -0
- package/dist/components/infield-button/InfieldButton.base.js +22 -0
- package/dist/components/infield-button/InfieldButton.base.js.map +1 -0
- package/dist/components/infield-button/InfieldButton.types.d.ts +13 -0
- package/dist/components/infield-button/InfieldButton.types.js +11 -0
- package/dist/components/infield-button/InfieldButton.types.js.map +1 -0
- package/dist/components/infield-button/index.d.ts +13 -0
- package/dist/components/infield-button/index.js +3 -0
- package/dist/components/meter/Meter.base.js +11 -9
- package/dist/components/meter/Meter.base.js.map +1 -1
- package/dist/components/popover/Popover.base.d.ts +1 -21
- package/dist/components/popover/Popover.base.js +57 -77
- package/dist/components/popover/Popover.base.js.map +1 -1
- package/dist/components/progress-circle/ProgressCircle.base.d.ts +0 -1
- package/dist/components/progress-circle/ProgressCircle.base.js +26 -22
- package/dist/components/progress-circle/ProgressCircle.base.js.map +1 -1
- package/dist/components/status-light/StatusLight.base.d.ts +1 -1
- package/dist/components/status-light/StatusLight.base.js +12 -9
- package/dist/components/status-light/StatusLight.base.js.map +1 -1
- package/dist/components/tabs/TabPanel.base.d.ts +1 -1
- package/dist/components/tabs/TabPanel.base.js.map +1 -1
- package/dist/components/tabs/Tabs.base.js +44 -32
- package/dist/components/tabs/Tabs.base.js.map +1 -1
- package/dist/components/tooltip/Tooltip.base.js +57 -22
- package/dist/components/tooltip/Tooltip.base.js.map +1 -1
- package/dist/controllers/color-controller/src/color-controller.js +45 -44
- package/dist/controllers/color-controller/src/color-controller.js.map +1 -1
- package/dist/controllers/drag-and-drop-controller/index.d.ts +12 -0
- package/dist/controllers/drag-and-drop-controller/index.js +2 -0
- package/dist/controllers/drag-and-drop-controller/src/drag-and-drop-controller.d.ts +68 -0
- package/dist/controllers/drag-and-drop-controller/src/drag-and-drop-controller.js +56 -0
- package/dist/controllers/drag-and-drop-controller/src/drag-and-drop-controller.js.map +1 -0
- package/dist/controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.d.ts +1 -1
- package/dist/controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.js.map +1 -1
- package/dist/controllers/index.d.ts +6 -2
- package/dist/controllers/index.js +15 -11
- package/dist/controllers/language-resolution.d.ts +1 -1
- package/dist/controllers/language-resolution.js.map +1 -1
- package/dist/controllers/page-scroll-lock-controller/index.d.ts +12 -0
- package/dist/controllers/page-scroll-lock-controller/index.js +2 -0
- package/dist/controllers/{page-scroll-lock.js → page-scroll-lock-controller/src/page-scroll-lock.js} +1 -1
- package/dist/controllers/page-scroll-lock-controller/src/page-scroll-lock.js.map +1 -0
- package/dist/controllers/slot-presence-controller/index.d.ts +12 -0
- package/dist/controllers/slot-presence-controller/index.js +2 -0
- package/dist/controllers/slot-presence-controller/src/slot-presence-controller.d.ts +57 -0
- package/dist/controllers/slot-presence-controller/src/slot-presence-controller.js +44 -0
- package/dist/controllers/slot-presence-controller/src/slot-presence-controller.js.map +1 -0
- package/dist/controllers/slot-text-controller/index.d.ts +12 -0
- package/dist/controllers/slot-text-controller/index.js +2 -0
- package/dist/controllers/slot-text-controller/src/slot-text-controller.d.ts +92 -0
- package/dist/controllers/slot-text-controller/src/slot-text-controller.js +53 -0
- package/dist/controllers/slot-text-controller/src/slot-text-controller.js.map +1 -0
- package/dist/controllers/trigger-press-controller/index.d.ts +12 -0
- package/dist/controllers/trigger-press-controller/index.js +2 -0
- package/dist/controllers/trigger-press-controller/src/trigger-press-controller.d.ts +149 -0
- package/dist/controllers/trigger-press-controller/src/trigger-press-controller.js +43 -0
- package/dist/controllers/trigger-press-controller/src/trigger-press-controller.js.map +1 -0
- package/dist/directives/index.d.ts +1 -1
- package/dist/element/define-element.js +1 -1
- package/dist/element/define-element.js.map +1 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +2 -2
- package/dist/element/spectrum-element.d.ts +58 -15
- package/dist/element/spectrum-element.js +56 -37
- package/dist/element/spectrum-element.js.map +1 -1
- package/dist/element/version.d.ts +3 -3
- package/dist/element/version.js +1 -1
- package/dist/element/version.js.map +1 -1
- package/dist/mixins/index.d.ts +0 -2
- package/dist/mixins/index.js +4 -6
- package/dist/mixins/linear-progress-mixin.d.ts +1 -2
- package/dist/mixins/linear-progress-mixin.js +35 -34
- package/dist/mixins/linear-progress-mixin.js.map +1 -1
- package/dist/mixins/pending-mixin.js +12 -11
- package/dist/mixins/pending-mixin.js.map +1 -1
- package/dist/mixins/sized-mixin.d.ts +28 -0
- package/dist/mixins/sized-mixin.js +6 -6
- package/dist/mixins/sized-mixin.js.map +1 -1
- package/dist/utils/dev-validation.d.ts +88 -0
- package/dist/utils/dev-validation.js +33 -0
- package/dist/utils/dev-validation.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +12 -10
- package/dist/utils/is-focus-visible-in-tree.d.ts +33 -0
- package/dist/utils/is-focus-visible-in-tree.js +11 -0
- package/dist/utils/is-focus-visible-in-tree.js.map +1 -0
- package/dist/utils/resolve-trigger.js.map +1 -1
- package/package.json +213 -18
- package/CHANGELOG.md +0 -125
- package/dist/_virtual/_@oxc-project_runtime@0.124.0/helpers/defineProperty.js +0 -12
- package/dist/_virtual/_@oxc-project_runtime@0.124.0/helpers/toPrimitive.js +0 -14
- package/dist/_virtual/_@oxc-project_runtime@0.124.0/helpers/toPropertyKey.js +0 -9
- package/dist/_virtual/_@oxc-project_runtime@0.124.0/helpers/typeof.js +0 -11
- package/dist/controllers/page-scroll-lock.js.map +0 -1
- package/dist/mixins/observe-slot-presence.d.ts +0 -12
- package/dist/mixins/observe-slot-presence.js +0 -40
- package/dist/mixins/observe-slot-presence.js.map +0 -1
- package/dist/mixins/observe-slot-text.d.ts +0 -11
- package/dist/mixins/observe-slot-text.js +0 -60
- package/dist/mixins/observe-slot-text.js.map +0 -1
- /package/dist/controllers/{page-scroll-lock.d.ts → page-scroll-lock-controller/src/page-scroll-lock.d.ts} +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region controllers/drag-and-drop-controller/src/drag-and-drop-controller.ts
|
|
2
|
+
var e = 100, t = class {
|
|
3
|
+
constructor(t, n) {
|
|
4
|
+
this._dragLeaveTimer = null, this._abortController = null, this._onDragOver = (e) => {
|
|
5
|
+
var t, n, r, i, a;
|
|
6
|
+
if (e.preventDefault(), e.dataTransfer) {
|
|
7
|
+
if (this._clearDragLeaveTimer(), ((t = (n = this._options).shouldAccept) == null ? void 0 : t.call(n, e)) === !1) {
|
|
8
|
+
if (e.dataTransfer.dropEffect = "none", this._options.isDragged()) {
|
|
9
|
+
var o, s;
|
|
10
|
+
(o = (s = this._options).onDragLeave) == null || o.call(s, {
|
|
11
|
+
clientX: e.clientX,
|
|
12
|
+
clientY: e.clientY,
|
|
13
|
+
relatedTarget: e.relatedTarget
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (e.dataTransfer.dropEffect = (r = (i = (a = this._options).dropEffect) == null ? void 0 : i.call(a)) == null ? "copy" : r, !this._options.isDragged()) {
|
|
19
|
+
var c, l;
|
|
20
|
+
(c = (l = this._options).onDragEnter) == null || c.call(l, e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, this._onDragLeave = (t) => {
|
|
24
|
+
if (t.relatedTarget && this._host.contains(t.relatedTarget)) return;
|
|
25
|
+
this._clearDragLeaveTimer();
|
|
26
|
+
let { clientX: n, clientY: r, relatedTarget: i } = t;
|
|
27
|
+
this._dragLeaveTimer = setTimeout(() => {
|
|
28
|
+
var e, t;
|
|
29
|
+
this._dragLeaveTimer = null, (e = (t = this._options).onDragLeave) == null || e.call(t, {
|
|
30
|
+
clientX: n,
|
|
31
|
+
clientY: r,
|
|
32
|
+
relatedTarget: i
|
|
33
|
+
});
|
|
34
|
+
}, e);
|
|
35
|
+
}, this._onDrop = (e) => {
|
|
36
|
+
var t, n;
|
|
37
|
+
e.preventDefault(), this._options.isDragged() && (this._clearDragLeaveTimer(), (t = (n = this._options).onDrop) == null || t.call(n, e));
|
|
38
|
+
}, this._host = t, this._options = n, t.addController(this);
|
|
39
|
+
}
|
|
40
|
+
hostConnected() {
|
|
41
|
+
this._abortController = new AbortController();
|
|
42
|
+
let { signal: e } = this._abortController;
|
|
43
|
+
this._host.addEventListener("dragover", this._onDragOver, { signal: e }), this._host.addEventListener("dragleave", this._onDragLeave, { signal: e }), this._host.addEventListener("drop", this._onDrop, { signal: e });
|
|
44
|
+
}
|
|
45
|
+
hostDisconnected() {
|
|
46
|
+
var e;
|
|
47
|
+
(e = this._abortController) == null || e.abort(), this._abortController = null, this._clearDragLeaveTimer();
|
|
48
|
+
}
|
|
49
|
+
_clearDragLeaveTimer() {
|
|
50
|
+
this._dragLeaveTimer !== null && (clearTimeout(this._dragLeaveTimer), this._dragLeaveTimer = null);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { t as DragAndDropController };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=drag-and-drop-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-and-drop-controller.js","names":[],"sources":["../../../../controllers/drag-and-drop-controller/src/drag-and-drop-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 * Snapshot of the fields read from a native `DragEvent` before it's recycled\n * by the browser, safe to read after the debounce in {@link DragAndDropControllerOptions.onDragLeave}.\n */\nexport interface DragLeaveSnapshot {\n clientX: number;\n clientY: number;\n relatedTarget: EventTarget | null;\n}\n\n/** Configuration options for {@link DragAndDropController}. */\nexport interface DragAndDropControllerOptions {\n /**\n * The host's own current \"is a drag over me\" flag. The controller never\n * keeps a copy of this state; it re-reads it on every native event so a\n * host that also exposes this flag as a public, externally-settable\n * property (as `swc-dropzone` does) can never drift out of sync with it.\n */\n isDragged: () => boolean;\n\n /**\n * Return `false` to reject the current drag payload; sets the OS cursor to\n * \"not-allowed\". Called on every native `dragover` tick. Defaults to\n * always-accept.\n */\n shouldAccept?: (event: DragEvent) => boolean;\n\n /** Called once when an accepted drag enters — not on every `dragover` tick. Set `isDragged`'s backing flag to `true` here. */\n onDragEnter?: (event: DragEvent) => void;\n\n /** Called once the accepted drag leaves or becomes rejected. Set `isDragged`'s backing flag to `false` here. */\n onDragLeave?: (snapshot: DragLeaveSnapshot) => void;\n\n /** Called on drop, only when `isDragged()` is currently `true`. */\n onDrop?: (event: DragEvent) => void;\n\n /** OS cursor feedback while an accepted drag hovers. Defaults to `'copy'`. */\n dropEffect?: () => DataTransfer['dropEffect'];\n}\n\n// Long enough that passing over a child element's own dragleave/dragover pair\n// doesn't flicker the state; short enough that leaving the host still reads\n// as immediate.\nconst DRAG_LEAVE_DEBOUNCE_MS = 100;\n\n// ─────────────────────────\n// CONTROLLER\n// ─────────────────────────\n\n/**\n * A Lit {@link ReactiveController} that manages native drag-and-drop event\n * wiring (`dragover` / `dragleave` / `drop`) for a host element: debounces\n * `dragleave` so passing over a child element doesn't flicker the state,\n * de-duplicates entry so `onDragEnter` fires once per hover session rather\n * than on every `dragover` tick, and lets the host veto payloads it doesn't\n * want via `shouldAccept`.\n *\n * The host owns its own \"dragged\" state (via `isDragged`/`onDragEnter`/\n * `onDragLeave`); this controller only drives the timing.\n *\n * @example\n * ```ts\n * const dragAndDrop = new DragAndDropController(this, {\n * isDragged: () => this._dragged,\n * onDragEnter: () => { this._dragged = true; },\n * onDragLeave: () => { this._dragged = false; },\n * onDrop: (event) => this._handleFilesDropped(event.dataTransfer?.files),\n * });\n * ```\n */\nexport class DragAndDropController implements ReactiveController {\n private readonly _host: ReactiveElement;\n private readonly _options: DragAndDropControllerOptions;\n private _dragLeaveTimer: ReturnType<typeof setTimeout> | null = null;\n private _abortController: AbortController | null = null;\n\n constructor(host: ReactiveElement, options: DragAndDropControllerOptions) {\n this._host = host;\n this._options = options;\n host.addController(this);\n }\n\n public hostConnected(): void {\n this._abortController = new AbortController();\n const { signal } = this._abortController;\n this._host.addEventListener('dragover', this._onDragOver as EventListener, {\n signal,\n });\n this._host.addEventListener(\n 'dragleave',\n this._onDragLeave as EventListener,\n { signal }\n );\n this._host.addEventListener('drop', this._onDrop as EventListener, {\n signal,\n });\n }\n\n public hostDisconnected(): void {\n this._abortController?.abort();\n this._abortController = null;\n this._clearDragLeaveTimer();\n }\n\n private readonly _onDragOver = (event: DragEvent): void => {\n event.preventDefault();\n if (!event.dataTransfer) {\n return;\n }\n\n this._clearDragLeaveTimer();\n if (this._options.shouldAccept?.(event) === false) {\n event.dataTransfer.dropEffect = 'none';\n if (this._options.isDragged()) {\n this._options.onDragLeave?.({\n clientX: event.clientX,\n clientY: event.clientY,\n relatedTarget: event.relatedTarget,\n });\n }\n return;\n }\n\n event.dataTransfer.dropEffect = this._options.dropEffect?.() ?? 'copy';\n\n if (!this._options.isDragged()) {\n this._options.onDragEnter?.(event);\n }\n };\n\n private readonly _onDragLeave = (event: DragEvent): void => {\n if (\n event.relatedTarget &&\n this._host.contains(event.relatedTarget as Node)\n ) {\n return;\n }\n\n this._clearDragLeaveTimer();\n // Capture synchronously; browsers recycle DragEvent objects after the\n // synchronous handler returns, so reading them inside setTimeout is unsafe.\n const { clientX, clientY, relatedTarget } = event;\n this._dragLeaveTimer = setTimeout(() => {\n this._dragLeaveTimer = null;\n this._options.onDragLeave?.({ clientX, clientY, relatedTarget });\n }, DRAG_LEAVE_DEBOUNCE_MS);\n };\n\n private readonly _onDrop = (event: DragEvent): void => {\n event.preventDefault();\n if (!this._options.isDragged()) {\n return;\n }\n\n this._clearDragLeaveTimer();\n this._options.onDrop?.(event);\n };\n\n private _clearDragLeaveTimer(): void {\n if (this._dragLeaveTimer !== null) {\n clearTimeout(this._dragLeaveTimer);\n this._dragLeaveTimer = null;\n }\n }\n}\n"],"mappings":";AA6DA,IAAM,IAAyB,KA2BlB,IAAb,MAAiE;CAM/D,YAAY,GAAuB,GAAuC;AAGxE,yBAN8D,8BACb,0BA8BnB,MAA2B;;AACzD,SAAM,gBAAgB,EACjB,EAAM,cAKX;QADA,KAAK,sBAAsB,IAC3B,KAAA,IAAI,KAAK,UAAS,iBAAA,OAAA,KAAA,IAAA,EAAA,KAAA,GAAe,EAAM,MAAK,IAAO;AAEjD,SADA,EAAM,aAAa,aAAa,QAC5B,KAAK,SAAS,WAAW,EAAE;;AAC7B,OAAA,KAAA,IAAA,KAAK,UAAS,gBAAA,QAAA,EAAA,KAAA,GAAc;OAC1B,SAAS,EAAM;OACf,SAAS,EAAM;OACf,eAAe,EAAM;OACtB,CAAC;;AAEJ;;AAKF,QAFA,EAAM,aAAa,cAAA,KAAA,KAAA,IAAa,KAAK,UAAS,eAAA,OAAA,KAAA,IAAA,EAAA,KAAA,EAAc,KAAA,OAAI,SAAJ,GAExD,CAAC,KAAK,SAAS,WAAW,EAAE;;AAC9B,MAAA,KAAA,IAAA,KAAK,UAAS,gBAAA,QAAA,EAAA,KAAA,GAAc,EAAM;;;0BAIL,MAA2B;AAC1D,OACE,EAAM,iBACN,KAAK,MAAM,SAAS,EAAM,cAAsB,CAEhD;AAGF,QAAK,sBAAsB;GAG3B,IAAM,EAAE,YAAS,YAAS,qBAAkB;AAC5C,QAAK,kBAAkB,iBAAiB;;AAEtC,IADA,KAAK,kBAAkB,OACvB,KAAA,IAAA,KAAK,UAAS,gBAAA,QAAA,EAAA,KAAA,GAAc;KAAE;KAAS;KAAS;KAAe,CAAC;MAC/D,EAAuB;qBAGA,MAA2B;;AACrD,KAAM,gBAAgB,EACjB,KAAK,SAAS,WAAW,KAI9B,KAAK,sBAAsB,GAC3B,KAAA,IAAA,KAAK,UAAS,WAAA,QAAA,EAAA,KAAA,GAAS,EAAM;KA9E7B,KAAK,QAAQ,GACb,KAAK,WAAW,GAChB,EAAK,cAAc,KAAK;;CAG1B,gBAA6B;AAC3B,OAAK,mBAAmB,IAAI,iBAAiB;EAC7C,IAAM,EAAE,cAAW,KAAK;AASxB,EARA,KAAK,MAAM,iBAAiB,YAAY,KAAK,aAA8B,EACzE,WACD,CAAC,EACF,KAAK,MAAM,iBACT,aACA,KAAK,cACL,EAAE,WAAQ,CACX,EACD,KAAK,MAAM,iBAAiB,QAAQ,KAAK,SAA0B,EACjE,WACD,CAAC;;CAGJ,mBAAgC;;AAG9B,GAFA,IAAA,KAAK,qBAAA,QAAA,EAAkB,OAAO,EAC9B,KAAK,mBAAmB,MACxB,KAAK,sBAAsB;;CAyD7B,uBAAqC;AACnC,EAAI,KAAK,oBAAoB,SAC3B,aAAa,KAAK,gBAAgB,EAClC,KAAK,kBAAkB"}
|
package/dist/controllers/focusgroup-navigation-controller/src/focusgroup-navigation-controller.d.ts
CHANGED
|
@@ -277,7 +277,7 @@ export declare class FocusgroupNavigationController implements ReactiveControlle
|
|
|
277
277
|
* Resolves writing direction from the computed style of the host element.
|
|
278
278
|
*
|
|
279
279
|
* Uses `getComputedStyle` rather than walking `dir` attributes so that
|
|
280
|
-
* CSS-inherited direction (the
|
|
280
|
+
* CSS-inherited direction (the gen2 default) is correctly detected.
|
|
281
281
|
*
|
|
282
282
|
* @returns True when horizontal arrow directions should follow RTL semantics.
|
|
283
283
|
*/
|
|
@@ -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,14 +10,18 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Public exports for Lit reactive controllers shared across
|
|
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
|
+
export { DragAndDropController, type DragAndDropControllerOptions, type DragLeaveSnapshot, } from './drag-and-drop-controller/index.js';
|
|
16
17
|
export { focusgroupNavigationActiveChange, FocusgroupNavigationController, type FocusgroupActiveChangeSource, type FocusgroupDirection, type FocusgroupNavigationActiveChangeDetail, type FocusgroupNavigationOptions, } from './focusgroup-navigation-controller/index.js';
|
|
17
18
|
export { LiveSelectionController, type LiveSelectionMode, type LiveSelectionOptions, } from './live-selection-controller/index.js';
|
|
18
19
|
export { HoverController, type HoverControllerHost, type HoverControllerOptions, } from './hover-controller/index.js';
|
|
19
20
|
export { LanguageResolutionController, languageResolverUpdatedSymbol, } from './language-resolution.js';
|
|
20
|
-
export { PageScrollLockController } from './page-scroll-lock.js';
|
|
21
|
+
export { PageScrollLockController } from './page-scroll-lock-controller/index.js';
|
|
21
22
|
export { PendingController, type PendingControllerHost, type PendingControllerOptions, } from './pending-controller/index.js';
|
|
22
23
|
export { ALL_PLACEMENTS, fromFloatingPlacement, PlacementController, toFloatingPlacement, type Placement, type PlacementOptions, type VirtualTrigger, } from './placement-controller/index.js';
|
|
23
24
|
export { SlotAttributePropagationController, type SlotAttributePropagationControllerOptions, } from './slot-attribute-propagation-controller/index.js';
|
|
25
|
+
export { SlotPresenceController } from './slot-presence-controller/index.js';
|
|
26
|
+
export { SlotTextController, type SlotTextConfig, } from './slot-text-controller/index.js';
|
|
27
|
+
export { TriggerPressController, type TriggerPressControllerOptions, } from './trigger-press-controller/index.js';
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { SlotAttributePropagationController as e } from "./slot-attribute-propagation-controller/src/slot-attribute-propagation-controller.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { SlotTextController as t } from "./slot-text-controller/src/slot-text-controller.js";
|
|
3
|
+
import { DragAndDropController as n } from "./drag-and-drop-controller/src/drag-and-drop-controller.js";
|
|
4
|
+
import { HoverController as r } from "./hover-controller/src/hover-controller.js";
|
|
5
|
+
import { fromFloatingPlacement as i, toFloatingPlacement as a } from "./placement-controller/src/placement-conversion.js";
|
|
6
|
+
import { ALL_PLACEMENTS as o } from "./placement-controller/src/types.js";
|
|
7
|
+
import { PlacementController as s } from "./placement-controller/src/placement-controller.js";
|
|
6
8
|
import "./placement-controller/index.js";
|
|
7
|
-
import { LanguageResolutionController as
|
|
8
|
-
import {
|
|
9
|
+
import { LanguageResolutionController as c, languageResolverUpdatedSymbol as l } from "./language-resolution.js";
|
|
10
|
+
import { SlotPresenceController as u } from "./slot-presence-controller/src/slot-presence-controller.js";
|
|
11
|
+
import { PendingController as d } from "./pending-controller/src/pending-controller.js";
|
|
9
12
|
import "./pending-controller/index.js";
|
|
10
|
-
import { ColorController as
|
|
13
|
+
import { ColorController as f } from "./color-controller/src/color-controller.js";
|
|
11
14
|
import "./color-controller/index.js";
|
|
12
|
-
import { FocusgroupNavigationController as
|
|
13
|
-
import { LiveSelectionController as
|
|
14
|
-
import { PageScrollLockController as
|
|
15
|
-
|
|
15
|
+
import { FocusgroupNavigationController as p, focusgroupNavigationActiveChange as m } from "./focusgroup-navigation-controller/src/focusgroup-navigation-controller.js";
|
|
16
|
+
import { LiveSelectionController as h } from "./live-selection-controller/src/live-selection-controller.js";
|
|
17
|
+
import { PageScrollLockController as g } from "./page-scroll-lock-controller/src/page-scroll-lock.js";
|
|
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
|
|
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
|
|
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 { PageScrollLockController } from './src/page-scroll-lock.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-scroll-lock.js","names":[],"sources":["../../../../controllers/page-scroll-lock-controller/src/page-scroll-lock.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// Module-scoped reference-counted lock\n// ────────────────────────────────────────────\n//\n// Stacked blocking surfaces (e.g. nested modal dialogs, a modal popover opened\n// over another) must coordinate a single page-scroll lock. The document's\n// original `overflow` is captured once on the 0 -> 1 transition and restored\n// once on the 1 -> 0 transition. Saving per instance would let a later locker\n// capture an already-locked value (`hidden`) and then restore it permanently\n// when it releases, leaving the page unscrollable.\n\nlet lockCount = 0;\nlet savedOverflow = '';\n\nfunction acquirePageScrollLock(): void {\n if (lockCount === 0) {\n const html = document.documentElement;\n savedOverflow = html.style.overflow;\n html.style.overflow = 'hidden';\n }\n lockCount += 1;\n}\n\nfunction releasePageScrollLock(): void {\n if (lockCount === 0) {\n return;\n }\n lockCount -= 1;\n if (lockCount === 0) {\n document.documentElement.style.overflow = savedOverflow;\n savedOverflow = '';\n }\n}\n\n/**\n * Locks page scroll behind a blocking surface (a modal popover, dialog, or tray)\n * by setting `overflow: hidden` on the document element. A component's shadow\n * stylesheet cannot reach `<html>`, so this is done in JS.\n *\n * The lock is reference-counted at module scope so concurrent locks from stacked\n * surfaces do not clobber one another's saved state. Each host holds at most one\n * count (the per-instance `lock()` / `unlock()` are idempotent), and the\n * controller releases the host's count automatically on disconnect.\n */\nexport class PageScrollLockController implements ReactiveController {\n private _locked = false;\n\n constructor(host: ReactiveElement) {\n host.addController(this);\n }\n\n /** Lock page scroll on behalf of the host. Idempotent per host. */\n public lock(): void {\n if (this._locked) {\n return;\n }\n this._locked = true;\n acquirePageScrollLock();\n }\n\n /** Release the host's lock if it holds one. Idempotent per host. */\n public unlock(): void {\n if (!this._locked) {\n return;\n }\n this._locked = false;\n releasePageScrollLock();\n }\n\n public hostDisconnected(): void {\n this.unlock();\n }\n}\n"],"mappings":";AAyBA,IAAI,IAAY,GACZ,IAAgB;AAEpB,SAAS,IAA8B;AACrC,KAAI,MAAc,GAAG;EACnB,IAAM,IAAO,SAAS;AAEtB,EADA,IAAgB,EAAK,MAAM,UAC3B,EAAK,MAAM,WAAW;;AAExB,MAAa;;AAGf,SAAS,IAA8B;AACjC,OAAc,MAGlB,KACI,MAAc,MAChB,SAAS,gBAAgB,MAAM,WAAW,GAC1C,IAAgB;;AAcpB,IAAa,IAAb,MAAoE;CAGlE,YAAY,GAAuB;AACjC,iBAHgB,IAGhB,EAAK,cAAc,KAAK;;CAI1B,OAAoB;AACd,OAAK,YAGT,KAAK,UAAU,IACf,GAAuB;;CAIzB,SAAsB;AACf,OAAK,YAGV,KAAK,UAAU,IACf,GAAuB;;CAGzB,mBAAgC;AAC9B,OAAK,QAAQ"}
|
|
@@ -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 { SlotPresenceController } from './src/slot-presence-controller.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReactiveController, ReactiveElement } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* A reactive controller that observes whether slotted content matching
|
|
4
|
+
* given CSS selectors is present in the host's light DOM.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the `ObserveSlotPresence` mixin with a composition-based approach.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* class MyComponent extends SpectrumElement {
|
|
11
|
+
* private slotPresence = new SlotPresenceController(this, '[slot="icon"]');
|
|
12
|
+
*
|
|
13
|
+
* get hasIcon(): boolean {
|
|
14
|
+
* return this.slotPresence.isPresent;
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Observing multiple selectors
|
|
22
|
+
* class MyComponent extends SpectrumElement {
|
|
23
|
+
* private slotPresence = new SlotPresenceController(this, [
|
|
24
|
+
* '[slot="icon"]',
|
|
25
|
+
* '[slot="description"]',
|
|
26
|
+
* ]);
|
|
27
|
+
*
|
|
28
|
+
* get hasIcon(): boolean {
|
|
29
|
+
* return this.slotPresence.getPresence('[slot="icon"]');
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* get hasDescription(): boolean {
|
|
33
|
+
* return this.slotPresence.getPresence('[slot="description"]');
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class SlotPresenceController implements ReactiveController {
|
|
39
|
+
private host;
|
|
40
|
+
private selectors;
|
|
41
|
+
private presenceMap;
|
|
42
|
+
private observer;
|
|
43
|
+
constructor(host: ReactiveElement, selectors: string | string[]);
|
|
44
|
+
/**
|
|
45
|
+
* Whether slotted content is present. Use this when observing a single
|
|
46
|
+
* selector. Throws if multiple selectors were provided — use
|
|
47
|
+
* `getPresence(selector)` instead.
|
|
48
|
+
*/
|
|
49
|
+
get isPresent(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Returns whether content matching the given selector is present.
|
|
52
|
+
*/
|
|
53
|
+
getPresence(selector: string): boolean;
|
|
54
|
+
private checkPresence;
|
|
55
|
+
hostConnected(): void;
|
|
56
|
+
hostDisconnected(): void;
|
|
57
|
+
}
|