Skip to content

Commit 1f55d41

Browse files
asyncLizcopybara-github
authored andcommitted
chore(behaviors): update form controls to use shared mixins
PiperOrigin-RevId: 578297611
1 parent e1077c7 commit 1f55d41

File tree

12 files changed

+865
-327
lines changed

12 files changed

+865
-327
lines changed

checkbox/internal/checkbox.ts

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,25 @@ import {
1818
isActivationClick,
1919
redispatchEvent,
2020
} from '../../internal/controller/events.js';
21+
import {
22+
internals,
23+
mixinElementInternals,
24+
} from '../../labs/behaviors/element-internals.js';
25+
import {
26+
getFormState,
27+
getFormValue,
28+
mixinFormAssociated,
29+
} from '../../labs/behaviors/form-associated.js';
30+
31+
// Separate variable needed for closure.
32+
const checkboxBaseClass = mixinFormAssociated(
33+
mixinElementInternals(LitElement),
34+
);
2135

2236
/**
2337
* A checkbox component.
2438
*/
25-
export class Checkbox extends LitElement {
39+
export class Checkbox extends checkboxBaseClass {
2640
static {
2741
requestUpdateOnAriaChange(Checkbox);
2842
}
@@ -33,19 +47,11 @@ export class Checkbox extends LitElement {
3347
delegatesFocus: true,
3448
};
3549

36-
/** @nocollapse */
37-
static readonly formAssociated = true;
38-
3950
/**
4051
* Whether or not the checkbox is selected.
4152
*/
4253
@property({type: Boolean}) checked = false;
4354

44-
/**
45-
* Whether or not the checkbox is disabled.
46-
*/
47-
@property({type: Boolean, reflect: true}) disabled = false;
48-
4955
/**
5056
* Whether or not the checkbox is indeterminate.
5157
*
@@ -68,30 +74,6 @@ export class Checkbox extends LitElement {
6874
*/
6975
@property() value = 'on';
7076

71-
/**
72-
* The HTML name to use in form submission.
73-
*/
74-
get name() {
75-
return this.getAttribute('name') ?? '';
76-
}
77-
set name(name: string) {
78-
this.setAttribute('name', name);
79-
}
80-
81-
/**
82-
* The associated form element with which this element's value will submit.
83-
*/
84-
get form() {
85-
return this.internals.form;
86-
}
87-
88-
/**
89-
* The labels this element is associated with.
90-
*/
91-
get labels() {
92-
return this.internals.labels;
93-
}
94-
9577
/**
9678
* Returns a ValidityState object that represents the validity states of the
9779
* checkbox.
@@ -103,7 +85,7 @@ export class Checkbox extends LitElement {
10385
*/
10486
get validity() {
10587
this.syncValidity();
106-
return this.internals.validity;
88+
return this[internals].validity;
10789
}
10890

10991
/**
@@ -113,7 +95,7 @@ export class Checkbox extends LitElement {
11395
*/
11496
get validationMessage() {
11597
this.syncValidity();
116-
return this.internals.validationMessage;
98+
return this[internals].validationMessage;
11799
}
118100

119101
/**
@@ -124,18 +106,16 @@ export class Checkbox extends LitElement {
124106
*/
125107
get willValidate() {
126108
this.syncValidity();
127-
return this.internals.willValidate;
109+
return this[internals].willValidate;
128110
}
129111

130112
@state() private prevChecked = false;
131113
@state() private prevDisabled = false;
132114
@state() private prevIndeterminate = false;
133115
@query('input') private readonly input!: HTMLInputElement | null;
134116
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
135-
// Replace with this.internals.validity.customError when resolved.
117+
// Replace with this[internals].validity.customError when resolved.
136118
private hasCustomValidityError = false;
137-
// Cast needed for closure
138-
private readonly internals = (this as HTMLElement).attachInternals();
139119

140120
constructor() {
141121
super();
@@ -162,7 +142,7 @@ export class Checkbox extends LitElement {
162142
*/
163143
checkValidity() {
164144
this.syncValidity();
165-
return this.internals.checkValidity();
145+
return this[internals].checkValidity();
166146
}
167147

168148
/**
@@ -180,7 +160,7 @@ export class Checkbox extends LitElement {
180160
*/
181161
reportValidity() {
182162
this.syncValidity();
183-
return this.internals.reportValidity();
163+
return this[internals].reportValidity();
184164
}
185165

186166
/**
@@ -196,7 +176,7 @@ export class Checkbox extends LitElement {
196176
*/
197177
setCustomValidity(error: string) {
198178
this.hasCustomValidityError = !!error;
199-
this.internals.setValidity({customError: !!error}, error, this.getInput());
179+
this[internals].setValidity({customError: !!error}, error, this.getInput());
200180
}
201181

202182
protected override update(changed: PropertyValues<Checkbox>) {
@@ -211,9 +191,6 @@ export class Checkbox extends LitElement {
211191
changed.get('indeterminate') ?? this.indeterminate;
212192
}
213193

214-
const shouldAddFormValue = this.checked && !this.indeterminate;
215-
const state = String(this.checked);
216-
this.internals.setFormValue(shouldAddFormValue ? this.value : null, state);
217194
super.update(changed);
218195
}
219196

@@ -285,12 +262,12 @@ export class Checkbox extends LitElement {
285262
// validity. We do this to re-use native `<input>` validation messages.
286263
const input = this.getInput();
287264
if (this.hasCustomValidityError) {
288-
input.setCustomValidity(this.internals.validationMessage);
265+
input.setCustomValidity(this[internals].validationMessage);
289266
} else {
290267
input.setCustomValidity('');
291268
}
292269

293-
this.internals.setValidity(
270+
this[internals].setValidity(
294271
input.validity,
295272
input.validationMessage,
296273
this.getInput(),
@@ -314,15 +291,29 @@ export class Checkbox extends LitElement {
314291
return this.input!;
315292
}
316293

317-
/** @private */
318-
formResetCallback() {
294+
// Writable mixin properties for lit-html binding, needed for lit-analyzer
295+
declare disabled: boolean;
296+
declare name: string;
297+
298+
override [getFormValue]() {
299+
if (!this.checked || this.indeterminate) {
300+
return null;
301+
}
302+
303+
return this.value;
304+
}
305+
306+
override [getFormState]() {
307+
return String(this.checked);
308+
}
309+
310+
override formResetCallback() {
319311
// The checked property does not reflect, so the original attribute set by
320312
// the user is used to determine the default value.
321313
this.checked = this.hasAttribute('checked');
322314
}
323315

324-
/** @private */
325-
formStateRestoreCallback(state: string) {
316+
override formStateRestoreCallback(state: string) {
326317
this.checked = state === 'true';
327318
}
328319
}

internal/aria/aria.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,12 @@ export function polyfillARIAMixin(ctor: typeof ReactiveElement) {
319319
* Polyfills an element and its `ElementInternals` to support `ARIAMixin`
320320
* properties on internals. This is needed for Firefox.
321321
*
322-
* `setupHostAria()` must be called for the element class.
322+
* `polyfillARIAMixin()` must be called for the element class.
323323
*
324324
* @example
325325
* class XButton extends LitElement {
326326
* static {
327-
* setupHostAria(XButton);
327+
* polyfillARIAMixin(XButton);
328328
* }
329329
*
330330
* private internals =
@@ -345,7 +345,7 @@ export function polyfillElementInternalsAria(
345345
}
346346

347347
if (!('role' in host)) {
348-
throw new Error('Missing setupHostAria()');
348+
throw new Error('Missing polyfillARIAMixin()');
349349
}
350350

351351
let firstConnectedCallbacks: Array<{

internal/controller/form-submitter.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,14 @@ type FormSubmitterConstructor =
6666
*
6767
* @example
6868
* ```ts
69-
* class MyElement extends LitElement {
69+
* class MyElement extends mixinElementInternals(LitElement) {
7070
* static {
7171
* setupFormSubmitter(MyElement);
7272
* }
7373
*
7474
* static formAssociated = true;
7575
*
7676
* type: FormSubmitterType = 'submit';
77-
*
78-
* [internals] = this.attachInternals();
7977
* }
8078
* ```
8179
*

labs/behaviors/element-internals.ts

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

77
import {LitElement} from 'lit';
88

9+
import {
10+
polyfillARIAMixin,
11+
polyfillElementInternalsAria,
12+
} from '../../internal/aria/aria.js';
913
import {MixinBase, MixinReturn} from './mixin.js';
1014

1115
/**
@@ -38,6 +42,9 @@ export interface WithElementInternals {
3842
[internals]: ElementInternals;
3943
}
4044

45+
// Private symbols
46+
const privateInternals = Symbol('privateInternals');
47+
4148
/**
4249
* Mixes in an attached `ElementInternals` instance.
4350
*
@@ -54,8 +61,27 @@ export function mixinElementInternals<T extends MixinBase<LitElement>>(
5461
extends base
5562
implements WithElementInternals
5663
{
57-
// Cast needed for closure
58-
[internals] = (this as HTMLElement).attachInternals();
64+
static {
65+
polyfillARIAMixin(
66+
WithElementInternalsElement as unknown as typeof LitElement,
67+
);
68+
}
69+
70+
get [internals]() {
71+
// Create internals in getter so that it can be used in methods called on
72+
// construction in `ReactiveElement`, such as `requestUpdate()`.
73+
if (!this[privateInternals]) {
74+
// Cast needed for closure
75+
this[privateInternals] = polyfillElementInternalsAria(
76+
this,
77+
(this as HTMLElement).attachInternals(),
78+
);
79+
}
80+
81+
return this[privateInternals];
82+
}
83+
84+
[privateInternals]?: ElementInternals;
5985
}
6086

6187
return WithElementInternalsElement;

0 commit comments

Comments
 (0)