diff --git a/button/internal/button.ts b/button/internal/button.ts index 501268dc307..a4c089d53f3 100644 --- a/button/internal/button.ts +++ b/button/internal/button.ts @@ -14,7 +14,6 @@ import {literal, html as staticHtml} from 'lit/static-html.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {internals} from '../../internal/controller/element-internals.js'; import { dispatchActivationClick, isActivationClick, @@ -24,11 +23,18 @@ import { FormSubmitterType, setupFormSubmitter, } from '../../internal/controller/form-submitter.js'; +import { + internals, + mixinElementInternals, +} from '../../labs/behaviors/element-internals.js'; + +// Separate variable needed for closure. +const buttonBaseClass = mixinElementInternals(LitElement); /** * A button component. */ -export abstract class Button extends LitElement implements FormSubmitter { +export abstract class Button extends buttonBaseClass implements FormSubmitter { static { requestUpdateOnAriaChange(Button); setupFormSubmitter(Button); @@ -95,10 +101,6 @@ export abstract class Button extends LitElement implements FormSubmitter { @queryAssignedElements({slot: 'icon', flatten: true}) private readonly assignedIcons!: HTMLElement[]; - /** @private */ - [internals] = (this as HTMLElement) /* needed for closure */ - .attachInternals(); - constructor() { super(); if (!isServer) { diff --git a/chips/internal/chip-set.ts b/chips/internal/chip-set.ts index c7ac6eeb866..8fa151f4357 100644 --- a/chips/internal/chip-set.ts +++ b/chips/internal/chip-set.ts @@ -8,8 +8,8 @@ import {html, isServer, LitElement} from 'lit'; import {queryAssignedElements} from 'lit/decorators.js'; import { + polyfillARIAMixin, polyfillElementInternalsAria, - setupHostAria, } from '../../internal/aria/aria.js'; import {Chip} from './chip.js'; @@ -19,7 +19,7 @@ import {Chip} from './chip.js'; */ export class ChipSet extends LitElement { static { - setupHostAria(ChipSet, {focusable: false}); + polyfillARIAMixin(ChipSet); } get chips() { diff --git a/iconbutton/internal/icon-button.ts b/iconbutton/internal/icon-button.ts index 9162480c704..02391d75a66 100644 --- a/iconbutton/internal/icon-button.ts +++ b/iconbutton/internal/icon-button.ts @@ -14,18 +14,24 @@ import {literal, html as staticHtml} from 'lit/static-html.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {internals} from '../../internal/controller/element-internals.js'; import { FormSubmitter, FormSubmitterType, setupFormSubmitter, } from '../../internal/controller/form-submitter.js'; import {isRtl} from '../../internal/controller/is-rtl.js'; +import { + internals, + mixinElementInternals, +} from '../../labs/behaviors/element-internals.js'; type LinkTarget = '_blank' | '_parent' | '_self' | '_top'; +// Separate variable needed for closure. +const iconButtonBaseClass = mixinElementInternals(LitElement); + // tslint:disable-next-line:enforce-comments-on-exported-symbols -export class IconButton extends LitElement implements FormSubmitter { +export class IconButton extends iconButtonBaseClass implements FormSubmitter { static { requestUpdateOnAriaChange(IconButton); setupFormSubmitter(IconButton); @@ -106,10 +112,6 @@ export class IconButton extends LitElement implements FormSubmitter { @state() private flipIcon = isRtl(this, this.flipIconInRtl); - /** @private */ - [internals] = (this as HTMLElement) /* needed for closure */ - .attachInternals(); - /** * Link buttons cannot be disabled. */ diff --git a/internal/aria/aria.ts b/internal/aria/aria.ts index 64828cb694d..b5c1be4542a 100644 --- a/internal/aria/aria.ts +++ b/internal/aria/aria.ts @@ -295,37 +295,11 @@ export type ARIARole = | 'doc-toc'; /** - * Enables a host custom element to be the target for aria roles and attributes. - * Components should set the `elementInternals.role` property. - * - * By default, aria components are tab focusable. Provide a `focusable: false` - * option for components that should not be tab focusable, such as - * `role="listbox"`. - * - * This function will also polyfill aria `ElementInternals` properties for - * Firefox. + * This function will polyfill `ARIAMixin` properties for Firefox. * * @param ctor The `ReactiveElement` constructor to set up. - * @param options Options to configure the element's host aria. */ -export function setupHostAria( - ctor: typeof ReactiveElement, - {focusable}: SetupHostAriaOptions = {}, -) { - if (focusable !== false) { - ctor.addInitializer((host) => { - host.addController({ - hostConnected() { - if (host.hasAttribute('tabindex')) { - return; - } - - host.tabIndex = 0; - }, - }); - }); - } - +export function polyfillARIAMixin(ctor: typeof ReactiveElement) { if (isServer || 'role' in Element.prototype) { return; } @@ -341,20 +315,6 @@ export function setupHostAria( ctor.createProperty('role', {reflect: true}); } -/** - * Options for setting up a host element as an aria target. - */ -export interface SetupHostAriaOptions { - /** - * Whether or not the element can be focused with the tab key. Defaults to - * true. - * - * Set this to false for aria roles that should not be tab focusable, such as - * `role="listbox"`. - */ - focusable?: boolean; -} - /** * Polyfills an element and its `ElementInternals` to support `ARIAMixin` * properties on internals. This is needed for Firefox. diff --git a/internal/aria/aria_test.ts b/internal/aria/aria_test.ts index 6a40249c4de..efa2dafde81 100644 --- a/internal/aria/aria_test.ts +++ b/internal/aria/aria_test.ts @@ -13,8 +13,8 @@ import { ARIAProperty, ariaPropertyToAttribute, isAriaAttribute, + polyfillARIAMixin, polyfillElementInternalsAria, - setupHostAria, } from './aria.js'; describe('aria', () => { @@ -52,11 +52,11 @@ describe('aria', () => { }); }); - describe('setupHostAria()', () => { + describe('polyfillARIAMixin()', () => { @customElement('test-setup-aria-host') class TestElement extends LitElement { static { - setupHostAria(TestElement); + polyfillARIAMixin(TestElement); } override render() { @@ -64,67 +64,28 @@ describe('aria', () => { } } - it('should not hydrate tabindex attribute on creation', () => { + it('should reflect ARIAMixin properties to attributes', async () => { const element = new TestElement(); - expect(element.hasAttribute('tabindex')) - .withContext('has tabindex attribute') - .toBeFalse(); - }); - - it('should set tabindex="0" on element once connected', () => { - const element = new TestElement(); - document.body.appendChild(element); - expect(element.getAttribute('tabindex')) - .withContext('tabindex attribute value') - .toEqual('0'); - - element.remove(); - }); - - it('should not set tabindex on connected if one already exists', () => { - const element = new TestElement(); - element.tabIndex = -1; document.body.appendChild(element); - expect(element.getAttribute('tabindex')) - .withContext('tabindex attribute value') - .toEqual('-1'); - + 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(); }); - - it('should not change tabindex if disconnected and reconnected', () => { - const element = new TestElement(); - document.body.appendChild(element); - element.tabIndex = -1; - element.remove(); - document.body.appendChild(element); - expect(element.getAttribute('tabindex')) - .withContext('tabindex attribute value') - .toEqual('-1'); - }); - - if (!('role' in Element.prototype)) { - describe('polyfill', () => { - it('should hydrate aria attributes when ARIAMixin is not supported', async () => { - const element = new TestElement(); - document.body.appendChild(element); - element.role = 'button'; - await element.updateComplete; - expect(element.getAttribute('role')) - .withContext('role attribute value') - .toEqual('button'); - - element.remove(); - }); - }); - } }); describe('polyfillElementInternalsAria()', () => { @customElement('test-polyfill-element-internals-aria') class TestElement extends LitElement { static { - setupHostAria(TestElement); + polyfillARIAMixin(TestElement); } internals = polyfillElementInternalsAria(this, this.attachInternals()); diff --git a/internal/controller/element-internals.ts b/internal/controller/element-internals.ts deleted file mode 100644 index 3cdc3ca59c9..00000000000 --- a/internal/controller/element-internals.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2023 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * A unique symbol used for protected access to an instance's - * `ElementInternals`. - * - * @example - * ```ts - * class MyElement extends LitElement { - * static formAssociated = true; - * - * [internals] = this.attachInternals(); - * } - * - * function getForm(element: MyElement) { - * return element[internals].form; - * } - * ``` - */ -export const internals = Symbol('internals'); - -/** - * An instance with `ElementInternals`. - * - * Use this when protected access is needed for an instance's `ElementInternals` - * from other files. A unique symbol is used to access the internals. - */ -export interface WithInternals { - /** - * An instance's `ElementInternals`. - */ - [internals]: ElementInternals; -} diff --git a/internal/controller/form-submitter.ts b/internal/controller/form-submitter.ts index 221c64d28f1..bb99101ca59 100644 --- a/internal/controller/form-submitter.ts +++ b/internal/controller/form-submitter.ts @@ -6,7 +6,10 @@ import {isServer, ReactiveElement} from 'lit'; -import {internals, WithInternals} from './element-internals.js'; +import { + internals, + WithElementInternals, +} from '../../labs/behaviors/element-internals.js'; /** * A string indicating the form submission behavior of the element. @@ -23,7 +26,7 @@ export type FormSubmitterType = 'button' | 'submit' | 'reset'; * An element that can submit or reset a `
`, similar to * `