Skip to content

Commit e7bc633

Browse files
asyncLizcopybara-github
authored andcommitted
chore(behaviors): add ElementInternals mixin
PiperOrigin-RevId: 576937116
1 parent 0ebd7c7 commit e7bc633

File tree

7 files changed

+122
-56
lines changed

7 files changed

+122
-56
lines changed

button/internal/button.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {literal, html as staticHtml} from 'lit/static-html.js';
1414

1515
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1616
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
17-
import {internals} from '../../internal/controller/element-internals.js';
1817
import {
1918
dispatchActivationClick,
2019
isActivationClick,
@@ -24,11 +23,18 @@ import {
2423
FormSubmitterType,
2524
setupFormSubmitter,
2625
} from '../../internal/controller/form-submitter.js';
26+
import {
27+
internals,
28+
mixinElementInternals,
29+
} from '../../labs/behaviors/element-internals.js';
30+
31+
// Separate variable needed for closure.
32+
const buttonBaseClass = mixinElementInternals(LitElement);
2733

2834
/**
2935
* A button component.
3036
*/
31-
export abstract class Button extends LitElement implements FormSubmitter {
37+
export abstract class Button extends buttonBaseClass implements FormSubmitter {
3238
static {
3339
requestUpdateOnAriaChange(Button);
3440
setupFormSubmitter(Button);
@@ -95,10 +101,6 @@ export abstract class Button extends LitElement implements FormSubmitter {
95101
@queryAssignedElements({slot: 'icon', flatten: true})
96102
private readonly assignedIcons!: HTMLElement[];
97103

98-
/** @private */
99-
[internals] = (this as HTMLElement) /* needed for closure */
100-
.attachInternals();
101-
102104
constructor() {
103105
super();
104106
if (!isServer) {

iconbutton/internal/icon-button.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,24 @@ import {literal, html as staticHtml} from 'lit/static-html.js';
1414

1515
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1616
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
17-
import {internals} from '../../internal/controller/element-internals.js';
1817
import {
1918
FormSubmitter,
2019
FormSubmitterType,
2120
setupFormSubmitter,
2221
} from '../../internal/controller/form-submitter.js';
2322
import {isRtl} from '../../internal/controller/is-rtl.js';
23+
import {
24+
internals,
25+
mixinElementInternals,
26+
} from '../../labs/behaviors/element-internals.js';
2427

2528
type LinkTarget = '_blank' | '_parent' | '_self' | '_top';
2629

30+
// Separate variable needed for closure.
31+
const iconButtonBaseClass = mixinElementInternals(LitElement);
32+
2733
// tslint:disable-next-line:enforce-comments-on-exported-symbols
28-
export class IconButton extends LitElement implements FormSubmitter {
34+
export class IconButton extends iconButtonBaseClass implements FormSubmitter {
2935
static {
3036
requestUpdateOnAriaChange(IconButton);
3137
setupFormSubmitter(IconButton);
@@ -106,10 +112,6 @@ export class IconButton extends LitElement implements FormSubmitter {
106112

107113
@state() private flipIcon = isRtl(this, this.flipIconInRtl);
108114

109-
/** @private */
110-
[internals] = (this as HTMLElement) /* needed for closure */
111-
.attachInternals();
112-
113115
/**
114116
* Link buttons cannot be disabled.
115117
*/

internal/controller/element-internals.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

internal/controller/form-submitter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import {isServer, ReactiveElement} from 'lit';
88

9-
import {internals, WithInternals} from './element-internals.js';
9+
import {
10+
internals,
11+
WithElementInternals,
12+
} from '../../labs/behaviors/element-internals.js';
1013

1114
/**
1215
* A string indicating the form submission behavior of the element.
@@ -23,7 +26,7 @@ export type FormSubmitterType = 'button' | 'submit' | 'reset';
2326
* An element that can submit or reset a `<form>`, similar to
2427
* `<button type="submit">`.
2528
*/
26-
export interface FormSubmitter extends ReactiveElement, WithInternals {
29+
export interface FormSubmitter extends ReactiveElement, WithElementInternals {
2730
/**
2831
* A string indicating the form submission behavior of the element.
2932
*

internal/controller/form-submitter_test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import {html, LitElement} from 'lit';
1010
import {customElement, property} from 'lit/decorators.js';
1111

12+
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
1213
import {Environment} from '../../testing/environment.js';
1314
import {Harness} from '../../testing/harness.js';
14-
15-
import {internals} from './element-internals.js';
1615
import {FormSubmitterType, setupFormSubmitter} from './form-submitter.js';
1716

1817
declare global {
@@ -22,7 +21,7 @@ declare global {
2221
}
2322

2423
@customElement('test-form-submitter-button')
25-
class FormSubmitterButton extends LitElement {
24+
class FormSubmitterButton extends mixinElementInternals(LitElement) {
2625
static {
2726
setupFormSubmitter(FormSubmitterButton);
2827
}
@@ -32,8 +31,6 @@ class FormSubmitterButton extends LitElement {
3231
type: FormSubmitterType = 'submit';
3332
@property({reflect: true}) name = '';
3433
value = '';
35-
36-
[internals] = this.attachInternals();
3734
}
3835

3936
describe('setupFormSubmitter()', () => {

labs/behaviors/element-internals.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {LitElement} from 'lit';
8+
9+
import {MixinBase, MixinReturn} from './mixin.js';
10+
11+
/**
12+
* A unique symbol used for protected access to an instance's
13+
* `ElementInternals`.
14+
*
15+
* @example
16+
* ```ts
17+
* class MyElement extends mixinElementInternals(LitElement) {
18+
* constructor() {
19+
* super();
20+
* this[internals].role = 'button';
21+
* }
22+
* }
23+
* ```
24+
*/
25+
export const internals = Symbol('internals');
26+
27+
/**
28+
* An instance with an `internals` symbol property for the component's
29+
* `ElementInternals`.
30+
*
31+
* Use this when protected access is needed for an instance's `ElementInternals`
32+
* from other files. A unique symbol is used to access the internals.
33+
*/
34+
export interface WithElementInternals {
35+
/**
36+
* An instance's `ElementInternals`.
37+
*/
38+
[internals]: ElementInternals;
39+
}
40+
41+
/**
42+
* Mixes in an attached `ElementInternals` instance.
43+
*
44+
* This mixin is only needed when other shared code needs access to a
45+
* component's `ElementInternals`, such as form-associated mixins.
46+
*
47+
* @param base The class to mix functionality into.
48+
* @return The provided class with `WithElementInternals` mixed in.
49+
*/
50+
export function mixinElementInternals<T extends MixinBase<LitElement>>(
51+
base: T,
52+
): MixinReturn<T, WithElementInternals> {
53+
abstract class WithElementInternalsElement
54+
extends base
55+
implements WithElementInternals
56+
{
57+
// Cast needed for closure
58+
[internals] = (this as HTMLElement).attachInternals();
59+
}
60+
61+
return WithElementInternalsElement;
62+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// import 'jasmine'; (google3-only)
8+
9+
import {html, LitElement} from 'lit';
10+
import {customElement} from 'lit/decorators.js';
11+
12+
import {Environment} from '../../testing/environment.js';
13+
14+
import {internals, mixinElementInternals} from './element-internals.js';
15+
16+
describe('mixinElementInternals()', () => {
17+
@customElement('test-element-internals')
18+
class TestElementInternals extends mixinElementInternals(LitElement) {}
19+
20+
const env = new Environment();
21+
22+
async function setupTest() {
23+
const root = env.render(
24+
html`<test-element-internals></test-element-internals>`,
25+
);
26+
const element = root.querySelector(
27+
'test-element-internals',
28+
) as TestElementInternals;
29+
await env.waitForStability();
30+
return element;
31+
}
32+
33+
it('should provide an `ElementInternals` instance', async () => {
34+
const element = await setupTest();
35+
expect(element[internals]).toBeInstanceOf(ElementInternals);
36+
});
37+
});

0 commit comments

Comments
 (0)