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 9137062
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 56 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
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
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
62 changes: 62 additions & 0 deletions labs/behaviors/element-internals.ts
Original file line number Diff line number Diff line change
@@ -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;
}
37 changes: 37 additions & 0 deletions labs/behaviors/element-internals_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 9137062

Please sign in to comment.