Skip to content

Commit

Permalink
chore(behaviors): update form controls to use shared mixins
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 578297611
  • Loading branch information
asyncLiz authored and copybara-github committed Oct 31, 2023
1 parent e1077c7 commit 1f55d41
Show file tree
Hide file tree
Showing 12 changed files with 865 additions and 327 deletions.
93 changes: 42 additions & 51 deletions checkbox/internal/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,25 @@ import {
isActivationClick,
redispatchEvent,
} from '../../internal/controller/events.js';
import {
internals,
mixinElementInternals,
} from '../../labs/behaviors/element-internals.js';
import {
getFormState,
getFormValue,
mixinFormAssociated,
} from '../../labs/behaviors/form-associated.js';

// Separate variable needed for closure.
const checkboxBaseClass = mixinFormAssociated(
mixinElementInternals(LitElement),
);

/**
* A checkbox component.
*/
export class Checkbox extends LitElement {
export class Checkbox extends checkboxBaseClass {
static {
requestUpdateOnAriaChange(Checkbox);
}
Expand All @@ -33,19 +47,11 @@ export class Checkbox extends LitElement {
delegatesFocus: true,
};

/** @nocollapse */
static readonly formAssociated = true;

/**
* Whether or not the checkbox is selected.
*/
@property({type: Boolean}) checked = false;

/**
* Whether or not the checkbox is disabled.
*/
@property({type: Boolean, reflect: true}) disabled = false;

/**
* Whether or not the checkbox is indeterminate.
*
Expand All @@ -68,30 +74,6 @@ export class Checkbox extends LitElement {
*/
@property() value = 'on';

/**
* The HTML name to use in form submission.
*/
get name() {
return this.getAttribute('name') ?? '';
}
set name(name: string) {
this.setAttribute('name', name);
}

/**
* The associated form element with which this element's value will submit.
*/
get form() {
return this.internals.form;
}

/**
* The labels this element is associated with.
*/
get labels() {
return this.internals.labels;
}

/**
* Returns a ValidityState object that represents the validity states of the
* checkbox.
Expand All @@ -103,7 +85,7 @@ export class Checkbox extends LitElement {
*/
get validity() {
this.syncValidity();
return this.internals.validity;
return this[internals].validity;
}

/**
Expand All @@ -113,7 +95,7 @@ export class Checkbox extends LitElement {
*/
get validationMessage() {
this.syncValidity();
return this.internals.validationMessage;
return this[internals].validationMessage;
}

/**
Expand All @@ -124,18 +106,16 @@ export class Checkbox extends LitElement {
*/
get willValidate() {
this.syncValidity();
return this.internals.willValidate;
return this[internals].willValidate;
}

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

constructor() {
super();
Expand All @@ -162,7 +142,7 @@ export class Checkbox extends LitElement {
*/
checkValidity() {
this.syncValidity();
return this.internals.checkValidity();
return this[internals].checkValidity();
}

/**
Expand All @@ -180,7 +160,7 @@ export class Checkbox extends LitElement {
*/
reportValidity() {
this.syncValidity();
return this.internals.reportValidity();
return this[internals].reportValidity();
}

/**
Expand All @@ -196,7 +176,7 @@ export class Checkbox extends LitElement {
*/
setCustomValidity(error: string) {
this.hasCustomValidityError = !!error;
this.internals.setValidity({customError: !!error}, error, this.getInput());
this[internals].setValidity({customError: !!error}, error, this.getInput());
}

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

const shouldAddFormValue = this.checked && !this.indeterminate;
const state = String(this.checked);
this.internals.setFormValue(shouldAddFormValue ? this.value : null, state);
super.update(changed);
}

Expand Down Expand Up @@ -285,12 +262,12 @@ export class Checkbox extends LitElement {
// validity. We do this to re-use native `<input>` validation messages.
const input = this.getInput();
if (this.hasCustomValidityError) {
input.setCustomValidity(this.internals.validationMessage);
input.setCustomValidity(this[internals].validationMessage);
} else {
input.setCustomValidity('');
}

this.internals.setValidity(
this[internals].setValidity(
input.validity,
input.validationMessage,
this.getInput(),
Expand All @@ -314,15 +291,29 @@ export class Checkbox extends LitElement {
return this.input!;
}

/** @private */
formResetCallback() {
// Writable mixin properties for lit-html binding, needed for lit-analyzer
declare disabled: boolean;
declare name: string;

override [getFormValue]() {
if (!this.checked || this.indeterminate) {
return null;
}

return this.value;
}

override [getFormState]() {
return String(this.checked);
}

override formResetCallback() {
// The checked property does not reflect, so the original attribute set by
// the user is used to determine the default value.
this.checked = this.hasAttribute('checked');
}

/** @private */
formStateRestoreCallback(state: string) {
override formStateRestoreCallback(state: string) {
this.checked = state === 'true';
}
}
6 changes: 3 additions & 3 deletions internal/aria/aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,12 @@ export function polyfillARIAMixin(ctor: typeof ReactiveElement) {
* Polyfills an element and its `ElementInternals` to support `ARIAMixin`
* properties on internals. This is needed for Firefox.
*
* `setupHostAria()` must be called for the element class.
* `polyfillARIAMixin()` must be called for the element class.
*
* @example
* class XButton extends LitElement {
* static {
* setupHostAria(XButton);
* polyfillARIAMixin(XButton);
* }
*
* private internals =
Expand All @@ -345,7 +345,7 @@ export function polyfillElementInternalsAria(
}

if (!('role' in host)) {
throw new Error('Missing setupHostAria()');
throw new Error('Missing polyfillARIAMixin()');
}

let firstConnectedCallbacks: Array<{
Expand Down
4 changes: 1 addition & 3 deletions internal/controller/form-submitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,14 @@ type FormSubmitterConstructor =
*
* @example
* ```ts
* class MyElement extends LitElement {
* class MyElement extends mixinElementInternals(LitElement) {
* static {
* setupFormSubmitter(MyElement);
* }
*
* static formAssociated = true;
*
* type: FormSubmitterType = 'submit';
*
* [internals] = this.attachInternals();
* }
* ```
*
Expand Down
30 changes: 28 additions & 2 deletions labs/behaviors/element-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

import {LitElement} from 'lit';

import {
polyfillARIAMixin,
polyfillElementInternalsAria,
} from '../../internal/aria/aria.js';
import {MixinBase, MixinReturn} from './mixin.js';

/**
Expand Down Expand Up @@ -38,6 +42,9 @@ export interface WithElementInternals {
[internals]: ElementInternals;
}

// Private symbols
const privateInternals = Symbol('privateInternals');

/**
* Mixes in an attached `ElementInternals` instance.
*
Expand All @@ -54,8 +61,27 @@ export function mixinElementInternals<T extends MixinBase<LitElement>>(
extends base
implements WithElementInternals
{
// Cast needed for closure
[internals] = (this as HTMLElement).attachInternals();
static {
polyfillARIAMixin(
WithElementInternalsElement as unknown as typeof LitElement,
);
}

get [internals]() {
// Create internals in getter so that it can be used in methods called on
// construction in `ReactiveElement`, such as `requestUpdate()`.
if (!this[privateInternals]) {
// Cast needed for closure
this[privateInternals] = polyfillElementInternalsAria(
this,
(this as HTMLElement).attachInternals(),
);
}

return this[privateInternals];
}

[privateInternals]?: ElementInternals;
}

return WithElementInternalsElement;
Expand Down
Loading

0 comments on commit 1f55d41

Please sign in to comment.