mirror of
https://github.com/material-components/material-web.git
synced 2024-09-17 16:48:02 +03:00
36aed9a2df
This is needed specifically for text fields. There appears to be an odd Chrome/Safari issue where an element that `delegatesFocus: true` to an inner `<input>` MUST provide that input as the validity anchor. Otherwise, when a form submits or calls `form.reportValidity()`, an error will be thrown. See https://lit.dev/playground/#gist=6c26e418e0010f7a5aac15005cde8bde for a reproduction. Unfortunately I couldn't programmatically trigger this behavior in a jasmine test. PiperOrigin-RevId: 586723140
260 lines
7.9 KiB
TypeScript
260 lines
7.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2023 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {isServer, LitElement, PropertyDeclaration, PropertyValues} 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 extends FormAssociated {
|
|
/**
|
|
* 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');
|
|
|
|
/**
|
|
* Mixes 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]();
|
|
}
|
|
|
|
override firstUpdated(changed: PropertyValues) {
|
|
super.firstUpdated(changed);
|
|
// Sync the validity again when the element first renders, since the
|
|
// validity anchor is now available.
|
|
//
|
|
// Elements that `delegatesFocus: true` to an `<input>` will throw an
|
|
// error in Chrome and Safari when a form tries to submit or call
|
|
// `form.reportValidity()`:
|
|
// "An invalid form control with name='' is not focusable"
|
|
//
|
|
// The validity anchor MUST be provided in `internals.setValidity()` and
|
|
// MUST be the `<input>` element rendered.
|
|
//
|
|
// See https://lit.dev/playground/#gist=6c26e418e0010f7a5aac15005cde8bde
|
|
// for a reproduction.
|
|
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;
|
|
}
|