mirror of
https://github.com/material-components/material-web.git
synced 2024-09-11 13:46:10 +03:00
refactor: add constraint validation mixin
This reduces the copy/paste of validation code. Constraint validation must be synchronous, so a `Validator` helps compute the validity and cache it since the validity must be checked when properties change. Implemented in checkbox-like controls. PiperOrigin-RevId: 584380464
This commit is contained in:
parent
3d8c7ac7f3
commit
f7a66a8bbe
@ -19,9 +19,11 @@ import {
|
||||
redispatchEvent,
|
||||
} from '../../internal/controller/events.js';
|
||||
import {
|
||||
internals,
|
||||
mixinElementInternals,
|
||||
} from '../../labs/behaviors/element-internals.js';
|
||||
createValidator,
|
||||
getValidityAnchor,
|
||||
mixinConstraintValidation,
|
||||
} from '../../labs/behaviors/constraint-validation.js';
|
||||
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
|
||||
import {
|
||||
getFormState,
|
||||
getFormValue,
|
||||
@ -30,8 +32,8 @@ import {
|
||||
import {CheckboxValidator} from '../../labs/behaviors/validators/checkbox-validator.js';
|
||||
|
||||
// Separate variable needed for closure.
|
||||
const checkboxBaseClass = mixinFormAssociated(
|
||||
mixinElementInternals(LitElement),
|
||||
const checkboxBaseClass = mixinConstraintValidation(
|
||||
mixinFormAssociated(mixinElementInternals(LitElement)),
|
||||
);
|
||||
|
||||
/**
|
||||
@ -83,111 +85,24 @@ export class Checkbox extends checkboxBaseClass {
|
||||
*/
|
||||
@property() value = 'on';
|
||||
|
||||
/**
|
||||
* Returns a ValidityState object that represents the validity states of the
|
||||
* checkbox.
|
||||
*
|
||||
* Note that checkboxes will only set `valueMissing` if `required` and not
|
||||
* checked.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
|
||||
*/
|
||||
get validity() {
|
||||
this.syncValidity();
|
||||
return this[internals].validity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the native validation error message.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
||||
*/
|
||||
get validationMessage() {
|
||||
this.syncValidity();
|
||||
return this[internals].validationMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an element will successfully validate based on forms
|
||||
* validation rules and constraints.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
||||
*/
|
||||
get willValidate() {
|
||||
this.syncValidity();
|
||||
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.
|
||||
private customValidityError = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!isServer) {
|
||||
this.addEventListener('click', (event: MouseEvent) => {
|
||||
if (!isActivationClick(event)) {
|
||||
if (!isActivationClick(event) || !this.input) {
|
||||
return;
|
||||
}
|
||||
this.focus();
|
||||
dispatchActivationClick(this.input!);
|
||||
dispatchActivationClick(this.input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the checkbox's native validation and returns whether or not the
|
||||
* element is valid.
|
||||
*
|
||||
* If invalid, this method will dispatch the `invalid` event.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity
|
||||
*
|
||||
* @return true if the checkbox is valid, or false if not.
|
||||
*/
|
||||
checkValidity() {
|
||||
this.syncValidity();
|
||||
return this[internals].checkValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the checkbox's native validation and returns whether or not the
|
||||
* element is valid.
|
||||
*
|
||||
* If invalid, this method will dispatch the `invalid` event.
|
||||
*
|
||||
* The `validationMessage` is reported to the user by the browser. Use
|
||||
* `setCustomValidity()` to customize the `validationMessage`.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity
|
||||
*
|
||||
* @return true if the checkbox is valid, or false if not.
|
||||
*/
|
||||
reportValidity() {
|
||||
this.syncValidity();
|
||||
return this[internals].reportValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the checkbox's native validation error message. This is used to
|
||||
* customize `validationMessage`.
|
||||
*
|
||||
* When the error is not an empty string, the checkbox is considered invalid
|
||||
* and `validity.customError` will be true.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
|
||||
*
|
||||
* @param error The error message to display.
|
||||
*/
|
||||
setCustomValidity(error: string) {
|
||||
this.customValidityError = error;
|
||||
this.syncValidity();
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues<Checkbox>) {
|
||||
if (
|
||||
changed.has('checked') ||
|
||||
@ -252,12 +167,6 @@ export class Checkbox extends checkboxBaseClass {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override updated() {
|
||||
// Sync validity when properties change, since validation properties may
|
||||
// have changed.
|
||||
this.syncValidity();
|
||||
}
|
||||
|
||||
private handleChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.checked = target.checked;
|
||||
@ -266,18 +175,6 @@ export class Checkbox extends checkboxBaseClass {
|
||||
redispatchEvent(this, event);
|
||||
}
|
||||
|
||||
private syncValidity() {
|
||||
const {validity, validationMessage} = this.validator.getValidity();
|
||||
this[internals].setValidity(
|
||||
{
|
||||
...validity,
|
||||
customError: !!this.customValidityError,
|
||||
},
|
||||
this.customValidityError || validationMessage,
|
||||
this.input ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
// Writable mixin properties for lit-html binding, needed for lit-analyzer
|
||||
declare disabled: boolean;
|
||||
declare name: string;
|
||||
@ -304,5 +201,11 @@ export class Checkbox extends checkboxBaseClass {
|
||||
this.checked = state === 'true';
|
||||
}
|
||||
|
||||
private readonly validator = new CheckboxValidator(() => this);
|
||||
[createValidator]() {
|
||||
return new CheckboxValidator(() => this);
|
||||
}
|
||||
|
||||
[getValidityAnchor]() {
|
||||
return this.input;
|
||||
}
|
||||
}
|
||||
|
241
labs/behaviors/constraint-validation.ts
Normal file
241
labs/behaviors/constraint-validation.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {isServer, LitElement, PropertyDeclaration} from 'lit';
|
||||
|
||||
import {internals, WithElementInternals} from './element-internals.js';
|
||||
import {FormAssociated} from './form-associated.js';
|
||||
import {MixinBase, MixinReturn} from './mixin.js';
|
||||
import {Validator} from './validators/validator.js';
|
||||
|
||||
/**
|
||||
* A form associated element that provides constraint validation APIs.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation
|
||||
*/
|
||||
export interface ConstraintValidation {
|
||||
/**
|
||||
* Returns a ValidityState object that represents the validity states of the
|
||||
* element.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
|
||||
*/
|
||||
readonly validity: ValidityState;
|
||||
|
||||
/**
|
||||
* Returns a validation error message or an empty string if the element is
|
||||
* valid.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validationMessage
|
||||
*/
|
||||
readonly validationMessage: string;
|
||||
|
||||
/**
|
||||
* Returns whether an element will successfully validate based on forms
|
||||
* validation rules and constraints.
|
||||
*
|
||||
* Disabled and readonly elements will not validate.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/willValidate
|
||||
*/
|
||||
readonly willValidate: boolean;
|
||||
|
||||
/**
|
||||
* Checks the element's constraint validation and returns true if the element
|
||||
* is valid or false if not.
|
||||
*
|
||||
* If invalid, this method will dispatch an `invalid` event.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity
|
||||
*
|
||||
* @return true if the element is valid, or false if not.
|
||||
*/
|
||||
checkValidity(): boolean;
|
||||
|
||||
/**
|
||||
* Checks the element's constraint validation and returns true if the element
|
||||
* is valid or false if not.
|
||||
*
|
||||
* If invalid, this method will dispatch a cancelable `invalid` event. If not
|
||||
* canceled, a the current `validationMessage` will be reported to the user.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity
|
||||
*
|
||||
* @return true if the element is valid, or false if not.
|
||||
*/
|
||||
reportValidity(): boolean;
|
||||
|
||||
/**
|
||||
* Sets the element's constraint validation error message. When set to a
|
||||
* non-empty string, `validity.customError` will be true and
|
||||
* `validationMessage` will display the provided error.
|
||||
*
|
||||
* Use this method to customize error messages reported.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
|
||||
*
|
||||
* @param error The error message to display, or an empty string.
|
||||
*/
|
||||
setCustomValidity(error: string): void;
|
||||
|
||||
/**
|
||||
* Creates and returns a `Validator` that is used to compute and cache
|
||||
* validity for the element.
|
||||
*
|
||||
* A validator that caches validity is important since constraint validation
|
||||
* must be computed synchronously and frequently in response to constraint
|
||||
* validation property changes.
|
||||
*/
|
||||
[createValidator](): Validator<unknown>;
|
||||
|
||||
/**
|
||||
* Returns shadow DOM child that is used as the anchor for the platform
|
||||
* `reportValidity()` popup. This is often the root element or the inner
|
||||
* focus-delegated element.
|
||||
*/
|
||||
[getValidityAnchor](): HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A symbol property used to create a constraint validation `Validator`.
|
||||
* Required for all `mixinConstraintValidation()` elements.
|
||||
*/
|
||||
export const createValidator = Symbol('createValidator');
|
||||
|
||||
/**
|
||||
* A symbol property used to return an anchor for constraint validation popups.
|
||||
* Required for all `mixinConstraintValidation()` elements.
|
||||
*/
|
||||
export const getValidityAnchor = Symbol('getValidityAnchor');
|
||||
|
||||
// Private symbol members, used to avoid name clashing.
|
||||
const privateValidator = Symbol('privateValidator');
|
||||
const privateSyncValidity = Symbol('privateSyncValidity');
|
||||
const privateCustomValidationMessage = Symbol('privateCustomValidationMessage');
|
||||
|
||||
/**
|
||||
* Mixins in constraint validation APIs for an element.
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation
|
||||
* for more details.
|
||||
*
|
||||
* Implementations must provide a validator to cache and compute its validity,
|
||||
* along with a shadow root element to anchor validation popups to.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const baseClass = mixinConstraintValidation(
|
||||
* mixinFormAssociated(mixinElementInternals(LitElement))
|
||||
* );
|
||||
*
|
||||
* class MyCheckbox extends baseClass {
|
||||
* \@property({type: Boolean}) checked = false;
|
||||
* \@property({type: Boolean}) required = false;
|
||||
*
|
||||
* [createValidator]() {
|
||||
* return new CheckboxValidator(() => this);
|
||||
* }
|
||||
*
|
||||
* [getValidityAnchor]() {
|
||||
* return this.renderRoot.querySelector('.root');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param base The class to mix functionality into.
|
||||
* @return The provided class with `ConstraintValidation` mixed in.
|
||||
*/
|
||||
export function mixinConstraintValidation<
|
||||
T extends MixinBase<LitElement & FormAssociated & WithElementInternals>,
|
||||
>(base: T): MixinReturn<T, ConstraintValidation> {
|
||||
abstract class ConstraintValidationElement
|
||||
extends base
|
||||
implements ConstraintValidation
|
||||
{
|
||||
get validity() {
|
||||
this[privateSyncValidity]();
|
||||
return this[internals].validity;
|
||||
}
|
||||
|
||||
get validationMessage() {
|
||||
this[privateSyncValidity]();
|
||||
return this[internals].validationMessage;
|
||||
}
|
||||
|
||||
get willValidate() {
|
||||
this[privateSyncValidity]();
|
||||
return this[internals].willValidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* A validator instance created from `[createValidator]()`.
|
||||
*/
|
||||
[privateValidator]?: Validator<unknown>;
|
||||
|
||||
/**
|
||||
* Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
||||
* Replace with this[internals].validity.customError when resolved.
|
||||
*/
|
||||
[privateCustomValidationMessage] = '';
|
||||
|
||||
checkValidity() {
|
||||
this[privateSyncValidity]();
|
||||
return this[internals].checkValidity();
|
||||
}
|
||||
|
||||
reportValidity() {
|
||||
this[privateSyncValidity]();
|
||||
return this[internals].reportValidity();
|
||||
}
|
||||
|
||||
setCustomValidity(error: string) {
|
||||
this[privateCustomValidationMessage] = error;
|
||||
this[privateSyncValidity]();
|
||||
}
|
||||
|
||||
override requestUpdate(
|
||||
name?: PropertyKey,
|
||||
oldValue?: unknown,
|
||||
options?: PropertyDeclaration,
|
||||
) {
|
||||
super.requestUpdate(name, oldValue, options);
|
||||
this[privateSyncValidity]();
|
||||
}
|
||||
|
||||
[privateSyncValidity]() {
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this[privateValidator]) {
|
||||
this[privateValidator] = this[createValidator]();
|
||||
}
|
||||
|
||||
const {validity, validationMessage: nonCustomValidationMessage} =
|
||||
this[privateValidator].getValidity();
|
||||
|
||||
const customError = !!this[privateCustomValidationMessage];
|
||||
const validationMessage =
|
||||
this[privateCustomValidationMessage] || nonCustomValidationMessage;
|
||||
|
||||
this[internals].setValidity(
|
||||
{...validity, customError},
|
||||
validationMessage,
|
||||
this[getValidityAnchor]() ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
[createValidator](): Validator<unknown> {
|
||||
throw new Error('Implement [createValidator]');
|
||||
}
|
||||
|
||||
[getValidityAnchor](): HTMLElement | null {
|
||||
throw new Error('Implement [getValidityAnchor]');
|
||||
}
|
||||
}
|
||||
|
||||
return ConstraintValidationElement;
|
||||
}
|
177
labs/behaviors/constraint-validation_test.ts
Normal file
177
labs/behaviors/constraint-validation_test.ts
Normal file
@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// import 'jasmine'; (google3-only)
|
||||
|
||||
import {LitElement, html} from 'lit';
|
||||
import {customElement, property} from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
createValidator,
|
||||
getValidityAnchor,
|
||||
mixinConstraintValidation,
|
||||
} from './constraint-validation.js';
|
||||
import {mixinElementInternals} from './element-internals.js';
|
||||
import {getFormValue, mixinFormAssociated} from './form-associated.js';
|
||||
import {CheckboxValidator} from './validators/checkbox-validator.js';
|
||||
|
||||
describe('mixinConstraintValidation()', () => {
|
||||
const baseClass = mixinConstraintValidation(
|
||||
mixinFormAssociated(mixinElementInternals(LitElement)),
|
||||
);
|
||||
|
||||
@customElement('test-constraint-validation')
|
||||
class TestConstraintValidation extends baseClass {
|
||||
@property({type: Boolean}) checked = false;
|
||||
@property({type: Boolean}) required = false;
|
||||
|
||||
override render() {
|
||||
return html`<div id="root"></div>`;
|
||||
}
|
||||
|
||||
[createValidator]() {
|
||||
return new CheckboxValidator(() => this);
|
||||
}
|
||||
|
||||
[getValidityAnchor]() {
|
||||
return this.shadowRoot?.querySelector<HTMLElement>('#root') ?? null;
|
||||
}
|
||||
|
||||
[getFormValue]() {
|
||||
return String(this.checked);
|
||||
}
|
||||
}
|
||||
|
||||
describe('validity', () => {
|
||||
it('should return a ValidityState value', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
expect(control.validity).toBeInstanceOf(ValidityState);
|
||||
});
|
||||
|
||||
it('should update synchronously when validation properties change', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
|
||||
expect(control.validity.valid)
|
||||
.withContext('validity.valid before changing required')
|
||||
.toBeTrue();
|
||||
control.required = true;
|
||||
expect(control.validity.valid)
|
||||
.withContext('validity.valid after changing required')
|
||||
.toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationMessage', () => {
|
||||
it('should be an empty string when valid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
expect(control.validationMessage).toBe('');
|
||||
});
|
||||
|
||||
it('should have an error message when invalid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
expect(control.validationMessage).not.toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('willValidate', () => {
|
||||
it('should validate by default', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
expect(control.willValidate).withContext('willValidate').toBeTrue();
|
||||
});
|
||||
|
||||
it('should not validate when a disabled attribute is present', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.toggleAttribute('disabled', true);
|
||||
expect(control.willValidate).withContext('willValidate').toBeFalse();
|
||||
});
|
||||
|
||||
it('should not validate when a readonly attribute is present', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.toggleAttribute('readonly', true);
|
||||
expect(control.willValidate).withContext('willValidate').toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkValidity()', () => {
|
||||
it('should return true when element is valid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
expect(control.checkValidity())
|
||||
.withContext('checkValidity() return')
|
||||
.toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when element is invalid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
expect(control.checkValidity())
|
||||
.withContext('checkValidity() return')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should dispatch invalid event when invalid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
const invalidListener = jasmine.createSpy('invalidListener');
|
||||
control.addEventListener('invalid', invalidListener);
|
||||
control.checkValidity();
|
||||
expect(invalidListener).toHaveBeenCalledWith(jasmine.any(Event));
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportValidity()', () => {
|
||||
it('should return true when element is valid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
expect(control.reportValidity())
|
||||
.withContext('reportValidity() return')
|
||||
.toBeTrue();
|
||||
});
|
||||
|
||||
it('should return false when element is invalid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
expect(control.reportValidity())
|
||||
.withContext('reportValidity() return')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should dispatch invalid event when invalid', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
const invalidListener = jasmine.createSpy('invalidListener');
|
||||
control.addEventListener('invalid', invalidListener);
|
||||
control.reportValidity();
|
||||
expect(invalidListener).toHaveBeenCalledWith(jasmine.any(Event));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCustomValidity()', () => {
|
||||
it('should set customError to true when given a non-empty string', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.setCustomValidity('Error');
|
||||
expect(control.validity.customError)
|
||||
.withContext('validity.customError')
|
||||
.toBeTrue();
|
||||
});
|
||||
|
||||
it('should set customError to false when set to an empty string', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.setCustomValidity('');
|
||||
expect(control.validity.customError)
|
||||
.withContext('validity.customError')
|
||||
.toBeFalse();
|
||||
});
|
||||
|
||||
it('should report custom validation message over other validation messages', () => {
|
||||
const control = new TestConstraintValidation();
|
||||
control.required = true;
|
||||
control.setCustomValidity('Error');
|
||||
expect(control.validationMessage)
|
||||
.withContext('validationMessage')
|
||||
.toBe('Error');
|
||||
});
|
||||
});
|
||||
});
|
@ -18,9 +18,11 @@ import {
|
||||
redispatchEvent,
|
||||
} from '../../internal/controller/events.js';
|
||||
import {
|
||||
internals,
|
||||
mixinElementInternals,
|
||||
} from '../../labs/behaviors/element-internals.js';
|
||||
createValidator,
|
||||
getValidityAnchor,
|
||||
mixinConstraintValidation,
|
||||
} from '../../labs/behaviors/constraint-validation.js';
|
||||
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
|
||||
import {
|
||||
getFormState,
|
||||
getFormValue,
|
||||
@ -29,7 +31,9 @@ import {
|
||||
import {CheckboxValidator} from '../../labs/behaviors/validators/checkbox-validator.js';
|
||||
|
||||
// Separate variable needed for closure.
|
||||
const switchBaseClass = mixinFormAssociated(mixinElementInternals(LitElement));
|
||||
const switchBaseClass = mixinConstraintValidation(
|
||||
mixinFormAssociated(mixinElementInternals(LitElement)),
|
||||
);
|
||||
|
||||
/**
|
||||
* @fires input {InputEvent} Fired whenever `selected` changes due to user
|
||||
@ -80,108 +84,21 @@ export class Switch extends switchBaseClass {
|
||||
*/
|
||||
@property() value = 'on';
|
||||
|
||||
/**
|
||||
* Returns a ValidityState object that represents the validity states of the
|
||||
* switch.
|
||||
*
|
||||
* Note that switches will only set `valueMissing` if `required` and not
|
||||
* selected.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
|
||||
*/
|
||||
get validity() {
|
||||
this.syncValidity();
|
||||
return this[internals].validity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the native validation error message.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
||||
*/
|
||||
get validationMessage() {
|
||||
this.syncValidity();
|
||||
return this[internals].validationMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an element will successfully validate based on forms
|
||||
* validation rules and constraints.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
|
||||
*/
|
||||
get willValidate() {
|
||||
this.syncValidity();
|
||||
return this[internals].willValidate;
|
||||
}
|
||||
|
||||
@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.
|
||||
private customValidityError = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!isServer) {
|
||||
this.addEventListener('click', (event: MouseEvent) => {
|
||||
if (!isActivationClick(event)) {
|
||||
if (!isActivationClick(event) || !this.input) {
|
||||
return;
|
||||
}
|
||||
this.focus();
|
||||
dispatchActivationClick(this.input!);
|
||||
dispatchActivationClick(this.input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the switch's native validation and returns whether or not the
|
||||
* element is valid.
|
||||
*
|
||||
* If invalid, this method will dispatch the `invalid` event.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity
|
||||
*
|
||||
* @return true if the switch is valid, or false if not.
|
||||
*/
|
||||
checkValidity() {
|
||||
this.syncValidity();
|
||||
return this[internals].checkValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the switch's native validation and returns whether or not the
|
||||
* element is valid.
|
||||
*
|
||||
* If invalid, this method will dispatch the `invalid` event.
|
||||
*
|
||||
* The `validationMessage` is reported to the user by the browser. Use
|
||||
* `setCustomValidity()` to customize the `validationMessage`.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity
|
||||
*
|
||||
* @return true if the switch is valid, or false if not.
|
||||
*/
|
||||
reportValidity() {
|
||||
this.syncValidity();
|
||||
return this[internals].reportValidity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the switch's native validation error message. This is used to
|
||||
* customize `validationMessage`.
|
||||
*
|
||||
* When the error is not an empty string, the switch is considered invalid
|
||||
* and `validity.customError` will be true.
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
|
||||
*
|
||||
* @param error The error message to display.
|
||||
*/
|
||||
setCustomValidity(error: string) {
|
||||
this.customValidityError = error;
|
||||
this.syncValidity();
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
// NOTE: buttons must use only [phrasing
|
||||
// content](https://html.spec.whatwg.org/multipage/dom.html#phrasing-content)
|
||||
@ -205,12 +122,6 @@ export class Switch extends switchBaseClass {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override updated() {
|
||||
// Sync validity when properties change, since validation properties may
|
||||
// have changed.
|
||||
this.syncValidity();
|
||||
}
|
||||
|
||||
private getRenderClasses(): ClassInfo {
|
||||
return {
|
||||
'selected': this.selected,
|
||||
@ -281,18 +192,6 @@ export class Switch extends switchBaseClass {
|
||||
redispatchEvent(this, event);
|
||||
}
|
||||
|
||||
private syncValidity() {
|
||||
const {validity, validationMessage} = this.validator.getValidity();
|
||||
this[internals].setValidity(
|
||||
{
|
||||
...validity,
|
||||
customError: !!this.customValidityError,
|
||||
},
|
||||
this.customValidityError || validationMessage,
|
||||
this.input ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
// Writable mixin properties for lit-html binding, needed for lit-analyzer
|
||||
declare disabled: boolean;
|
||||
declare name: string;
|
||||
@ -315,8 +214,14 @@ export class Switch extends switchBaseClass {
|
||||
this.selected = state === 'true';
|
||||
}
|
||||
|
||||
private readonly validator = new CheckboxValidator(() => ({
|
||||
checked: this.selected,
|
||||
required: this.required,
|
||||
}));
|
||||
[createValidator]() {
|
||||
return new CheckboxValidator(() => ({
|
||||
checked: this.selected,
|
||||
required: this.required,
|
||||
}));
|
||||
}
|
||||
|
||||
[getValidityAnchor]() {
|
||||
return this.input;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user