Skip to content

Commit e594b00

Browse files
committed
fix(core): narrow host type for combobox, internals controllers
1 parent cdf6ea1 commit e594b00

File tree

2 files changed

+23
-36
lines changed

2 files changed

+23
-36
lines changed

core/pfe-core/controllers/combobox-controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class ComboboxController<
159159
Item extends HTMLElement
160160
> implements ReactiveController {
161161
public static of<T extends HTMLElement>(
162-
host: ReactiveControllerHost,
162+
host: ReactiveControllerHost & HTMLElement,
163163
options: ComboboxControllerOptions<T>,
164164
): ComboboxController<T> {
165165
return new ComboboxController(host, options);
@@ -327,7 +327,7 @@ export class ComboboxController<
327327
}
328328

329329
private constructor(
330-
public host: ReactiveControllerHost,
330+
public host: ReactiveControllerHost & HTMLElement,
331331
options: ComboboxControllerOptions<Item>,
332332
) {
333333
host.addController(this);

core/pfe-core/controllers/internals-controller.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
isServer,
33
type ReactiveController,
44
type ReactiveControllerHost,
5-
type LitElement,
65
} from 'lit';
76

87
function isARIAMixinProp(key: string): key is keyof ARIAMixin {
@@ -59,17 +58,19 @@ function aria(
5958
protos.get(target).add(key);
6059
}
6160

62-
function getLabelText(label: HTMLElement) {
63-
if (label.hidden) {
61+
function getLabelText(label: Node) {
62+
if (!(label instanceof HTMLElement) || label.hidden) {
6463
return '';
6564
} else {
6665
const ariaLabel = label.getAttribute?.('aria-label');
6766
return ariaLabel ?? label.textContent;
6867
}
6968
}
7069

70+
type InternalsHost = ReactiveControllerHost & HTMLElement;
71+
7172
export class InternalsController implements ReactiveController, ARIAMixin {
72-
private static instances = new WeakMap<ReactiveControllerHost, InternalsController>();
73+
private static instances = new WeakMap<HTMLElement, InternalsController>();
7374

7475
declare readonly form: ElementInternals['form'];
7576
declare readonly shadowRoot: ElementInternals['shadowRoot'];
@@ -79,7 +80,7 @@ export class InternalsController implements ReactiveController, ARIAMixin {
7980
declare readonly willValidate: ElementInternals['willValidate'];
8081
declare readonly validationMessage: ElementInternals['validationMessage'];
8182

82-
public static getLabels(host: ReactiveControllerHost): Element[] {
83+
public static getLabels(host: InternalsHost): Element[] {
8384
return Array.from(this.instances.get(host)?.internals.labels ?? []) as Element[];
8485
}
8586

@@ -89,8 +90,8 @@ export class InternalsController implements ReactiveController, ARIAMixin {
8990
* @param host - The listbox item element (option or option-like).
9091
* @param value - Position in set (1-based), or null to clear.
9192
*/
92-
public static setAriaPosInSet(host: Element, value: number | string | null): void {
93-
const instance = this.instances.get(host as unknown as ReactiveControllerHost);
93+
public static setAriaPosInSet(host: HTMLElement, value: number | string | null): void {
94+
const instance = this.instances.get(host);
9495
if (instance) {
9596
instance.ariaPosInSet = value != null ? String(value) : null;
9697
} else if (value != null) {
@@ -106,8 +107,8 @@ export class InternalsController implements ReactiveController, ARIAMixin {
106107
* @param host - The listbox item element (option or option-like).
107108
* @param value - Total set size, or null to clear.
108109
*/
109-
public static setAriaSetSize(host: Element, value: number | string | null): void {
110-
const instance = this.instances.get(host as unknown as ReactiveControllerHost);
110+
public static setAriaSetSize(host: HTMLElement, value: number | string | null): void {
111+
const instance = this.instances.get(host);
111112
if (instance) {
112113
instance.ariaSetSize = value != null ? String(value) : null;
113114
} else if (value != null) {
@@ -121,8 +122,8 @@ export class InternalsController implements ReactiveController, ARIAMixin {
121122
* Gets aria-posinset from a listbox item (internals or attribute).
122123
* @param host - The listbox item element.
123124
*/
124-
public static getAriaPosInSet(host: Element): string | null {
125-
const instance = this.instances.get(host as unknown as ReactiveControllerHost);
125+
public static getAriaPosInSet(host: HTMLElement): string | null {
126+
const instance = this.instances.get(host);
126127
return instance != null ?
127128
instance.ariaPosInSet
128129
: host.getAttribute('aria-posinset');
@@ -132,21 +133,17 @@ export class InternalsController implements ReactiveController, ARIAMixin {
132133
* Gets aria-setsize from a listbox item (internals or attribute).
133134
* @param host - The listbox item element.
134135
*/
135-
public static getAriaSetSize(host: Element): string | null {
136-
const instance = this.instances.get(host as unknown as ReactiveControllerHost);
136+
public static getAriaSetSize(host: HTMLElement): string | null {
137+
const instance = this.instances.get(host);
137138
return instance != null ?
138139
instance.ariaSetSize
139140
: host.getAttribute('aria-setsize');
140141
}
141142

142-
143143
public static isSafari: boolean =
144144
!isServer && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
145145

146-
public static of(
147-
host: ReactiveControllerHost,
148-
options?: InternalsControllerOptions,
149-
): InternalsController {
146+
public static of(host: InternalsHost, options?: InternalsControllerOptions): InternalsController {
150147
constructingAllowed = true;
151148
// implement the singleton pattern
152149
// using a public static constructor method is much easier to manage,
@@ -206,21 +203,16 @@ export class InternalsController implements ReactiveController, ARIAMixin {
206203
@aria ariaValueNow: string | null = null;
207204
@aria ariaValueText: string | null = null;
208205

209-
/** WARNING: be careful of cross-root ARIA browser support */
206+
/** As of April 2025, the following are considered Baseline supported in evergreen browsers */
210207
@aria ariaActiveDescendantElement: Element | null = null;
211-
/** WARNING: be careful of cross-root ARIA browser support */
212208
@aria ariaControlsElements: Element[] | null = null;
213-
/** WARNING: be careful of cross-root ARIA browser support */
214209
@aria ariaDescribedByElements: Element[] | null = null;
215-
/** WARNING: be careful of cross-root ARIA browser support */
216210
@aria ariaDetailsElements: Element[] | null = null;
217-
/** WARNING: be careful of cross-root ARIA browser support */
218211
@aria ariaErrorMessageElements: Element[] | null = null;
219-
/** WARNING: be careful of cross-root ARIA browser support */
220212
@aria ariaFlowToElements: Element[] | null = null;
221-
/** WARNING: be careful of cross-root ARIA browser support */
222213
@aria ariaLabelledByElements: Element[] | null = null;
223-
/** WARNING: be careful of cross-root ARIA browser support */
214+
215+
/** As of February 2026, this is not supported in Chromium browsers */
224216
@aria ariaOwnsElements: Element[] | null = null;
225217

226218
/** True when the control is disabled via it's containing fieldset element */
@@ -243,16 +235,14 @@ export class InternalsController implements ReactiveController, ARIAMixin {
243235
/** A best-attempt based on observed behaviour in FireFox 115 on fedora 38 */
244236
get computedLabelText(): string {
245237
return this.internals.ariaLabel
246-
|| Array.from(this.internals.labels as NodeListOf<HTMLElement>)
238+
|| Array.from(this.internals.labels)
247239
.reduce((acc, label) =>
248240
`${acc}${getLabelText(label)}`, '');
249241
}
250242

251243
private get element() {
252244
if (isServer) {
253-
// FIXME(bennyp): a little white lie, which may break
254-
// when the controller is applied to non-lit frameworks.
255-
return this.host as LitElement;
245+
return this.host;
256246
} else {
257247
return this.host instanceof HTMLElement ? this.host : this.options?.getHTMLElement?.();
258248
}
@@ -262,10 +252,7 @@ export class InternalsController implements ReactiveController, ARIAMixin {
262252

263253
private _formDisabled = false;
264254

265-
private constructor(
266-
public host: ReactiveControllerHost,
267-
private options?: InternalsControllerOptions,
268-
) {
255+
private constructor(public host: InternalsHost, private options?: InternalsControllerOptions) {
269256
if (!constructingAllowed) {
270257
throw new Error('InternalsController must be constructed with `InternalsController.for()`');
271258
}

0 commit comments

Comments
 (0)