Skip to content

Commit

Permalink
chore(behaviors): add ElementInternals mixin
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 576657058
  • Loading branch information
asyncLiz authored and copybara-github committed Oct 26, 2023
1 parent 0ebd7c7 commit 86ed2e7
Show file tree
Hide file tree
Showing 15 changed files with 169 additions and 169 deletions.
14 changes: 8 additions & 6 deletions button/internal/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions chips/internal/chip-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,7 +19,7 @@ import {Chip} from './chip.js';
*/
export class ChipSet extends LitElement {
static {
setupHostAria(ChipSet, {focusable: false});
polyfillARIAMixin(ChipSet);
}

get chips() {
Expand Down
14 changes: 8 additions & 6 deletions iconbutton/internal/icon-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down
44 changes: 2 additions & 42 deletions internal/aria/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
Expand Down
69 changes: 15 additions & 54 deletions internal/aria/aria_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
ARIAProperty,
ariaPropertyToAttribute,
isAriaAttribute,
polyfillARIAMixin,
polyfillElementInternalsAria,
setupHostAria,
} from './aria.js';

describe('aria', () => {
Expand Down Expand Up @@ -52,79 +52,40 @@ describe('aria', () => {
});
});

describe('setupHostAria()', () => {
describe('polyfillARIAMixin()', () => {
@customElement('test-setup-aria-host')
class TestElement extends LitElement {
static {
setupHostAria(TestElement);
polyfillARIAMixin(TestElement);
}

override render() {
return html`<slot></slot>`;
}
}

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());
Expand Down
37 changes: 0 additions & 37 deletions internal/controller/element-internals.ts

This file was deleted.

7 changes: 5 additions & 2 deletions internal/controller/form-submitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,7 +26,7 @@ export type FormSubmitterType = 'button' | 'submit' | 'reset';
* An element that can submit or reset a `<form>`, similar to
* `<button type="submit">`.
*/
export interface FormSubmitter extends ReactiveElement, WithInternals {
export interface FormSubmitter extends ReactiveElement, WithElementInternals {
/**
* A string indicating the form submission behavior of the element.
*
Expand Down
7 changes: 2 additions & 5 deletions internal/controller/form-submitter_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
import {html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';

import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
import {Environment} from '../../testing/environment.js';
import {Harness} from '../../testing/harness.js';

import {internals} from './element-internals.js';
import {FormSubmitterType, setupFormSubmitter} from './form-submitter.js';

declare global {
Expand All @@ -22,7 +21,7 @@ declare global {
}

@customElement('test-form-submitter-button')
class FormSubmitterButton extends LitElement {
class FormSubmitterButton extends mixinElementInternals(LitElement) {
static {
setupFormSubmitter(FormSubmitterButton);
}
Expand All @@ -32,8 +31,6 @@ class FormSubmitterButton extends LitElement {
type: FormSubmitterType = 'submit';
@property({reflect: true}) name = '';
value = '';

[internals] = this.attachInternals();
}

describe('setupFormSubmitter()', () => {
Expand Down
Loading

0 comments on commit 86ed2e7

Please sign in to comment.