diff --git a/chips/internal/chip-set.ts b/chips/internal/chip-set.ts index 8fa151f43..2a0819926 100644 --- a/chips/internal/chip-set.ts +++ b/chips/internal/chip-set.ts @@ -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(); diff --git a/docs/support.md b/docs/support.md index 971390c22..3349aec76 100644 --- a/docs/support.md +++ b/docs/support.md @@ -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 diff --git a/internal/aria/aria.ts b/internal/aria/aria.ts index e223d5585..22f2c7f4d 100644 --- a/internal/aria/aria.ts +++ b/internal/aria/aria.ts @@ -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(); - - // 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; -} diff --git a/internal/aria/aria_test.ts b/internal/aria/aria_test.ts index efa2dafde..7ae577f59 100644 --- a/internal/aria/aria_test.ts +++ b/internal/aria/aria_test.ts @@ -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``; - } - } - - 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``; - } - } - - 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(); - }); - } - }); }); diff --git a/labs/behaviors/element-internals.ts b/labs/behaviors/element-internals.ts index 49468e77b..34b88bec6 100644 --- a/labs/behaviors/element-internals.ts +++ b/labs/behaviors/element-internals.ts @@ -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>( 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]; diff --git a/list/internal/list.ts b/list/internal/list.ts index bf0ce07c3..c5ec9dbb9 100644 --- a/list/internal/list.ts +++ b/list/internal/list.ts @@ -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(); diff --git a/menu/internal/menu.ts b/menu/internal/menu.ts index 090c03f12..2afca9356 100644 --- a/menu/internal/menu.ts +++ b/menu/internal/menu.ts @@ -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(); diff --git a/tabs/internal/tab.ts b/tabs/internal/tab.ts index 5a1925902..ae064554c 100644 --- a/tabs/internal/tab.ts +++ b/tabs/internal/tab.ts @@ -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, ``. 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(); diff --git a/tabs/internal/tabs.ts b/tabs/internal/tabs.ts index b3d4dc2e3..290ff0fd0 100644 --- a/tabs/internal/tabs.ts +++ b/tabs/internal/tabs.ts @@ -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();