@patternfly/pfe-core 5.0.4 → 5.0.6

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.
@@ -84,6 +84,12 @@ export declare class ActivedescendantController<Item extends HTMLElement = HTMLE
84
84
  set atFocusedItemIndex(index: number);
85
85
  protected get controlsElements(): HTMLElement[];
86
86
  protected set controlsElements(elements: HTMLElement[]);
87
+ /**
88
+ * Check the source item's focusable state, not the clone's.
89
+ * This is needed because filtering sets `hidden` on the light DOM item,
90
+ * and the MutationObserver sync to clones is asynchronous.
91
+ */
92
+ get atFocusableItems(): Item[];
87
93
  /** All items */
88
94
  get items(): Item[];
89
95
  /**
@@ -92,7 +98,8 @@ export declare class ActivedescendantController<Item extends HTMLElement = HTMLE
92
98
  */
93
99
  set items(items: Item[]);
94
100
  private constructor();
95
- protected initItems(): void;
101
+ /** @internal */
102
+ initItems(): void;
96
103
  hostDisconnected(): void;
97
104
  protected onKeydown(event: KeyboardEvent): void;
98
105
  renderItemsToShadowRoot(): typeof nothing | Node[];
@@ -70,7 +70,10 @@ export class ActivedescendantController extends ATFocusController {
70
70
  super.atFocusedItemIndex = index;
71
71
  const item = this._items.at(this.atFocusedItemIndex);
72
72
  for (const _item of this.items) {
73
- this.options.setItemActive?.(_item, _item === item);
73
+ const isActive = _item === item;
74
+ // Map clone back to original item for setItemActive callback
75
+ const originalItem = __classPrivateFieldGet(this, _ActivedescendantController_shadowToLightMap, "f").get(_item) ?? _item;
76
+ this.options.setItemActive?.(originalItem, isActive);
74
77
  }
75
78
  const container = this.options.getActiveDescendantContainer();
76
79
  if (!ActivedescendantController.supportsCrossRootActiveDescendant) {
@@ -85,6 +88,12 @@ export class ActivedescendantController extends ATFocusController {
85
88
  return __classPrivateFieldGet(this, _ActivedescendantController_controlsElements, "f");
86
89
  }
87
90
  set controlsElements(elements) {
91
+ // Avoid removing/re-adding listeners if elements haven't changed
92
+ // This prevents breaking event listeners during active event dispatch
93
+ if (elements.length === __classPrivateFieldGet(this, _ActivedescendantController_controlsElements, "f").length
94
+ && elements.every((el, i) => el === __classPrivateFieldGet(this, _ActivedescendantController_controlsElements, "f")[i])) {
95
+ return;
96
+ }
88
97
  for (const old of __classPrivateFieldGet(this, _ActivedescendantController_controlsElements, "f")) {
89
98
  old?.removeEventListener('keydown', this.onKeydown);
90
99
  }
@@ -93,6 +102,21 @@ export class ActivedescendantController extends ATFocusController {
93
102
  element.addEventListener('keydown', this.onKeydown);
94
103
  }
95
104
  }
105
+ /**
106
+ * Check the source item's focusable state, not the clone's.
107
+ * This is needed because filtering sets `hidden` on the light DOM item,
108
+ * and the MutationObserver sync to clones is asynchronous.
109
+ */
110
+ get atFocusableItems() {
111
+ return this._items.filter(item => {
112
+ // Map clone to source item to check actual hidden state
113
+ const sourceItem = __classPrivateFieldGet(this, _ActivedescendantController_shadowToLightMap, "f").get(item) ?? item;
114
+ return !!sourceItem
115
+ && sourceItem.ariaHidden !== 'true'
116
+ && !sourceItem.hasAttribute('inert')
117
+ && !sourceItem.hasAttribute('hidden');
118
+ });
119
+ }
96
120
  /** All items */
97
121
  get items() {
98
122
  return this._items;
@@ -130,6 +154,11 @@ export class ActivedescendantController extends ATFocusController {
130
154
  return item;
131
155
  }
132
156
  else {
157
+ // Reuse existing clone if available to maintain stable IDs
158
+ const existingClone = __classPrivateFieldGet(this, _ActivedescendantController_lightToShadowMap, "f").get(item);
159
+ if (existingClone) {
160
+ return existingClone;
161
+ }
133
162
  const clone = item.cloneNode(true);
134
163
  clone.id = getRandomId();
135
164
  __classPrivateFieldGet(this, _ActivedescendantController_lightToShadowMap, "f").set(item, clone);
@@ -160,12 +189,14 @@ export class ActivedescendantController extends ATFocusController {
160
189
  _ActivedescendantController_observing.set(this, false);
161
190
  _ActivedescendantController_listMO.set(this, new MutationObserver(records => __classPrivateFieldGet(this, _ActivedescendantController_instances, "m", _ActivedescendantController_onItemsDOMChange).call(this, records)));
162
191
  _ActivedescendantController_attrMO.set(this, new MutationObserver(records => __classPrivateFieldGet(this, _ActivedescendantController_instances, "m", _ActivedescendantController_onItemAttributeChange).call(this, records)));
192
+ this.initItems();
163
193
  (_a = this.options).getItemValue ?? (_a.getItemValue = function () {
164
194
  return this.value;
165
195
  });
166
196
  }
167
197
  ;
168
198
  ;
199
+ /** @internal */
169
200
  initItems() {
170
201
  __classPrivateFieldGet(this, _ActivedescendantController_attrMO, "f").disconnect();
171
202
  super.initItems();
@@ -1 +1 @@
1
- {"version":3,"file":"activedescendant-controller.js","sourceRoot":"","sources":["activedescendant-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveControllerHost } from 'lit';\n\nimport { type ATFocusControllerOptions, ATFocusController } from './at-focus-controller.js';\n\nimport { isServer, nothing } from 'lit';\nimport { getRandomId } from '../functions/random.js';\nimport { bound } from '../decorators/bound.js';\n\nexport interface ActivedescendantControllerOptions<\n Item extends HTMLElement\n> extends ATFocusControllerOptions<Item> {\n /**\n * Returns a reference to the element which acts as the assistive technology container for\n * the items. In the case of a combobox, this is the input element.\n */\n getActiveDescendantContainer(): HTMLElement | null;\n /**\n * Optional callback to control the assistive technology focus behavior of items.\n * By default, ActivedescendantController will not do anything special to items when they receive\n * assistive technology focus, and will only set the `activedescendant` property on the container.\n * If you provide this callback, ActivedescendantController will call it on your item with the\n * active state. You may use this to set active styles.\n */\n setItemActive?(item: Item, active: boolean): void;\n /**\n * Optional callback to retrieve the value from an option element.\n * By default, retrieves the `value` attribute, or the text content.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionElement\n */\n getItemValue?(item: Item): string;\n}\n\n/**\n * Implements activedescendant pattern, as described in WAI-ARIA practices,\n * [Managing Focus in Composites Using aria-activedescendant][ad]\n *\n * The steps for using the aria-activedescendant method of managing focus are as follows.\n *\n * - When the container element that has a role that supports aria-activedescendant is loaded\n * or created, ensure that:\n * - The container element is included in the tab sequence as described in\n * Keyboard Navigation Between Components or is a focusable element of a composite\n * that implements a roving tabindex.\n * - It has aria-activedescendant=\"IDREF\" where IDREF is the ID of the element within\n * the container that should be identified as active when the widget receives focus.\n * The referenced element needs to meet the DOM relationship requirements described below.\n * - When the container element receives DOM focus, draw a visual focus indicator on the active\n * element and ensure the active element is scrolled into view.\n * - When the composite widget contains focus and the user presses a navigation key that moves\n * focus within the widget, such as an arrow key:\n * - Change the value of aria-activedescendant on the container to refer to the element\n * that should be reported to assistive technologies as active.\n * - Move the visual focus indicator and, if necessary, scrolled the active element into view.\n * - If the design calls for a specific element to be focused the next time a user moves focus\n * into the composite with Tab or Shift+Tab, check if aria-activedescendant is referring to\n * that target element when the container loses focus. If it is not, set aria-activedescendant\n * to refer to the target element.\n *\n * The specification for aria-activedescendant places important restrictions on the\n * DOM relationship between the focused element that has the aria-activedescendant attribute\n * and the element referenced as active by the value of the attribute.\n * One of the following three conditions must be met.\n *\n * 1. The element referenced as active is a DOM descendant of the focused referencing element.\n * 2. The focused referencing element has a value specified for the aria-owns property that\n * includes the ID of the element referenced as active.\n * 3. The focused referencing element has role of combobox, textbox, or searchbox\n * and has aria-controls property referring to an element with a role that supports\n * aria-activedescendant and either:\n * 1. The element referenced as active is a descendant of the controlled element.\n * 2. The controlled element has a value specified for the aria-owns property that includes\n * the ID of the element referenced as active.\n *\n * [ad]: https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant\n */\nexport class ActivedescendantController<\n Item extends HTMLElement = HTMLElement\n> extends ATFocusController<Item> {\n /**\n * When true, the browser supports cross-root ARIA such that the controller does not need\n * to copy item nodes into the controlling nodes' root\n */\n public static get supportsCrossRootActiveDescendant(): boolean {\n return !isServer && 'ariaActiveDescendantElement' in HTMLElement.prototype;\n }\n\n static of<Item extends HTMLElement>(\n host: ReactiveControllerHost,\n options: ActivedescendantControllerOptions<Item>,\n ): ActivedescendantController<Item> {\n return new ActivedescendantController(host, options);\n }\n\n /** Maps from original element to shadow DOM clone */\n #lightToShadowMap = new WeakMap<Item, Item>();\n\n /** Maps from shadow DOM clone to original element */\n #shadowToLightMap = new WeakMap<Item, Item>();\n\n /** Set of item which should not be cloned */\n #noCloneSet = new WeakSet<Item>();\n\n /** Element which controls the list i.e. combobox */\n #controlsElements: HTMLElement[] = [];\n\n #observing = false;\n\n #listMO = new MutationObserver(records => this.#onItemsDOMChange(records));\n\n #attrMO = new MutationObserver(records => this.#onItemAttributeChange(records));\n\n #syncAttr(attributeName: string, fromNode: Item) {\n const toNode = this.#shadowToLightMap.get(fromNode as Item)\n ?? this.#lightToShadowMap.get(fromNode as Item);\n const newVal = fromNode.getAttribute(attributeName);\n const oldVal = toNode?.getAttribute(attributeName);\n if (!fromNode.hasAttribute(attributeName)) {\n toNode?.removeAttribute(attributeName);\n } else if (oldVal !== newVal) {\n toNode?.setAttribute(attributeName, newVal!);\n }\n }\n\n get atFocusedItemIndex(): number {\n return super.atFocusedItemIndex;\n }\n\n /**\n * Rather than setting DOM focus, applies the `aria-activedescendant` attribute,\n * using AriaIDLAttributes for cross-root aria, if supported by the browser\n * @param item item\n */\n set atFocusedItemIndex(index: number) {\n super.atFocusedItemIndex = index;\n const item = this._items.at(this.atFocusedItemIndex);\n for (const _item of this.items) {\n this.options.setItemActive?.(_item, _item === item);\n }\n const container = this.options.getActiveDescendantContainer();\n if (!ActivedescendantController.supportsCrossRootActiveDescendant) {\n container?.setAttribute('aria-activedescendant', item?.id ?? '');\n } else if (container) {\n container.ariaActiveDescendantElement = item ?? null;\n }\n this.host.requestUpdate();\n }\n\n protected get controlsElements(): HTMLElement[] {\n return this.#controlsElements;\n }\n\n protected set controlsElements(elements: HTMLElement[]) {\n for (const old of this.#controlsElements) {\n old?.removeEventListener('keydown', this.onKeydown);\n }\n this.#controlsElements = elements;\n for (const element of this.#controlsElements) {\n element.addEventListener('keydown', this.onKeydown);\n }\n }\n\n /** All items */\n get items() {\n return this._items;\n }\n\n /**\n * Sets the list of items and activates the next activatable item after the current one\n * @param items tabindex items\n */\n override set items(items: Item[]) {\n const container = this.options.getItemsContainer?.() ?? this.host;\n if (!(container instanceof HTMLElement)) {\n throw new Error('items container must be an HTMLElement');\n }\n this.itemsContainerElement = container;\n const { supportsCrossRootActiveDescendant } = ActivedescendantController;\n if (supportsCrossRootActiveDescendant\n || [container] // all nodes are in the same root\n .concat(this.controlsElements)\n .concat(items)\n .every((node, _, a) => node.getRootNode() === a[0].getRootNode())) {\n this._items = items.map(x => {\n if (!supportsCrossRootActiveDescendant) {\n x.id ||= getRandomId();\n }\n return x;\n });\n } else {\n this._items = items?.map((item: Item) => {\n item.removeAttribute('tabindex');\n if (container.contains(item)) {\n item.id ||= getRandomId();\n this.#noCloneSet.add(item);\n this.#shadowToLightMap.set(item, item);\n return item;\n } else {\n const clone = item.cloneNode(true) as Item;\n clone.id = getRandomId();\n this.#lightToShadowMap.set(item, clone);\n this.#shadowToLightMap.set(clone, item);\n // Though efforts were taken to disconnect\n // this observer, it may still be a memory leak\n this.#attrMO.observe(clone, { attributes: true });\n this.#attrMO.observe(item, { attributes: true });\n return clone;\n }\n });\n }\n }\n\n private constructor(\n public host: ReactiveControllerHost,\n protected options: ActivedescendantControllerOptions<Item>,\n ) {\n super(host, options);\n this.options.getItemValue ??= function(this: Item) {\n return (this as unknown as HTMLOptionElement).value;\n };\n }\n\n #onItemsDOMChange(records: MutationRecord[]) {\n for (const { removedNodes } of records) {\n for (const removed of removedNodes as NodeListOf<Item>) {\n this.#lightToShadowMap.get(removed)?.remove();\n this.#lightToShadowMap.delete(removed);\n }\n }\n };\n\n #onItemAttributeChange(records: MutationRecord[]) {\n for (const { target, attributeName } of records) {\n if (attributeName) {\n this.#syncAttr(attributeName, target as Item);\n }\n }\n };\n\n protected override initItems(): void {\n this.#attrMO.disconnect();\n super.initItems();\n this.controlsElements = this.options.getControlsElements?.() ?? [];\n if (!this.#observing && this.itemsContainerElement && this.itemsContainerElement.isConnected) {\n this.#listMO.observe(this.itemsContainerElement, { childList: true });\n this.#observing = true;\n }\n }\n\n hostDisconnected(): void {\n this.controlsElements = [];\n this.#observing = false;\n this.#listMO.disconnect();\n this.#attrMO.disconnect();\n }\n\n @bound\n protected override onKeydown(event: KeyboardEvent): void {\n if (!event.ctrlKey\n && !event.altKey\n && !event.metaKey\n && !!this.atFocusableItems.length) {\n super.onKeydown(event);\n };\n }\n\n public renderItemsToShadowRoot(): typeof nothing | Node[] {\n if (ActivedescendantController.supportsCrossRootActiveDescendant) {\n return nothing;\n } else {\n return this.items?.filter(x => !this.#noCloneSet.has(x));\n }\n }\n}\n"]}
1
+ {"version":3,"file":"activedescendant-controller.js","sourceRoot":"","sources":["activedescendant-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveControllerHost } from 'lit';\n\nimport { type ATFocusControllerOptions, ATFocusController } from './at-focus-controller.js';\n\nimport { isServer, nothing } from 'lit';\nimport { getRandomId } from '../functions/random.js';\nimport { bound } from '../decorators/bound.js';\n\nexport interface ActivedescendantControllerOptions<\n Item extends HTMLElement\n> extends ATFocusControllerOptions<Item> {\n /**\n * Returns a reference to the element which acts as the assistive technology container for\n * the items. In the case of a combobox, this is the input element.\n */\n getActiveDescendantContainer(): HTMLElement | null;\n /**\n * Optional callback to control the assistive technology focus behavior of items.\n * By default, ActivedescendantController will not do anything special to items when they receive\n * assistive technology focus, and will only set the `activedescendant` property on the container.\n * If you provide this callback, ActivedescendantController will call it on your item with the\n * active state. You may use this to set active styles.\n */\n setItemActive?(item: Item, active: boolean): void;\n /**\n * Optional callback to retrieve the value from an option element.\n * By default, retrieves the `value` attribute, or the text content.\n * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionElement\n */\n getItemValue?(item: Item): string;\n}\n\n/**\n * Implements activedescendant pattern, as described in WAI-ARIA practices,\n * [Managing Focus in Composites Using aria-activedescendant][ad]\n *\n * The steps for using the aria-activedescendant method of managing focus are as follows.\n *\n * - When the container element that has a role that supports aria-activedescendant is loaded\n * or created, ensure that:\n * - The container element is included in the tab sequence as described in\n * Keyboard Navigation Between Components or is a focusable element of a composite\n * that implements a roving tabindex.\n * - It has aria-activedescendant=\"IDREF\" where IDREF is the ID of the element within\n * the container that should be identified as active when the widget receives focus.\n * The referenced element needs to meet the DOM relationship requirements described below.\n * - When the container element receives DOM focus, draw a visual focus indicator on the active\n * element and ensure the active element is scrolled into view.\n * - When the composite widget contains focus and the user presses a navigation key that moves\n * focus within the widget, such as an arrow key:\n * - Change the value of aria-activedescendant on the container to refer to the element\n * that should be reported to assistive technologies as active.\n * - Move the visual focus indicator and, if necessary, scrolled the active element into view.\n * - If the design calls for a specific element to be focused the next time a user moves focus\n * into the composite with Tab or Shift+Tab, check if aria-activedescendant is referring to\n * that target element when the container loses focus. If it is not, set aria-activedescendant\n * to refer to the target element.\n *\n * The specification for aria-activedescendant places important restrictions on the\n * DOM relationship between the focused element that has the aria-activedescendant attribute\n * and the element referenced as active by the value of the attribute.\n * One of the following three conditions must be met.\n *\n * 1. The element referenced as active is a DOM descendant of the focused referencing element.\n * 2. The focused referencing element has a value specified for the aria-owns property that\n * includes the ID of the element referenced as active.\n * 3. The focused referencing element has role of combobox, textbox, or searchbox\n * and has aria-controls property referring to an element with a role that supports\n * aria-activedescendant and either:\n * 1. The element referenced as active is a descendant of the controlled element.\n * 2. The controlled element has a value specified for the aria-owns property that includes\n * the ID of the element referenced as active.\n *\n * [ad]: https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant\n */\nexport class ActivedescendantController<\n Item extends HTMLElement = HTMLElement\n> extends ATFocusController<Item> {\n /**\n * When true, the browser supports cross-root ARIA such that the controller does not need\n * to copy item nodes into the controlling nodes' root\n */\n public static get supportsCrossRootActiveDescendant(): boolean {\n return !isServer && 'ariaActiveDescendantElement' in HTMLElement.prototype;\n }\n\n static of<Item extends HTMLElement>(\n host: ReactiveControllerHost,\n options: ActivedescendantControllerOptions<Item>,\n ): ActivedescendantController<Item> {\n return new ActivedescendantController(host, options);\n }\n\n /** Maps from original element to shadow DOM clone */\n #lightToShadowMap = new WeakMap<Item, Item>();\n\n /** Maps from shadow DOM clone to original element */\n #shadowToLightMap = new WeakMap<Item, Item>();\n\n /** Set of item which should not be cloned */\n #noCloneSet = new WeakSet<Item>();\n\n /** Element which controls the list i.e. combobox */\n #controlsElements: HTMLElement[] = [];\n\n #observing = false;\n\n #listMO = new MutationObserver(records => this.#onItemsDOMChange(records));\n\n #attrMO = new MutationObserver(records => this.#onItemAttributeChange(records));\n\n #syncAttr(attributeName: string, fromNode: Item) {\n const toNode = this.#shadowToLightMap.get(fromNode as Item)\n ?? this.#lightToShadowMap.get(fromNode as Item);\n const newVal = fromNode.getAttribute(attributeName);\n const oldVal = toNode?.getAttribute(attributeName);\n if (!fromNode.hasAttribute(attributeName)) {\n toNode?.removeAttribute(attributeName);\n } else if (oldVal !== newVal) {\n toNode?.setAttribute(attributeName, newVal!);\n }\n }\n\n get atFocusedItemIndex(): number {\n return super.atFocusedItemIndex;\n }\n\n /**\n * Rather than setting DOM focus, applies the `aria-activedescendant` attribute,\n * using AriaIDLAttributes for cross-root aria, if supported by the browser\n * @param item item\n */\n set atFocusedItemIndex(index: number) {\n super.atFocusedItemIndex = index;\n const item = this._items.at(this.atFocusedItemIndex);\n for (const _item of this.items) {\n const isActive = _item === item;\n // Map clone back to original item for setItemActive callback\n const originalItem = this.#shadowToLightMap.get(_item) ?? _item;\n this.options.setItemActive?.(originalItem, isActive);\n }\n const container = this.options.getActiveDescendantContainer();\n if (!ActivedescendantController.supportsCrossRootActiveDescendant) {\n container?.setAttribute('aria-activedescendant', item?.id ?? '');\n } else if (container) {\n container.ariaActiveDescendantElement = item ?? null;\n }\n this.host.requestUpdate();\n }\n\n protected get controlsElements(): HTMLElement[] {\n return this.#controlsElements;\n }\n\n protected set controlsElements(elements: HTMLElement[]) {\n // Avoid removing/re-adding listeners if elements haven't changed\n // This prevents breaking event listeners during active event dispatch\n if (elements.length === this.#controlsElements.length\n && elements.every((el, i) => el === this.#controlsElements[i])) {\n return;\n }\n for (const old of this.#controlsElements) {\n old?.removeEventListener('keydown', this.onKeydown);\n }\n this.#controlsElements = elements;\n for (const element of this.#controlsElements) {\n element.addEventListener('keydown', this.onKeydown);\n }\n }\n\n /**\n * Check the source item's focusable state, not the clone's.\n * This is needed because filtering sets `hidden` on the light DOM item,\n * and the MutationObserver sync to clones is asynchronous.\n */\n override get atFocusableItems(): Item[] {\n return this._items.filter(item => {\n // Map clone to source item to check actual hidden state\n const sourceItem = this.#shadowToLightMap.get(item) ?? item;\n return !!sourceItem\n && sourceItem.ariaHidden !== 'true'\n && !sourceItem.hasAttribute('inert')\n && !sourceItem.hasAttribute('hidden');\n });\n }\n\n /** All items */\n get items() {\n return this._items;\n }\n\n /**\n * Sets the list of items and activates the next activatable item after the current one\n * @param items tabindex items\n */\n override set items(items: Item[]) {\n const container = this.options.getItemsContainer?.() ?? this.host;\n if (!(container instanceof HTMLElement)) {\n throw new Error('items container must be an HTMLElement');\n }\n this.itemsContainerElement = container;\n const { supportsCrossRootActiveDescendant } = ActivedescendantController;\n if (supportsCrossRootActiveDescendant\n || [container] // all nodes are in the same root\n .concat(this.controlsElements)\n .concat(items)\n .every((node, _, a) => node.getRootNode() === a[0].getRootNode())) {\n this._items = items.map(x => {\n if (!supportsCrossRootActiveDescendant) {\n x.id ||= getRandomId();\n }\n return x;\n });\n } else {\n this._items = items?.map((item: Item) => {\n item.removeAttribute('tabindex');\n if (container.contains(item)) {\n item.id ||= getRandomId();\n this.#noCloneSet.add(item);\n this.#shadowToLightMap.set(item, item);\n return item;\n } else {\n // Reuse existing clone if available to maintain stable IDs\n const existingClone = this.#lightToShadowMap.get(item);\n if (existingClone) {\n return existingClone;\n }\n const clone = item.cloneNode(true) as Item;\n clone.id = getRandomId();\n this.#lightToShadowMap.set(item, clone);\n this.#shadowToLightMap.set(clone, item);\n // Though efforts were taken to disconnect\n // this observer, it may still be a memory leak\n this.#attrMO.observe(clone, { attributes: true });\n this.#attrMO.observe(item, { attributes: true });\n return clone;\n }\n });\n }\n }\n\n private constructor(\n public host: ReactiveControllerHost,\n protected options: ActivedescendantControllerOptions<Item>,\n ) {\n super(host, options);\n this.initItems();\n this.options.getItemValue ??= function(this: Item) {\n return (this as unknown as HTMLOptionElement).value;\n };\n }\n\n #onItemsDOMChange(records: MutationRecord[]) {\n for (const { removedNodes } of records) {\n for (const removed of removedNodes as NodeListOf<Item>) {\n this.#lightToShadowMap.get(removed)?.remove();\n this.#lightToShadowMap.delete(removed);\n }\n }\n };\n\n #onItemAttributeChange(records: MutationRecord[]) {\n for (const { target, attributeName } of records) {\n if (attributeName) {\n this.#syncAttr(attributeName, target as Item);\n }\n }\n };\n\n /** @internal */\n override initItems(): void {\n this.#attrMO.disconnect();\n super.initItems();\n this.controlsElements = this.options.getControlsElements?.() ?? [];\n if (!this.#observing && this.itemsContainerElement && this.itemsContainerElement.isConnected) {\n this.#listMO.observe(this.itemsContainerElement, { childList: true });\n this.#observing = true;\n }\n }\n\n hostDisconnected(): void {\n this.controlsElements = [];\n this.#observing = false;\n this.#listMO.disconnect();\n this.#attrMO.disconnect();\n }\n\n @bound\n protected override onKeydown(event: KeyboardEvent): void {\n if (!event.ctrlKey\n && !event.altKey\n && !event.metaKey\n && !!this.atFocusableItems.length) {\n super.onKeydown(event);\n };\n }\n\n public renderItemsToShadowRoot(): typeof nothing | Node[] {\n if (ActivedescendantController.supportsCrossRootActiveDescendant) {\n return nothing;\n } else {\n return this.items?.filter(x => !this.#noCloneSet.has(x));\n }\n }\n}\n"]}
@@ -31,7 +31,7 @@ export declare abstract class ATFocusController<Item extends HTMLElement> {
31
31
  * wrap around to the other side of the list.
32
32
  */
33
33
  get atFocusedItemIndex(): number;
34
- set atFocusedItemIndex(index: number);
34
+ set atFocusedItemIndex(requestedIndex: number);
35
35
  /** Elements which control the items container e.g. a combobox input */
36
36
  protected get controlsElements(): HTMLElement[];
37
37
  /** All items which are able to receive assistive technology focus */
@@ -41,9 +41,12 @@ export declare abstract class ATFocusController<Item extends HTMLElement> {
41
41
  set itemsContainerElement(container: HTMLElement | null);
42
42
  constructor(host: ReactiveControllerHost, options: ATFocusControllerOptions<Item>);
43
43
  /**
44
- * Initialize the items and itemsContainerElement fields
44
+ * Initialize the items and itemsContainerElement fields.
45
+ * Call this when the list of items has changed
46
+ * (e.g. when a parent controller sets items).
47
+ * @internal not for use by element authors
45
48
  */
46
- protected initItems(): void;
49
+ initItems(): void;
47
50
  hostConnected(): void;
48
51
  hostDisconnected(): void;
49
52
  hostUpdate(): void;
@@ -1,7 +1,6 @@
1
- var _ATFocusController_instances, _ATFocusController_itemsContainerElement, _ATFocusController_atFocusedItemIndex, _ATFocusController_initContainer;
1
+ var _ATFocusController_instances, _ATFocusController_itemsContainerElement, _ATFocusController_atFocusedItemIndex, _ATFocusController_initContainer, _ATFocusController_getNextFocusableItem;
2
2
  import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib";
3
3
  import { isServer } from 'lit';
4
- import { bound } from '../decorators/bound.js';
5
4
  function isATFocusableItem(el) {
6
5
  return !!el
7
6
  && el.ariaHidden !== 'true'
@@ -17,33 +16,41 @@ export class ATFocusController {
17
16
  get atFocusedItemIndex() {
18
17
  return __classPrivateFieldGet(this, _ATFocusController_atFocusedItemIndex, "f");
19
18
  }
20
- set atFocusedItemIndex(index) {
21
- const previousIndex = __classPrivateFieldGet(this, _ATFocusController_atFocusedItemIndex, "f");
22
- const direction = index > previousIndex ? 1 : -1;
19
+ set atFocusedItemIndex(requestedIndex) {
23
20
  const { items, atFocusableItems } = this;
24
- const itemsIndexOfLastATFocusableItem = items.indexOf(this.atFocusableItems.at(-1));
25
- let itemToGainFocus = items.at(index);
26
- let itemToGainFocusIsFocusable = atFocusableItems.includes(itemToGainFocus);
27
- if (atFocusableItems.length) {
28
- let count = 0;
29
- while (!itemToGainFocus || !itemToGainFocusIsFocusable && count++ <= 1000) {
30
- if (index < 0) {
31
- index = itemsIndexOfLastATFocusableItem;
32
- }
33
- else if (index >= itemsIndexOfLastATFocusableItem) {
34
- index = 0;
35
- }
36
- else {
37
- index = index + direction;
38
- }
39
- itemToGainFocus = items.at(index);
40
- itemToGainFocusIsFocusable = atFocusableItems.includes(itemToGainFocus);
41
- }
42
- if (count >= 1000) {
43
- throw new Error('Could not atFocusedItemIndex');
44
- }
21
+ if (!atFocusableItems.length) {
22
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, requestedIndex, "f");
23
+ return;
24
+ }
25
+ // Fast path: requested item is already focusable
26
+ if (requestedIndex >= 0
27
+ && requestedIndex < items.length
28
+ && atFocusableItems.includes(items[requestedIndex])) {
29
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, requestedIndex, "f");
30
+ return;
31
+ }
32
+ const lastFocusableIndex = items.indexOf(atFocusableItems.at(-1));
33
+ // Navigated before start → wrap to last focusable
34
+ if (requestedIndex < 0) {
35
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, lastFocusableIndex, "f");
36
+ return;
45
37
  }
46
- __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, index, "f");
38
+ const firstFocusableIndex = items.indexOf(atFocusableItems[0]);
39
+ // Navigated past end or past last focusable → wrap to first focusable
40
+ if (requestedIndex >= items.length
41
+ || requestedIndex > lastFocusableIndex) {
42
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, firstFocusableIndex, "f");
43
+ return;
44
+ }
45
+ // Before first focusable (e.g. disabled placeholder at index 0).
46
+ // ArrowUp from first focusable → wrap to last; otherwise snap to first.
47
+ if (requestedIndex < firstFocusableIndex) {
48
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, __classPrivateFieldGet(this, _ATFocusController_atFocusedItemIndex, "f") === firstFocusableIndex ? lastFocusableIndex
49
+ : firstFocusableIndex, "f");
50
+ return;
51
+ }
52
+ // Mid-list non-focusable item: find nearest focusable in the navigation direction
53
+ __classPrivateFieldSet(this, _ATFocusController_atFocusedItemIndex, items.indexOf(__classPrivateFieldGet(this, _ATFocusController_instances, "m", _ATFocusController_getNextFocusableItem).call(this, requestedIndex)), "f");
47
54
  }
48
55
  /** Elements which control the items container e.g. a combobox input */
49
56
  get controlsElements() {
@@ -75,7 +82,10 @@ export class ATFocusController {
75
82
  this.host.updateComplete.then(() => this.initItems());
76
83
  }
77
84
  /**
78
- * Initialize the items and itemsContainerElement fields
85
+ * Initialize the items and itemsContainerElement fields.
86
+ * Call this when the list of items has changed
87
+ * (e.g. when a parent controller sets items).
88
+ * @internal not for use by element authors
79
89
  */
80
90
  initItems() {
81
91
  this.items = this.options.getItems();
@@ -136,24 +146,30 @@ export class ATFocusController {
136
146
  event.stopPropagation();
137
147
  event.preventDefault();
138
148
  break;
139
- case 'Home':
149
+ case 'Home': {
140
150
  if (!(event.target instanceof HTMLElement
141
151
  && (event.target.hasAttribute('aria-activedescendant')
142
152
  || event.target.ariaActiveDescendantElement))) {
143
- this.atFocusedItemIndex = 0;
153
+ // Use first focusable index so the setter doesn't see 0 (reserved for Up-from-first wrap).
154
+ const first = this.atFocusableItems.at(0);
155
+ this.atFocusedItemIndex = first != null ? this.items.indexOf(first) : 0;
144
156
  event.stopPropagation();
145
157
  event.preventDefault();
146
158
  }
147
159
  break;
148
- case 'End':
160
+ }
161
+ case 'End': {
149
162
  if (!(event.target instanceof HTMLElement
150
163
  && (event.target.hasAttribute('aria-activedescendant')
151
164
  || event.target.ariaActiveDescendantElement))) {
152
- this.atFocusedItemIndex = this.items.length - 1;
165
+ // Use last focusable index for consistency with lists that have non-focusable items.
166
+ const last = this.atFocusableItems.at(-1);
167
+ this.atFocusedItemIndex = last != null ? this.items.indexOf(last) : this.items.length - 1;
153
168
  event.stopPropagation();
154
169
  event.preventDefault();
155
170
  }
156
171
  break;
172
+ }
157
173
  default:
158
174
  break;
159
175
  }
@@ -164,5 +180,13 @@ export class ATFocusController {
164
180
  _ATFocusController_itemsContainerElement = new WeakMap(), _ATFocusController_atFocusedItemIndex = new WeakMap(), _ATFocusController_instances = new WeakSet(), _ATFocusController_initContainer = function _ATFocusController_initContainer() {
165
181
  return this.options.getItemsContainer?.()
166
182
  ?? (!isServer && this.host instanceof HTMLElement ? this.host : null);
183
+ }, _ATFocusController_getNextFocusableItem = function _ATFocusController_getNextFocusableItem(requestedIndex) {
184
+ const { items, atFocusableItems } = this;
185
+ if (requestedIndex > __classPrivateFieldGet(this, _ATFocusController_atFocusedItemIndex, "f")) {
186
+ return atFocusableItems.find(item => items.indexOf(item) > requestedIndex);
187
+ }
188
+ else {
189
+ return atFocusableItems.findLast(item => items.indexOf(item) < requestedIndex);
190
+ }
167
191
  };
168
192
  //# sourceMappingURL=at-focus-controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"at-focus-controller.js","sourceRoot":"","sources":["at-focus-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { isServer, type ReactiveControllerHost } from 'lit';\nimport { bound } from '../decorators/bound.js';\n\nfunction isATFocusableItem(el: Element): el is HTMLElement {\n return !!el\n && el.ariaHidden !== 'true'\n && !el.hasAttribute('inert')\n && !el.hasAttribute('hidden');\n}\n\nexport interface ATFocusControllerOptions<Item extends HTMLElement> {\n /**\n * Callback to return the list of items\n */\n getItems(): Item[];\n /**\n * Callback to return the listbox container element\n */\n getItemsContainer?(): HTMLElement | null;\n /**\n * Callback to return the direction of navigation in the list box.\n */\n getOrientation?(): 'horizontal' | 'vertical' | 'both' | 'undefined';\n /**\n * Function returning the DOM nodes which are accessibility controllers of item container\n * e.g. the button toggle and combobox input which control a listbox.\n */\n getControlsElements?(): HTMLElement[];\n}\n\nexport abstract class ATFocusController<Item extends HTMLElement> {\n #itemsContainerElement: HTMLElement | null = null;\n\n #atFocusedItemIndex = -1;\n\n protected _items: Item[] = [];\n\n /** All items */\n abstract items: Item[];\n\n /**\n * Index of the Item which currently has assistive technology focus\n * Set this to change focus. Setting to an out-of-bounds value will\n * wrap around to the other side of the list.\n */\n get atFocusedItemIndex() {\n return this.#atFocusedItemIndex;\n }\n\n set atFocusedItemIndex(index: number) {\n const previousIndex = this.#atFocusedItemIndex;\n const direction = index > previousIndex ? 1 : -1;\n const { items, atFocusableItems } = this;\n const itemsIndexOfLastATFocusableItem = items.indexOf(this.atFocusableItems.at(-1)!);\n let itemToGainFocus = items.at(index);\n let itemToGainFocusIsFocusable = atFocusableItems.includes(itemToGainFocus!);\n if (atFocusableItems.length) {\n let count = 0;\n while (!itemToGainFocus || !itemToGainFocusIsFocusable && count++ <= 1000) {\n if (index < 0) {\n index = itemsIndexOfLastATFocusableItem;\n } else if (index >= itemsIndexOfLastATFocusableItem) {\n index = 0;\n } else {\n index = index + direction;\n }\n itemToGainFocus = items.at(index);\n itemToGainFocusIsFocusable = atFocusableItems.includes(itemToGainFocus!);\n }\n if (count >= 1000) {\n throw new Error('Could not atFocusedItemIndex');\n }\n }\n this.#atFocusedItemIndex = index;\n }\n\n /** Elements which control the items container e.g. a combobox input */\n protected get controlsElements(): HTMLElement[] {\n return this.options.getControlsElements?.() ?? [];\n }\n\n /** All items which are able to receive assistive technology focus */\n get atFocusableItems(): Item[] {\n return this._items.filter(isATFocusableItem);\n }\n\n /** The element containing focusable items, e.g. a listbox */\n get itemsContainerElement() {\n return this.#itemsContainerElement ?? null;\n }\n\n set itemsContainerElement(container: HTMLElement | null) {\n if (container !== this.#itemsContainerElement) {\n this.#itemsContainerElement?.removeEventListener('keydown', this.onKeydown);\n this.#itemsContainerElement = container;\n this.#itemsContainerElement?.addEventListener('keydown', this.onKeydown);\n this.host.requestUpdate();\n }\n }\n\n constructor(\n public host: ReactiveControllerHost,\n protected options: ATFocusControllerOptions<Item>,\n ) {\n this.host.updateComplete.then(() => this.initItems());\n }\n\n /**\n * Initialize the items and itemsContainerElement fields\n */\n protected initItems(): void {\n this.items = this.options.getItems();\n this.itemsContainerElement ??= this.#initContainer();\n }\n\n hostConnected(): void {\n this.hostUpdate();\n }\n\n hostDisconnected(): void {\n this.#itemsContainerElement?.removeEventListener('keydown', this.onKeydown);\n }\n\n hostUpdate(): void {\n this.itemsContainerElement ??= this.#initContainer();\n }\n\n #initContainer() {\n return this.options.getItemsContainer?.()\n ?? (!isServer && this.host instanceof HTMLElement ? this.host : null);\n }\n\n /**\n * Override and conditionally call `super.onKeydown` to filter out keyboard events\n * which should not result in a focus change. Ensure that subclass' method is bound\n * @param event keyboard event\n */\n protected onKeydown(event: KeyboardEvent): void {\n const orientation = this.options.getOrientation?.() ?? this\n .#itemsContainerElement\n ?.getAttribute('aria-orientation') as\n 'horizontal' | 'vertical' | 'grid' | 'undefined';\n\n const item = this._items.at(this.atFocusedItemIndex);\n\n const horizontalOnly =\n orientation === 'horizontal'\n || item?.tagName === 'SELECT'\n || item?.getAttribute('role') === 'spinbutton';\n\n const verticalOnly = orientation === 'vertical';\n\n switch (event.key) {\n case 'ArrowLeft':\n if (verticalOnly) {\n return;\n }\n this.atFocusedItemIndex--;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowRight':\n if (verticalOnly) {\n return;\n }\n this.atFocusedItemIndex++;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowUp':\n if (horizontalOnly) {\n return;\n }\n this.atFocusedItemIndex--;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowDown':\n if (horizontalOnly) {\n return;\n }\n this.atFocusedItemIndex++;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'Home':\n if (!(event.target instanceof HTMLElement\n && (event.target.hasAttribute('aria-activedescendant')\n || event.target.ariaActiveDescendantElement))) {\n this.atFocusedItemIndex = 0;\n event.stopPropagation();\n event.preventDefault();\n }\n break;\n case 'End':\n if (!(event.target instanceof HTMLElement\n && (event.target.hasAttribute('aria-activedescendant')\n || event.target.ariaActiveDescendantElement))) {\n this.atFocusedItemIndex = this.items.length - 1;\n event.stopPropagation();\n event.preventDefault();\n }\n break;\n default:\n break;\n }\n this.host.requestUpdate();\n };\n}\n"]}
1
+ {"version":3,"file":"at-focus-controller.js","sourceRoot":"","sources":["at-focus-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { isServer, type ReactiveControllerHost } from 'lit';\n\nfunction isATFocusableItem(el: Element): el is HTMLElement {\n return !!el\n && el.ariaHidden !== 'true'\n && !el.hasAttribute('inert')\n && !el.hasAttribute('hidden');\n}\n\nexport interface ATFocusControllerOptions<Item extends HTMLElement> {\n /**\n * Callback to return the list of items\n */\n getItems(): Item[];\n /**\n * Callback to return the listbox container element\n */\n getItemsContainer?(): HTMLElement | null;\n /**\n * Callback to return the direction of navigation in the list box.\n */\n getOrientation?(): 'horizontal' | 'vertical' | 'both' | 'undefined';\n /**\n * Function returning the DOM nodes which are accessibility controllers of item container\n * e.g. the button toggle and combobox input which control a listbox.\n */\n getControlsElements?(): HTMLElement[];\n}\n\nexport abstract class ATFocusController<Item extends HTMLElement> {\n #itemsContainerElement: HTMLElement | null = null;\n\n #atFocusedItemIndex = -1;\n\n protected _items: Item[] = [];\n\n /** All items */\n abstract items: Item[];\n\n /**\n * Index of the Item which currently has assistive technology focus\n * Set this to change focus. Setting to an out-of-bounds value will\n * wrap around to the other side of the list.\n */\n get atFocusedItemIndex() {\n return this.#atFocusedItemIndex;\n }\n\n set atFocusedItemIndex(requestedIndex: number) {\n const { items, atFocusableItems } = this;\n\n if (!atFocusableItems.length) {\n this.#atFocusedItemIndex = requestedIndex;\n return;\n }\n\n // Fast path: requested item is already focusable\n if (requestedIndex >= 0\n && requestedIndex < items.length\n && atFocusableItems.includes(items[requestedIndex])) {\n this.#atFocusedItemIndex = requestedIndex;\n return;\n }\n\n const lastFocusableIndex = items.indexOf(atFocusableItems.at(-1)!);\n\n // Navigated before start → wrap to last focusable\n if (requestedIndex < 0) {\n this.#atFocusedItemIndex = lastFocusableIndex;\n return;\n }\n\n const firstFocusableIndex = items.indexOf(atFocusableItems[0]);\n\n // Navigated past end or past last focusable → wrap to first focusable\n if (requestedIndex >= items.length\n || requestedIndex > lastFocusableIndex) {\n this.#atFocusedItemIndex = firstFocusableIndex;\n return;\n }\n\n // Before first focusable (e.g. disabled placeholder at index 0).\n // ArrowUp from first focusable → wrap to last; otherwise snap to first.\n if (requestedIndex < firstFocusableIndex) {\n this.#atFocusedItemIndex =\n this.#atFocusedItemIndex === firstFocusableIndex ? lastFocusableIndex\n : firstFocusableIndex;\n return;\n }\n\n // Mid-list non-focusable item: find nearest focusable in the navigation direction\n this.#atFocusedItemIndex =\n items.indexOf(this.#getNextFocusableItem(requestedIndex));\n }\n\n /** Elements which control the items container e.g. a combobox input */\n protected get controlsElements(): HTMLElement[] {\n return this.options.getControlsElements?.() ?? [];\n }\n\n /** All items which are able to receive assistive technology focus */\n get atFocusableItems(): Item[] {\n return this._items.filter(isATFocusableItem);\n }\n\n /** The element containing focusable items, e.g. a listbox */\n get itemsContainerElement() {\n return this.#itemsContainerElement ?? null;\n }\n\n set itemsContainerElement(container: HTMLElement | null) {\n if (container !== this.#itemsContainerElement) {\n this.#itemsContainerElement?.removeEventListener('keydown', this.onKeydown);\n this.#itemsContainerElement = container;\n this.#itemsContainerElement?.addEventListener('keydown', this.onKeydown);\n this.host.requestUpdate();\n }\n }\n\n constructor(\n public host: ReactiveControllerHost,\n protected options: ATFocusControllerOptions<Item>,\n ) {\n this.host.updateComplete.then(() => this.initItems());\n }\n\n /**\n * Initialize the items and itemsContainerElement fields.\n * Call this when the list of items has changed\n * (e.g. when a parent controller sets items).\n * @internal not for use by element authors\n */\n initItems(): void {\n this.items = this.options.getItems();\n this.itemsContainerElement ??= this.#initContainer();\n }\n\n hostConnected(): void {\n this.hostUpdate();\n }\n\n hostDisconnected(): void {\n this.#itemsContainerElement?.removeEventListener('keydown', this.onKeydown);\n }\n\n hostUpdate(): void {\n this.itemsContainerElement ??= this.#initContainer();\n }\n\n #initContainer() {\n return this.options.getItemsContainer?.()\n ?? (!isServer && this.host instanceof HTMLElement ? this.host : null);\n }\n\n /**\n * When setting atFocusedItemIndex, and the current focus is on a\n * mid-list non-focusable item: find nearest focusable in the navigation direction.\n * disabled items will be skipped, and out of bounds items will be wrapped\n *\n * @param requestedIndex the desired index\n */\n #getNextFocusableItem(requestedIndex: number) {\n const { items, atFocusableItems } = this;\n if (requestedIndex > this.#atFocusedItemIndex) {\n return atFocusableItems.find(item => items.indexOf(item) > requestedIndex)!;\n } else {\n return atFocusableItems.findLast(item => items.indexOf(item) < requestedIndex)!;\n }\n }\n\n /**\n * Override and conditionally call `super.onKeydown` to filter out keyboard events\n * which should not result in a focus change. Ensure that subclass' method is bound\n * @param event keyboard event\n */\n protected onKeydown(event: KeyboardEvent): void {\n const orientation = this.options.getOrientation?.() ?? this\n .#itemsContainerElement\n ?.getAttribute('aria-orientation') as\n 'horizontal' | 'vertical' | 'grid' | 'undefined';\n\n const item = this._items.at(this.atFocusedItemIndex);\n\n const horizontalOnly =\n orientation === 'horizontal'\n || item?.tagName === 'SELECT'\n || item?.getAttribute('role') === 'spinbutton';\n\n const verticalOnly = orientation === 'vertical';\n\n switch (event.key) {\n case 'ArrowLeft':\n if (verticalOnly) {\n return;\n }\n this.atFocusedItemIndex--;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowRight':\n if (verticalOnly) {\n return;\n }\n this.atFocusedItemIndex++;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowUp':\n if (horizontalOnly) {\n return;\n }\n this.atFocusedItemIndex--;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'ArrowDown':\n if (horizontalOnly) {\n return;\n }\n this.atFocusedItemIndex++;\n event.stopPropagation();\n event.preventDefault();\n break;\n case 'Home': {\n if (!(event.target instanceof HTMLElement\n && (event.target.hasAttribute('aria-activedescendant')\n || event.target.ariaActiveDescendantElement))) {\n // Use first focusable index so the setter doesn't see 0 (reserved for Up-from-first wrap).\n const first = this.atFocusableItems.at(0);\n this.atFocusedItemIndex = first != null ? this.items.indexOf(first) : 0;\n event.stopPropagation();\n event.preventDefault();\n }\n break;\n }\n case 'End': {\n if (!(event.target instanceof HTMLElement\n && (event.target.hasAttribute('aria-activedescendant')\n || event.target.ariaActiveDescendantElement))) {\n // Use last focusable index for consistency with lists that have non-focusable items.\n const last = this.atFocusableItems.at(-1);\n this.atFocusedItemIndex = last != null ? this.items.indexOf(last) : this.items.length - 1;\n event.stopPropagation();\n event.preventDefault();\n }\n break;\n }\n default:\n break;\n }\n this.host.requestUpdate();\n };\n}\n"]}
@@ -81,8 +81,8 @@ export interface ComboboxControllerOptions<Item extends HTMLElement> extends Omi
81
81
  */
82
82
  export declare class ComboboxController<Item extends HTMLElement> implements ReactiveController {
83
83
  #private;
84
- host: ReactiveControllerHost;
85
- static of<T extends HTMLElement>(host: ReactiveControllerHost, options: ComboboxControllerOptions<T>): ComboboxController<T>;
84
+ host: ReactiveControllerHost & HTMLElement;
85
+ static of<T extends HTMLElement>(host: ReactiveControllerHost & HTMLElement, options: ComboboxControllerOptions<T>): ComboboxController<T>;
86
86
  /**
87
87
  * Whether the `ariaActiveDescendantElement` IDL attribute is supported for cross-root ARIA.
88
88
  */
@@ -109,7 +109,7 @@ export declare class ComboboxController<Item extends HTMLElement> implements Rea
109
109
  hostUpdated(): void;
110
110
  hostDisconnected(): void;
111
111
  disconnect(): void;
112
- _onFocusoutElement(): Promise<void>;
112
+ private _onFocusoutElement;
113
113
  /**
114
114
  * For Browsers which do not support `ariaActiveDescendantElement`, we must clone
115
115
  * the listbox items into the same root as the combobox input
@@ -1,4 +1,4 @@
1
- var _ComboboxController_instances, _a, _ComboboxController_alert, _ComboboxController_alertTemplate, _ComboboxController_lb, _ComboboxController_fc, _ComboboxController_preventListboxGainingFocus, _ComboboxController_input, _ComboboxController_button, _ComboboxController_listbox, _ComboboxController_buttonInitialRole, _ComboboxController_mo, _ComboboxController_microcopy, _ComboboxController_hasTextInput_get, _ComboboxController_focusedItem_get, _ComboboxController_element_get, _ComboboxController_init, _ComboboxController_initListbox, _ComboboxController_initButton, _ComboboxController_initInput, _ComboboxController_initLabels, _ComboboxController_initController, _ComboboxController_initItems, _ComboboxController_show, _ComboboxController_hide, _ComboboxController_toggle, _ComboboxController_translate, _ComboboxController_announce, _ComboboxController_filterItems, _ComboboxController_onClickButton, _ComboboxController_onClickListbox, _ComboboxController_onKeydownInput, _ComboboxController_onKeyupInput, _ComboboxController_onKeydownButton, _ComboboxController_onKeydownListbox, _ComboboxController_onFocusoutListbox, _ComboboxController_onKeydownToggleButton;
1
+ var _ComboboxController_instances, _a, _ComboboxController_alert, _ComboboxController_alertTemplate, _ComboboxController_lb, _ComboboxController_fc, _ComboboxController_initializing, _ComboboxController_preventListboxGainingFocus, _ComboboxController_input, _ComboboxController_button, _ComboboxController_listbox, _ComboboxController_buttonInitialRole, _ComboboxController_buttonHasMouseDown, _ComboboxController_mo, _ComboboxController_microcopy, _ComboboxController_hasTextInput_get, _ComboboxController_focusedItem_get, _ComboboxController_element_get, _ComboboxController_init, _ComboboxController_initListbox, _ComboboxController_initButton, _ComboboxController_initInput, _ComboboxController_initLabels, _ComboboxController_initController, _ComboboxController_initItems, _ComboboxController_show, _ComboboxController_hide, _ComboboxController_toggle, _ComboboxController_translate, _ComboboxController_announce, _ComboboxController_filterItems, _ComboboxController_onClickButton, _ComboboxController_onMousedownButton, _ComboboxController_onMouseupButton, _ComboboxController_onClickListbox, _ComboboxController_onKeydownInput, _ComboboxController_onKeyupInput, _ComboboxController_onKeydownButton, _ComboboxController_onKeydownListbox, _ComboboxController_onFocusoutListbox, _ComboboxController_onKeydownToggleButton;
2
2
  import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib";
3
3
  import { isServer, nothing } from 'lit';
4
4
  import { ListboxController, isItem, isItemDisabled } from './listbox-controller.js';
@@ -89,6 +89,7 @@ export class ComboboxController {
89
89
  }
90
90
  set items(value) {
91
91
  __classPrivateFieldGet(this, _ComboboxController_lb, "f").items = value;
92
+ __classPrivateFieldGet(this, _ComboboxController_fc, "f")?.initItems();
92
93
  }
93
94
  /** Whether the combobox is disabled */
94
95
  get disabled() {
@@ -116,11 +117,13 @@ export class ComboboxController {
116
117
  this.host = host;
117
118
  _ComboboxController_lb.set(this, void 0);
118
119
  _ComboboxController_fc.set(this, void 0);
120
+ _ComboboxController_initializing.set(this, false);
119
121
  _ComboboxController_preventListboxGainingFocus.set(this, false);
120
122
  _ComboboxController_input.set(this, null);
121
123
  _ComboboxController_button.set(this, null);
122
124
  _ComboboxController_listbox.set(this, null);
123
125
  _ComboboxController_buttonInitialRole.set(this, null);
126
+ _ComboboxController_buttonHasMouseDown.set(this, false);
124
127
  _ComboboxController_mo.set(this, new MutationObserver(() => __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initItems).call(this)));
125
128
  _ComboboxController_microcopy.set(this, new Map(Object.entries({
126
129
  dimmed: {
@@ -159,6 +162,15 @@ export class ComboboxController {
159
162
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_hide).call(this);
160
163
  }
161
164
  });
165
+ /**
166
+ * Distinguish click-to-toggle vs Tab/Shift+Tab
167
+ */
168
+ _ComboboxController_onMousedownButton.set(this, () => {
169
+ __classPrivateFieldSet(this, _ComboboxController_buttonHasMouseDown, true, "f");
170
+ });
171
+ _ComboboxController_onMouseupButton.set(this, () => {
172
+ __classPrivateFieldSet(this, _ComboboxController_buttonHasMouseDown, false, "f");
173
+ });
162
174
  _ComboboxController_onClickListbox.set(this, (event) => {
163
175
  if (!this.multi && event.composedPath().some(this.options.isItem)) {
164
176
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_hide).call(this);
@@ -309,8 +321,13 @@ export class ComboboxController {
309
321
  _ComboboxController_onFocusoutListbox.set(this, (event) => {
310
322
  if (!__classPrivateFieldGet(this, _ComboboxController_instances, "a", _ComboboxController_hasTextInput_get) && this.options.isExpanded()) {
311
323
  const root = __classPrivateFieldGet(this, _ComboboxController_instances, "a", _ComboboxController_element_get)?.getRootNode();
324
+ // Check if focus moved to the toggle button via mouse click
325
+ // If so, let the click handler manage toggle (prevents double-toggle)
326
+ // But if focus moved via Shift+Tab (no mousedown), we should still hide
327
+ const isClickOnToggleButton = event.relatedTarget === __classPrivateFieldGet(this, _ComboboxController_button, "f") && __classPrivateFieldGet(this, _ComboboxController_buttonHasMouseDown, "f");
312
328
  if ((root instanceof ShadowRoot || root instanceof Document)
313
- && !this.items.includes(event.relatedTarget)) {
329
+ && !this.items.includes(event.relatedTarget)
330
+ && !isClickOnToggleButton) {
314
331
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_hide).call(this);
315
332
  }
316
333
  }
@@ -382,7 +399,7 @@ export class ComboboxController {
382
399
  this.hostUpdated();
383
400
  }
384
401
  hostUpdated() {
385
- if (!__classPrivateFieldGet(this, _ComboboxController_fc, "f")) {
402
+ if (!__classPrivateFieldGet(this, _ComboboxController_fc, "f") && !__classPrivateFieldGet(this, _ComboboxController_initializing, "f")) {
386
403
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_init).call(this);
387
404
  }
388
405
  const expanded = this.options.isExpanded();
@@ -424,7 +441,7 @@ export class ComboboxController {
424
441
  }
425
442
  }
426
443
  }
427
- _a = ComboboxController, _ComboboxController_lb = new WeakMap(), _ComboboxController_fc = new WeakMap(), _ComboboxController_preventListboxGainingFocus = new WeakMap(), _ComboboxController_input = new WeakMap(), _ComboboxController_button = new WeakMap(), _ComboboxController_listbox = new WeakMap(), _ComboboxController_buttonInitialRole = new WeakMap(), _ComboboxController_mo = new WeakMap(), _ComboboxController_microcopy = new WeakMap(), _ComboboxController_onClickButton = new WeakMap(), _ComboboxController_onClickListbox = new WeakMap(), _ComboboxController_onKeydownInput = new WeakMap(), _ComboboxController_onKeyupInput = new WeakMap(), _ComboboxController_onKeydownButton = new WeakMap(), _ComboboxController_onKeydownListbox = new WeakMap(), _ComboboxController_onFocusoutListbox = new WeakMap(), _ComboboxController_onKeydownToggleButton = new WeakMap(), _ComboboxController_instances = new WeakSet(), _ComboboxController_hasTextInput_get = function _ComboboxController_hasTextInput_get() {
444
+ _a = ComboboxController, _ComboboxController_lb = new WeakMap(), _ComboboxController_fc = new WeakMap(), _ComboboxController_initializing = new WeakMap(), _ComboboxController_preventListboxGainingFocus = new WeakMap(), _ComboboxController_input = new WeakMap(), _ComboboxController_button = new WeakMap(), _ComboboxController_listbox = new WeakMap(), _ComboboxController_buttonInitialRole = new WeakMap(), _ComboboxController_buttonHasMouseDown = new WeakMap(), _ComboboxController_mo = new WeakMap(), _ComboboxController_microcopy = new WeakMap(), _ComboboxController_onClickButton = new WeakMap(), _ComboboxController_onMousedownButton = new WeakMap(), _ComboboxController_onMouseupButton = new WeakMap(), _ComboboxController_onClickListbox = new WeakMap(), _ComboboxController_onKeydownInput = new WeakMap(), _ComboboxController_onKeyupInput = new WeakMap(), _ComboboxController_onKeydownButton = new WeakMap(), _ComboboxController_onKeydownListbox = new WeakMap(), _ComboboxController_onFocusoutListbox = new WeakMap(), _ComboboxController_onKeydownToggleButton = new WeakMap(), _ComboboxController_instances = new WeakSet(), _ComboboxController_hasTextInput_get = function _ComboboxController_hasTextInput_get() {
428
445
  return this.options.getComboboxInput();
429
446
  }, _ComboboxController_focusedItem_get = function _ComboboxController_focusedItem_get() {
430
447
  return __classPrivateFieldGet(this, _ComboboxController_fc, "f")?.items.at(Math.max(__classPrivateFieldGet(this, _ComboboxController_fc, "f")?.atFocusedItemIndex ?? -1, 0)) ?? null;
@@ -440,6 +457,7 @@ _a = ComboboxController, _ComboboxController_lb = new WeakMap(), _ComboboxContro
440
457
  * Order of operations is important
441
458
  */
442
459
  async function _ComboboxController_init() {
460
+ __classPrivateFieldSet(this, _ComboboxController_initializing, true, "f");
443
461
  await this.host.updateComplete;
444
462
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initListbox).call(this);
445
463
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initItems).call(this);
@@ -447,6 +465,7 @@ async function _ComboboxController_init() {
447
465
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initInput).call(this);
448
466
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initLabels).call(this);
449
467
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initController).call(this);
468
+ __classPrivateFieldSet(this, _ComboboxController_initializing, false, "f");
450
469
  }, _ComboboxController_initListbox = function _ComboboxController_initListbox() {
451
470
  var _b;
452
471
  __classPrivateFieldGet(this, _ComboboxController_mo, "f").disconnect();
@@ -465,6 +484,8 @@ async function _ComboboxController_init() {
465
484
  }, _ComboboxController_initButton = function _ComboboxController_initButton() {
466
485
  __classPrivateFieldGet(this, _ComboboxController_button, "f")?.removeEventListener('click', __classPrivateFieldGet(this, _ComboboxController_onClickButton, "f"));
467
486
  __classPrivateFieldGet(this, _ComboboxController_button, "f")?.removeEventListener('keydown', __classPrivateFieldGet(this, _ComboboxController_onKeydownButton, "f"));
487
+ __classPrivateFieldGet(this, _ComboboxController_button, "f")?.removeEventListener('mousedown', __classPrivateFieldGet(this, _ComboboxController_onMousedownButton, "f"));
488
+ __classPrivateFieldGet(this, _ComboboxController_button, "f")?.removeEventListener('mouseup', __classPrivateFieldGet(this, _ComboboxController_onMouseupButton, "f"));
468
489
  __classPrivateFieldSet(this, _ComboboxController_button, this.options.getToggleButton(), "f");
469
490
  if (!__classPrivateFieldGet(this, _ComboboxController_button, "f")) {
470
491
  throw new Error('ComboboxController getToggleButton() option must return an element');
@@ -474,6 +495,8 @@ async function _ComboboxController_init() {
474
495
  __classPrivateFieldGet(this, _ComboboxController_button, "f").setAttribute('aria-controls', __classPrivateFieldGet(this, _ComboboxController_listbox, "f")?.id ?? '');
475
496
  __classPrivateFieldGet(this, _ComboboxController_button, "f").addEventListener('click', __classPrivateFieldGet(this, _ComboboxController_onClickButton, "f"));
476
497
  __classPrivateFieldGet(this, _ComboboxController_button, "f").addEventListener('keydown', __classPrivateFieldGet(this, _ComboboxController_onKeydownButton, "f"));
498
+ __classPrivateFieldGet(this, _ComboboxController_button, "f").addEventListener('mousedown', __classPrivateFieldGet(this, _ComboboxController_onMousedownButton, "f"));
499
+ __classPrivateFieldGet(this, _ComboboxController_button, "f").addEventListener('mouseup', __classPrivateFieldGet(this, _ComboboxController_onMouseupButton, "f"));
477
500
  }, _ComboboxController_initInput = function _ComboboxController_initInput() {
478
501
  __classPrivateFieldGet(this, _ComboboxController_input, "f")?.removeEventListener('click', __classPrivateFieldGet(this, _ComboboxController_onClickButton, "f"));
479
502
  __classPrivateFieldGet(this, _ComboboxController_input, "f")?.removeEventListener('keyup', __classPrivateFieldGet(this, _ComboboxController_onKeyupInput, "f"));
@@ -535,6 +558,8 @@ async function _ComboboxController_init() {
535
558
  this.items = this.options.getItems();
536
559
  }
537
560
  }, _ComboboxController_show = async function _ComboboxController_show() {
561
+ // Re-read items on open so slotted/dynamically added options are included:
562
+ __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_initItems).call(this);
538
563
  const success = await this.options.requestShowListbox();
539
564
  __classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_filterItems).call(this);
540
565
  if (success !== false && !__classPrivateFieldGet(this, _ComboboxController_instances, "a", _ComboboxController_hasTextInput_get)) {
@@ -569,12 +594,14 @@ async function _ComboboxController_init() {
569
594
  if (__classPrivateFieldGet(this, _ComboboxController_lb, "f").isSelected(item)) {
570
595
  text += `, (${__classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_translate).call(this, 'selected', langKey)})`;
571
596
  }
572
- if (item.hasAttribute('aria-setsize') && item.hasAttribute('aria-posinset')) {
597
+ const posInSet = InternalsController.getAriaPosInSet(item);
598
+ const setSize = InternalsController.getAriaSetSize(item);
599
+ if (posInSet != null && setSize != null) {
573
600
  if (langKey === 'ja') {
574
- text += `, (${item.getAttribute('aria-setsize')} 件中 ${item.getAttribute('aria-posinset')} 件目)`;
601
+ text += `, (${setSize} 件中 ${posInSet} 件目)`;
575
602
  }
576
603
  else {
577
- text += `, (${item.getAttribute('aria-posinset')} ${__classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_translate).call(this, 'of', langKey)} ${item.getAttribute('aria-setsize')})`;
604
+ text += `, (${posInSet} ${__classPrivateFieldGet(this, _ComboboxController_instances, "m", _ComboboxController_translate).call(this, 'of', langKey)} ${setSize})`;
578
605
  }
579
606
  }
580
607
  __classPrivateFieldGet(_a, _a, "f", _ComboboxController_alert).lang = lang;