UNPKG

@material/mwc-textfield

Version:
616 lines • 24 kB
/** * @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)}" @input="${this.handleInputChange}" @focus="${this.onInputFocus}" @blur="${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