refactor: remove Firefox ARIAMixin polyfills (v119+)

This is now fixed in the latest two versions of Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1785412

PiperOrigin-RevId: 586442947
This commit is contained in:
Elizabeth Mitchell 2023-11-29 13:47:49 -08:00 committed by Copybara-Service
parent 4c479299b1
commit c319236cd1
9 changed files with 13 additions and 532 deletions

View File

@ -7,21 +7,12 @@
import {html, isServer, LitElement} from 'lit';
import {queryAssignedElements} from 'lit/decorators.js';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {Chip} from './chip.js';
/**
* A chip set component.
*/
export class ChipSet extends LitElement {
static {
polyfillARIAMixin(ChipSet);
}
get chips() {
return this.childElements.filter(
(child): child is Chip => child instanceof Chip,
@ -29,11 +20,9 @@ export class ChipSet extends LitElement {
}
@queryAssignedElements() private readonly childElements!: HTMLElement[];
private readonly internals = polyfillElementInternalsAria(
this,
private readonly internals =
// Cast needed for closure
(this as HTMLElement).attachInternals(),
);
(this as HTMLElement).attachInternals();
constructor() {
super();

View File

@ -28,7 +28,7 @@ Browser | Version
------- | -------
Chrome | 112 +
Edge | 112 +
Firefox | 113 +
Firefox | 119 +
Safari* | 16.4 +
*\* previous versions of Safari may be supported with an

View File

@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {isServer, ReactiveElement} from 'lit';
/**
* Accessibility Object Model reflective aria property name types.
*/
@ -293,172 +291,3 @@ export type ARIARole =
| 'doc-subtitle'
| 'doc-tip'
| 'doc-toc';
/**
* This function will polyfill `ARIAMixin` properties for Firefox.
*
* @param ctor The `ReactiveElement` constructor to set up.
*/
export function polyfillARIAMixin(ctor: typeof ReactiveElement) {
if (isServer || 'role' in Element.prototype) {
return;
}
// Polyfill reflective aria properties for Firefox
for (const ariaProperty of ARIA_PROPERTIES) {
ctor.createProperty(ariaProperty, {
attribute: ariaPropertyToAttribute(ariaProperty),
reflect: true,
});
}
ctor.createProperty('role', {reflect: true});
}
/**
* Polyfills an element and its `ElementInternals` to support `ARIAMixin`
* properties on internals. This is needed for Firefox.
*
* `polyfillARIAMixin()` must be called for the element class.
*
* @example
* class XButton extends LitElement {
* static {
* polyfillARIAMixin(XButton);
* }
*
* private internals =
* polyfillElementInternalsAria(this, this.attachInternals());
*
* constructor() {
* super();
* this.internals.role = 'button';
* }
* }
*/
export function polyfillElementInternalsAria(
host: ReactiveElement,
internals: ElementInternals,
) {
if (checkIfElementInternalsSupportsAria(internals)) {
return internals;
}
if (!('role' in host)) {
throw new Error('Missing polyfillARIAMixin()');
}
let firstConnectedCallbacks: Array<{
property: ARIAProperty | 'role';
callback: () => void;
}> = [];
let hasBeenConnected = false;
// Add support for Firefox, which has not yet implement ElementInternals aria
for (const ariaProperty of ARIA_PROPERTIES) {
let internalAriaValue: string | null = null;
Object.defineProperty(internals, ariaProperty, {
enumerable: true,
configurable: true,
get() {
return internalAriaValue;
},
set(value: string | null) {
const setValue = () => {
internalAriaValue = value;
if (!hasBeenConnected) {
firstConnectedCallbacks.push({
property: ariaProperty,
callback: setValue,
});
return;
}
// Dynamic lookup rather than hardcoding all properties.
// tslint:disable-next-line:no-dict-access-on-struct-type
host[ariaProperty] = value;
};
setValue();
},
});
}
let internalRoleValue: string | null = null;
Object.defineProperty(internals, 'role', {
enumerable: true,
configurable: true,
get() {
return internalRoleValue;
},
set(value: string | null) {
const setRole = () => {
internalRoleValue = value;
if (!hasBeenConnected) {
firstConnectedCallbacks.push({
property: 'role',
callback: setRole,
});
return;
}
if (value === null) {
host.removeAttribute('role');
} else {
host.setAttribute('role', value);
}
};
setRole();
},
});
host.addController({
hostConnected() {
if (hasBeenConnected) {
return;
}
hasBeenConnected = true;
const propertiesSetByUser = new Set<ARIAProperty | 'role'>();
// See which properties were set by the user on host before we apply
// internals values as attributes to host. Needs to be done in another
// for loop because the callbacks set these attributes on host.
for (const {property} of firstConnectedCallbacks) {
const wasSetByUser =
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
// Dynamic lookup rather than hardcoding all properties.
// tslint:disable-next-line:no-dict-access-on-struct-type
host[property] !== undefined;
if (wasSetByUser) {
propertiesSetByUser.add(property);
}
}
for (const {property, callback} of firstConnectedCallbacks) {
// If the user has set the attribute or property, do not override the
// user's value
if (propertiesSetByUser.has(property)) {
continue;
}
callback();
}
// Remove strong callback references
firstConnectedCallbacks = [];
},
});
return internals;
}
// Separate function so that typescript doesn't complain about internals being
// "never".
function checkIfElementInternalsSupportsAria(internals: ElementInternals) {
return 'role' in internals;
}

View File

@ -6,16 +6,7 @@
// import 'jasmine'; (google3-only)
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {
ARIAProperty,
ariaPropertyToAttribute,
isAriaAttribute,
polyfillARIAMixin,
polyfillElementInternalsAria,
} from './aria.js';
import {ARIAProperty, ariaPropertyToAttribute, isAriaAttribute} from './aria.js';
describe('aria', () => {
describe('isAriaAttribute()', () => {
@ -51,277 +42,4 @@ describe('aria', () => {
).toBe('aria-labelledby');
});
});
describe('polyfillARIAMixin()', () => {
@customElement('test-setup-aria-host')
class TestElement extends LitElement {
static {
polyfillARIAMixin(TestElement);
}
override render() {
return html`<slot></slot>`;
}
}
it('should reflect ARIAMixin properties to attributes', async () => {
const element = new TestElement();
document.body.appendChild(element);
element.role = 'button';
element.ariaLabel = 'Foo';
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Foo');
element.remove();
});
});
describe('polyfillElementInternalsAria()', () => {
@customElement('test-polyfill-element-internals-aria')
class TestElement extends LitElement {
static {
polyfillARIAMixin(TestElement);
}
internals = polyfillElementInternalsAria(this, this.attachInternals());
constructor() {
super();
this.internals.role = 'button';
}
override render() {
return html`<slot></slot>`;
}
}
if ('role' in ElementInternals.prototype) {
it('should not hydrate attributes when role set', () => {
const element = new TestElement();
document.body.appendChild(element);
expect(element.hasAttribute('role'))
.withContext('has role attribute')
.toBeFalse();
element.remove();
});
} else {
it('should preserve role values when set before connected', () => {
const element = new TestElement();
// TestElement() sets role in constructor
expect(element.internals.role)
.withContext('ElementInternals.role')
.toEqual('button');
});
it('should preserve aria values when set before connected', () => {
const element = new TestElement();
element.internals.ariaLabel = 'Foo';
expect(element.internals.ariaLabel)
.withContext('ElementInternals.ariaLabel')
.toEqual('Foo');
});
it('should hydrate role attributes when set before connection', async () => {
const element = new TestElement();
// TestElement() sets role in constructor
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
element.remove();
});
it('should hydrate aria attributes when set before connection', async () => {
const element = new TestElement();
element.internals.ariaLabel = 'Foo';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Foo');
element.remove();
});
it('should set aria attributes when set after connection', async () => {
const element = new TestElement();
document.body.appendChild(element);
element.internals.ariaLabel = 'Value after construction';
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Value after construction');
element.remove();
});
it('should not override aria attributes on host when set before connection', async () => {
const element = new TestElement();
element.setAttribute('aria-label', 'Value set by user');
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
it('should not override aria properties on host when set before connection', async () => {
const element = new TestElement();
element.ariaLabel = 'Value set by user';
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.ariaLabel)
.withContext('ariaLabel property value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
it('should not override role attribute on host when set before connection', async () => {
const element = new TestElement();
element.setAttribute('role', 'Value set by user');
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
it('should not override role property on host when set before connection', async () => {
const element = new TestElement();
element.role = 'Value set by user';
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.role)
.withContext('role property value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
it('should handle setting role multiple times before connection', async () => {
const element = new TestElement();
element.internals.role = 'button';
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
element.remove();
});
it('should handle setting role multiple times before connection when property is set on host', async () => {
const element = new TestElement();
element.role = 'radio';
element.internals.role = 'button';
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
element.remove();
});
it('should handle setting aria properties multiple times before connection', async () => {
const element = new TestElement();
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
element.remove();
});
it('should handle setting aria properties multiple times before connection when property is set on host', async () => {
const element = new TestElement();
element.ariaLabel = 'First';
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
element.remove();
});
it('should handle setting role after first connection while disconnected', async () => {
const element = new TestElement();
element.internals.role = 'button';
document.body.appendChild(element);
await element.updateComplete;
element.remove();
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role after connected and disconnected')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after reconnected')
.toEqual('checkbox');
element.remove();
});
}
});
});

View File

@ -6,10 +6,6 @@
import {LitElement} from 'lit';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {MixinBase, MixinReturn} from './mixin.js';
/**
@ -61,21 +57,12 @@ export function mixinElementInternals<T extends MixinBase<LitElement>>(
extends base
implements WithElementInternals
{
static {
polyfillARIAMixin(
WithElementInternalsElement as unknown as typeof LitElement,
);
}
get [internals]() {
// Create internals in getter so that it can be used in methods called on
// construction in `ReactiveElement`, such as `requestUpdate()`.
if (!this[privateInternals]) {
// Cast needed for closure
this[privateInternals] = polyfillElementInternalsAria(
this,
(this as HTMLElement).attachInternals(),
);
this[privateInternals] = (this as HTMLElement).attachInternals();
}
return this[privateInternals];

View File

@ -7,11 +7,6 @@
import {html, isServer, LitElement} from 'lit';
import {queryAssignedElements} from 'lit/decorators.js';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {ListController, NavigableKeys} from './list-controller.js';
import {ListItem as SharedListItem} from './list-navigation-helpers.js';
@ -23,10 +18,6 @@ interface ListItem extends SharedListItem {
// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class List extends LitElement {
static {
polyfillARIAMixin(List);
}
/**
* An array of activatable and disableable list items. Queries every assigned
* element that has the `md-list-item` attribute.
@ -58,11 +49,9 @@ export class List extends LitElement {
isActivatable: (item) => !item.disabled && item.type !== 'text',
});
private readonly internals = polyfillElementInternalsAria(
this,
private readonly internals =
// Cast needed for closure
(this as HTMLElement).attachInternals(),
);
(this as HTMLElement).attachInternals();
constructor() {
super();

View File

@ -12,10 +12,6 @@ import {property, query, queryAssignedElements, state} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {styleMap} from 'lit/directives/style-map.js';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {EASING, createAnimationSignal} from '../../internal/motion/animation.js';
import {
ListController,
@ -90,10 +86,6 @@ function getFocusedElement(
* @fires closed {Event} Fired once the menu is closed, after any animations
*/
export abstract class Menu extends LitElement {
static {
polyfillARIAMixin(Menu);
}
@query('.menu') private readonly surfaceEl!: HTMLElement | null;
@query('slot') private readonly slotEl!: HTMLSlotElement | null;
@ -341,11 +333,9 @@ export abstract class Menu extends LitElement {
this.requestUpdate('anchorElement');
}
private readonly internals = polyfillElementInternalsAria(
this,
private readonly internals =
// Cast needed for closure
(this as HTMLElement).attachInternals(),
);
(this as HTMLElement).attachInternals();
constructor() {
super();

View File

@ -18,10 +18,6 @@ import {
} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {EASING} from '../../internal/motion/animation.js';
import {mixinFocusable} from '../../labs/behaviors/focusable.js';
@ -44,10 +40,6 @@ const tabBaseClass = mixinFocusable(LitElement);
* Tab component.
*/
export class Tab extends tabBaseClass {
static {
polyfillARIAMixin(Tab);
}
/**
* The attribute `md-tab` indicates that the element is a tab for the parent
* element, `<md-tabs>`. Make sure if you're implementing your own `md-tab`
@ -89,11 +81,9 @@ export class Tab extends tabBaseClass {
private readonly assignedDefaultNodes!: Node[];
@queryAssignedElements({slot: 'icon', flatten: true})
private readonly assignedIcons!: HTMLElement[];
private readonly internals = polyfillElementInternalsAria(
this,
private readonly internals =
// Cast needed for closure
(this as HTMLElement).attachInternals(),
);
(this as HTMLElement).attachInternals();
constructor() {
super();

View File

@ -9,11 +9,6 @@ import '../../divider/divider.js';
import {html, isServer, LitElement} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators.js';
import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {ANIMATE_INDICATOR, Tab} from './tab.js';
/**
@ -41,10 +36,6 @@ import {ANIMATE_INDICATOR, Tab} from './tab.js';
*
*/
export class Tabs extends LitElement {
static {
polyfillARIAMixin(Tabs);
}
/**
* The tabs of this tab bar.
*/
@ -117,11 +108,9 @@ export class Tabs extends LitElement {
return this.tabs.find((tab) => tab.matches(':focus-within'));
}
private readonly internals = polyfillElementInternalsAria(
this,
private readonly internals =
// Cast needed for closure
(this as HTMLElement).attachInternals(),
);
(this as HTMLElement).attachInternals();
constructor() {
super();