@material/mwc-textfield
Version:
Material Design textfield web component
616 lines • 24 kB
JavaScript
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { __decorate } from "tslib";
// Style preference for leading underscores.
// tslint:disable:strip-private-property-underscore
import '@material/mwc-notched-outline/mwc-notched-outline.js';
import { addHasRemoveClass, FormElement } from '@material/mwc-base/form-element.js';
import { observer } from '@material/mwc-base/observer.js';
import { floatingLabel } from '@material/mwc-floating-label/mwc-floating-label-directive.js';
import { lineRipple } from '@material/mwc-line-ripple/mwc-line-ripple-directive.js';
import MDCTextFieldFoundation from '@material/textfield/foundation.js';
import { html } from 'lit';
import { eventOptions, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
const passiveEvents = ['touchstart', 'touchmove', 'scroll', 'mousewheel'];
const createValidityObj = (customValidity = {}) => {
/*
* We need to make ValidityState an object because it is readonly and
* we cannot use the spread operator. Also, we don't export
* `CustomValidityState` because it is a leaky implementation and the user
* already has access to `ValidityState` in lib.dom.ts. Also an interface
* {a: Type} can be casted to {readonly a: Type} so passing any object
* should be fine.
*/
const objectifiedCustomValidity = {};
// eslint-disable-next-line guard-for-in
for (const propName in customValidity) {
/*
* Casting is needed because ValidityState's props are all readonly and
* thus cannot be set on `onjectifiedCustomValidity`. In the end, the
* interface is the same as ValidityState (but not readonly), but the
* function signature casts the output to ValidityState (thus readonly).
*/
objectifiedCustomValidity[propName] =
customValidity[propName];
}
return Object.assign({ badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valid: true, valueMissing: false }, objectifiedCustomValidity);
};
/** @soyCompatible */
export class TextFieldBase extends FormElement {
constructor() {
super(...arguments);
this.mdcFoundationClass = MDCTextFieldFoundation;
this.value = '';
this.type = 'text';
this.placeholder = '';
this.label = '';
this.icon = '';
this.iconTrailing = '';
this.disabled = false;
this.required = false;
this.minLength = -1;
this.maxLength = -1;
this.outlined = false;
this.helper = '';
this.validateOnInitialRender = false;
this.validationMessage = '';
this.autoValidate = false;
this.pattern = '';
this.min = '';
this.max = '';
/**
* step can be a number or the keyword "any".
*
* Use `String` typing to pass down the value as a string and let the native
* input cast internally as needed.
*/
this.step = null;
this.size = null;
this.helperPersistent = false;
this.charCounter = false;
this.endAligned = false;
this.prefix = '';
this.suffix = '';
this.name = '';
this.readOnly = false;
this.autocapitalize = '';
this.outlineOpen = false;
this.outlineWidth = 0;
this.isUiValid = true;
this.focused = false;
this._validity = createValidityObj();
this.validityTransform = null;
}
get validity() {
this._checkValidity(this.value);
return this._validity;
}
get willValidate() {
return this.formElement.willValidate;
}
get selectionStart() {
return this.formElement.selectionStart;
}
get selectionEnd() {
return this.formElement.selectionEnd;
}
focus() {
const focusEvt = new CustomEvent('focus');
this.formElement.dispatchEvent(focusEvt);
this.formElement.focus();
}
blur() {
const blurEvt = new CustomEvent('blur');
this.formElement.dispatchEvent(blurEvt);
this.formElement.blur();
}
select() {
this.formElement.select();
}
setSelectionRange(selectionStart, selectionEnd, selectionDirection) {
this.formElement.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
}
update(changedProperties) {
if (changedProperties.has('autoValidate') && this.mdcFoundation) {
this.mdcFoundation.setValidateOnValueChange(this.autoValidate);
}
if (changedProperties.has('value') && typeof this.value !== 'string') {
this.value = `${this.value}`;
}
super.update(changedProperties);
}
setFormData(formData) {
if (this.name) {
formData.append(this.name, this.value);
}
}
/** @soyTemplate */
render() {
const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1;
const shouldRenderHelperText = !!this.helper || !!this.validationMessage || shouldRenderCharCounter;
/** @classMap */
const classes = {
'mdc-text-field--disabled': this.disabled,
'mdc-text-field--no-label': !this.label,
'mdc-text-field--filled': !this.outlined,
'mdc-text-field--outlined': this.outlined,
'mdc-text-field--with-leading-icon': this.icon,
'mdc-text-field--with-trailing-icon': this.iconTrailing,
'mdc-text-field--end-aligned': this.endAligned,
};
return html `
<label class="mdc-text-field ${classMap(classes)}">
${this.renderRipple()}
${this.outlined ? this.renderOutline() : this.renderLabel()}
${this.renderLeadingIcon()}
${this.renderPrefix()}
${this.renderInput(shouldRenderHelperText)}
${this.renderSuffix()}
${this.renderTrailingIcon()}
${this.renderLineRipple()}
</label>
${this.renderHelperText(shouldRenderHelperText, shouldRenderCharCounter)}
`;
}
updated(changedProperties) {
if (changedProperties.has('value') &&
changedProperties.get('value') !== undefined) {
this.mdcFoundation.setValue(this.value);
if (this.autoValidate) {
this.reportValidity();
}
}
}
/** @soyTemplate */
renderRipple() {
return this.outlined ? '' : html `
<span class="mdc-text-field__ripple"></span>
`;
}
/** @soyTemplate */
renderOutline() {
return !this.outlined ? '' : html `
<mwc-notched-outline
.width=${this.outlineWidth}
.open=${this.outlineOpen}
class="mdc-notched-outline">
${this.renderLabel()}
</mwc-notched-outline>`;
}
/** @soyTemplate */
renderLabel() {
return !this.label ?
'' :
html `
<span
.floatingLabelFoundation=${floatingLabel(this.label)}
id="label">${this.label}</span>
`;
}
/** @soyTemplate */
renderLeadingIcon() {
return this.icon ? this.renderIcon(this.icon) : '';
}
/** @soyTemplate */
renderTrailingIcon() {
return this.iconTrailing ? this.renderIcon(this.iconTrailing, true) : '';
}
/** @soyTemplate */
renderIcon(icon, isTrailingIcon = false) {
/** @classMap */
const classes = {
'mdc-text-field__icon--leading': !isTrailingIcon,
'mdc-text-field__icon--trailing': isTrailingIcon
};
return html `<i class="material-icons mdc-text-field__icon ${classMap(classes)}">${icon}</i>`;
}
/** @soyTemplate */
renderPrefix() {
return this.prefix ? this.renderAffix(this.prefix) : '';
}
/** @soyTemplate */
renderSuffix() {
return this.suffix ? this.renderAffix(this.suffix, true) : '';
}
/** @soyTemplate */
renderAffix(content, isSuffix = false) {
/** @classMap */
const classes = {
'mdc-text-field__affix--prefix': !isSuffix,
'mdc-text-field__affix--suffix': isSuffix
};
return html `<span class="mdc-text-field__affix ${classMap(classes)}">
${content}</span>`;
}
/** @soyTemplate */
renderInput(shouldRenderHelperText) {
const minOrUndef = this.minLength === -1 ? undefined : this.minLength;
const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
const autocapitalizeOrUndef = this.autocapitalize ?
this.autocapitalize :
undefined;
const showValidationMessage = this.validationMessage && !this.isUiValid;
const ariaLabelledbyOrUndef = !!this.label ? 'label' : undefined;
const ariaControlsOrUndef = shouldRenderHelperText ? 'helper-text' : undefined;
const ariaDescribedbyOrUndef = this.focused || this.helperPersistent || showValidationMessage ?
'helper-text' :
undefined;
// TODO: live() directive needs casting for lit-analyzer
// https://github.com/runem/lit-analyzer/pull/91/files
// TODO: lit-analyzer labels min/max as (number|string) instead of string
return html `
<input
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
aria-controls="${ifDefined(ariaControlsOrUndef)}"
aria-describedby="${ifDefined(ariaDescribedbyOrUndef)}"
class="mdc-text-field__input"
type="${this.type}"
.value="${live(this.value)}"
?disabled="${this.disabled}"
placeholder="${this.placeholder}"
?required="${this.required}"
?readonly="${this.readOnly}"
minlength="${ifDefined(minOrUndef)}"
maxlength="${ifDefined(maxOrUndef)}"
pattern="${ifDefined(this.pattern ? this.pattern : undefined)}"
min="${ifDefined(this.min === '' ? undefined : this.min)}"
max="${ifDefined(this.max === '' ? undefined : this.max)}"
step="${ifDefined(this.step === null ? undefined : this.step)}"
size="${ifDefined(this.size === null ? undefined : this.size)}"
name="${ifDefined(this.name === '' ? undefined : this.name)}"
inputmode="${ifDefined(this.inputMode)}"
autocapitalize="${ifDefined(autocapitalizeOrUndef)}"
="${this.handleInputChange}"
="${this.onInputFocus}"
="${this.onInputBlur}">`;
}
/** @soyTemplate */
renderLineRipple() {
return this.outlined ?
'' :
html `
<span .lineRippleFoundation=${lineRipple()}></span>
`;
}
/** @soyTemplate */
renderHelperText(shouldRenderHelperText, shouldRenderCharCounter) {
const showValidationMessage = this.validationMessage && !this.isUiValid;
/** @classMap */
const classes = {
'mdc-text-field-helper-text--persistent': this.helperPersistent,
'mdc-text-field-helper-text--validation-msg': showValidationMessage,
};
const ariaHiddenOrUndef = this.focused || this.helperPersistent || showValidationMessage ?
undefined :
'true';
const helperText = showValidationMessage ? this.validationMessage : this.helper;
return !shouldRenderHelperText ? '' : html `
<div class="mdc-text-field-helper-line">
<div id="helper-text"
aria-hidden="${ifDefined(ariaHiddenOrUndef)}"
class="mdc-text-field-helper-text ${classMap(classes)}"
>${helperText}</div>
${this.renderCharCounter(shouldRenderCharCounter)}
</div>`;
}
/** @soyTemplate */
renderCharCounter(shouldRenderCharCounter) {
const length = Math.min(this.value.length, this.maxLength);
return !shouldRenderCharCounter ? '' : html `
<span class="mdc-text-field-character-counter"
>${length} / ${this.maxLength}</span>`;
}
onInputFocus() {
this.focused = true;
}
onInputBlur() {
this.focused = false;
this.reportValidity();
}
checkValidity() {
const isValid = this._checkValidity(this.value);
if (!isValid) {
const invalidEvent = new Event('invalid', { bubbles: false, cancelable: true });
this.dispatchEvent(invalidEvent);
}
return isValid;
}
reportValidity() {
const isValid = this.checkValidity();
this.mdcFoundation.setValid(isValid);
this.isUiValid = isValid;
return isValid;
}
_checkValidity(value) {
const nativeValidity = this.formElement.validity;
let validity = createValidityObj(nativeValidity);
if (this.validityTransform) {
const customValidity = this.validityTransform(value, validity);
validity = Object.assign(Object.assign({}, validity), customValidity);
this.mdcFoundation.setUseNativeValidation(false);
}
else {
this.mdcFoundation.setUseNativeValidation(true);
}
this._validity = validity;
return this._validity.valid;
}
setCustomValidity(message) {
this.validationMessage = message;
this.formElement.setCustomValidity(message);
}
handleInputChange() {
this.value = this.formElement.value;
}
createAdapter() {
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, this.getRootAdapterMethods()), this.getInputAdapterMethods()), this.getLabelAdapterMethods()), this.getLineRippleAdapterMethods()), this.getOutlineAdapterMethods());
}
getRootAdapterMethods() {
return Object.assign({ registerTextFieldInteractionHandler: (evtType, handler) => this.addEventListener(evtType, handler), deregisterTextFieldInteractionHandler: (evtType, handler) => this.removeEventListener(evtType, handler), registerValidationAttributeChangeHandler: (handler) => {
const getAttributesList = (mutationsList) => {
return mutationsList.map((mutation) => mutation.attributeName)
.filter((attributeName) => attributeName);
};
const observer = new MutationObserver((mutationsList) => {
handler(getAttributesList(mutationsList));
});
const config = { attributes: true };
observer.observe(this.formElement, config);
return observer;
}, deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect() }, addHasRemoveClass(this.mdcRoot));
}
getInputAdapterMethods() {
return {
getNativeInput: () => this.formElement,
// since HelperTextFoundation is not used, aria-describedby a11y logic
// is implemented in render method instead of these adapter methods
setInputAttr: () => undefined,
removeInputAttr: () => undefined,
isFocused: () => this.shadowRoot ?
this.shadowRoot.activeElement === this.formElement :
false,
registerInputInteractionHandler: (evtType, handler) => this.formElement.addEventListener(evtType, handler, { passive: evtType in passiveEvents }),
deregisterInputInteractionHandler: (evtType, handler) => this.formElement.removeEventListener(evtType, handler),
};
}
getLabelAdapterMethods() {
return {
floatLabel: (shouldFloat) => this.labelElement &&
this.labelElement.floatingLabelFoundation.float(shouldFloat),
getLabelWidth: () => {
return this.labelElement ?
this.labelElement.floatingLabelFoundation.getWidth() :
0;
},
hasLabel: () => Boolean(this.labelElement),
shakeLabel: (shouldShake) => this.labelElement &&
this.labelElement.floatingLabelFoundation.shake(shouldShake),
setLabelRequired: (isRequired) => {
if (this.labelElement) {
this.labelElement.floatingLabelFoundation.setRequired(isRequired);
}
},
};
}
getLineRippleAdapterMethods() {
return {
activateLineRipple: () => {
if (this.lineRippleElement) {
this.lineRippleElement.lineRippleFoundation.activate();
}
},
deactivateLineRipple: () => {
if (this.lineRippleElement) {
this.lineRippleElement.lineRippleFoundation.deactivate();
}
},
setLineRippleTransformOrigin: (normalizedX) => {
if (this.lineRippleElement) {
this.lineRippleElement.lineRippleFoundation.setRippleCenter(normalizedX);
}
},
};
}
// tslint:disable:ban-ts-ignore
async getUpdateComplete() {
var _a;
// @ts-ignore
const result = await super.getUpdateComplete();
await ((_a = this.outlineElement) === null || _a === void 0 ? void 0 : _a.updateComplete);
return result;
}
// tslint:enable:ban-ts-ignore
firstUpdated() {
var _a;
super.firstUpdated();
this.mdcFoundation.setValidateOnValueChange(this.autoValidate);
if (this.validateOnInitialRender) {
this.reportValidity();
}
// wait for the outline element to render to update the notch width
(_a = this.outlineElement) === null || _a === void 0 ? void 0 : _a.updateComplete.then(() => {
var _a;
// `foundation.notchOutline()` assumes the label isn't floating and
// multiplies by a constant, but the label is already is floating at this
// stage, therefore directly set the outline width to the label width
this.outlineWidth =
((_a = this.labelElement) === null || _a === void 0 ? void 0 : _a.floatingLabelFoundation.getWidth()) || 0;
});
}
getOutlineAdapterMethods() {
return {
closeOutline: () => this.outlineElement && (this.outlineOpen = false),
hasOutline: () => Boolean(this.outlineElement),
notchOutline: (labelWidth) => {
const outlineElement = this.outlineElement;
if (outlineElement && !this.outlineOpen) {
this.outlineWidth = labelWidth;
this.outlineOpen = true;
}
}
};
}
async layout() {
await this.updateComplete;
const labelElement = this.labelElement;
if (!labelElement) {
this.outlineOpen = false;
return;
}
const shouldFloat = !!this.label && !!this.value;
labelElement.floatingLabelFoundation.float(shouldFloat);
if (!this.outlined) {
return;
}
this.outlineOpen = shouldFloat;
await this.updateComplete;
/* When the textfield automatically notches due to a value and label
* being defined, the textfield may be set to `display: none` by the user.
* this means that the notch is of size 0px. We provide this function so
* that the user may manually resize the notch to the floated label's
* width.
*/
const labelWidth = labelElement.floatingLabelFoundation.getWidth();
if (this.outlineOpen) {
this.outlineWidth = labelWidth;
await this.updateComplete;
}
}
}
__decorate([
query('.mdc-text-field')
], TextFieldBase.prototype, "mdcRoot", void 0);
__decorate([
query('input')
], TextFieldBase.prototype, "formElement", void 0);
__decorate([
query('.mdc-floating-label')
], TextFieldBase.prototype, "labelElement", void 0);
__decorate([
query('.mdc-line-ripple')
], TextFieldBase.prototype, "lineRippleElement", void 0);
__decorate([
query('mwc-notched-outline')
], TextFieldBase.prototype, "outlineElement", void 0);
__decorate([
query('.mdc-notched-outline__notch')
], TextFieldBase.prototype, "notchElement", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "value", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "type", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "placeholder", void 0);
__decorate([
property({ type: String }),
observer(function (_newVal, oldVal) {
if (oldVal !== undefined && this.label !== oldVal) {
this.layout();
}
})
], TextFieldBase.prototype, "label", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "icon", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "iconTrailing", void 0);
__decorate([
property({ type: Boolean, reflect: true })
], TextFieldBase.prototype, "disabled", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "required", void 0);
__decorate([
property({ type: Number })
], TextFieldBase.prototype, "minLength", void 0);
__decorate([
property({ type: Number })
], TextFieldBase.prototype, "maxLength", void 0);
__decorate([
property({ type: Boolean, reflect: true }),
observer(function (_newVal, oldVal) {
if (oldVal !== undefined && this.outlined !== oldVal) {
this.layout();
}
})
], TextFieldBase.prototype, "outlined", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "helper", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "validateOnInitialRender", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "validationMessage", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "autoValidate", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "pattern", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "min", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "max", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "step", void 0);
__decorate([
property({ type: Number })
], TextFieldBase.prototype, "size", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "helperPersistent", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "charCounter", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "endAligned", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "prefix", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "suffix", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "name", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "inputMode", void 0);
__decorate([
property({ type: Boolean })
], TextFieldBase.prototype, "readOnly", void 0);
__decorate([
property({ type: String })
], TextFieldBase.prototype, "autocapitalize", void 0);
__decorate([
state()
], TextFieldBase.prototype, "outlineOpen", void 0);
__decorate([
state()
], TextFieldBase.prototype, "outlineWidth", void 0);
__decorate([
state()
], TextFieldBase.prototype, "isUiValid", void 0);
__decorate([
state()
], TextFieldBase.prototype, "focused", void 0);
__decorate([
eventOptions({ passive: true })
], TextFieldBase.prototype, "handleInputChange", null);
//# sourceMappingURL=mwc-textfield-base.js.map