chore(behaviors): add ElementInternals mixin

PiperOrigin-RevId: 576937116
This commit is contained in:
Elizabeth Mitchell 2023-10-26 11:30:40 -07:00 committed by Copybara-Service
parent 0ebd7c786b
commit e7bc633e18
7 changed files with 122 additions and 56 deletions

View File

@ -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) {

View File

@ -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.
*/

View File

@ -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;
}

View File

@ -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 `<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.
*

View File

@ -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 {
@ -22,7 +21,7 @@ declare global {
}
@customElement('test-form-submitter-button')
class FormSubmitterButton extends LitElement {
class FormSubmitterButton extends mixinElementInternals(LitElement) {
static {
setupFormSubmitter(FormSubmitterButton);
}
@ -32,8 +31,6 @@ class FormSubmitterButton extends LitElement {
type: FormSubmitterType = 'submit';
@property({reflect: true}) name = '';
value = '';
[internals] = this.attachInternals();
}
describe('setupFormSubmitter()', () => {

View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {LitElement} from 'lit';
import {MixinBase, MixinReturn} from './mixin.js';
/**
* A unique symbol used for protected access to an instance's
* `ElementInternals`.
*
* @example
* ```ts
* class MyElement extends mixinElementInternals(LitElement) {
* constructor() {
* super();
* this[internals].role = 'button';
* }
* }
* ```
*/
export const internals = Symbol('internals');
/**
* An instance with an `internals` symbol property for the component's
* `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 WithElementInternals {
/**
* An instance's `ElementInternals`.
*/
[internals]: ElementInternals;
}
/**
* Mixes in an attached `ElementInternals` instance.
*
* This mixin is only needed when other shared code needs access to a
* component's `ElementInternals`, such as form-associated mixins.
*
* @param base The class to mix functionality into.
* @return The provided class with `WithElementInternals` mixed in.
*/
export function mixinElementInternals<T extends MixinBase<LitElement>>(
base: T,
): MixinReturn<T, WithElementInternals> {
abstract class WithElementInternalsElement
extends base
implements WithElementInternals
{
// Cast needed for closure
[internals] = (this as HTMLElement).attachInternals();
}
return WithElementInternalsElement;
}

View File

@ -0,0 +1,37 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// import 'jasmine'; (google3-only)
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {Environment} from '../../testing/environment.js';
import {internals, mixinElementInternals} from './element-internals.js';
describe('mixinElementInternals()', () => {
@customElement('test-element-internals')
class TestElementInternals extends mixinElementInternals(LitElement) {}
const env = new Environment();
async function setupTest() {
const root = env.render(
html`<test-element-internals></test-element-internals>`,
);
const element = root.querySelector(
'test-element-internals',
) as TestElementInternals;
await env.waitForStability();
return element;
}
it('should provide an `ElementInternals` instance', async () => {
const element = await setupTest();
expect(element[internals]).toBeInstanceOf(ElementInternals);
});
});