UNPKG

24 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2019 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
5 */
6import { __decorate } from "tslib";
7// Style preference for leading underscores.
8// tslint:disable:strip-private-property-underscore
9import '@material/mwc-notched-outline/mwc-notched-outline.js';
10import { addHasRemoveClass, FormElement } from '@material/mwc-base/form-element.js';
11import { observer } from '@material/mwc-base/observer.js';
12import { floatingLabel } from '@material/mwc-floating-label/mwc-floating-label-directive.js';
13import { lineRipple } from '@material/mwc-line-ripple/mwc-line-ripple-directive.js';
14import MDCTextFieldFoundation from '@material/textfield/foundation.js';
15import { html } from 'lit';
16import { eventOptions, property, query, state } from 'lit/decorators.js';
17import { classMap } from 'lit/directives/class-map.js';
18import { ifDefined } from 'lit/directives/if-defined.js';
19import { live } from 'lit/directives/live.js';
20const passiveEvents = ['touchstart', 'touchmove', 'scroll', 'mousewheel'];
21const createValidityObj = (customValidity = {}) => {
22 /*
23 * We need to make ValidityState an object because it is readonly and
24 * we cannot use the spread operator. Also, we don't export
25 * `CustomValidityState` because it is a leaky implementation and the user
26 * already has access to `ValidityState` in lib.dom.ts. Also an interface
27 * {a: Type} can be casted to {readonly a: Type} so passing any object
28 * should be fine.
29 */
30 const objectifiedCustomValidity = {};
31 // eslint-disable-next-line guard-for-in
32 for (const propName in customValidity) {
33 /*
34 * Casting is needed because ValidityState's props are all readonly and
35 * thus cannot be set on `onjectifiedCustomValidity`. In the end, the
36 * interface is the same as ValidityState (but not readonly), but the
37 * function signature casts the output to ValidityState (thus readonly).
38 */
39 objectifiedCustomValidity[propName] =
40 customValidity[propName];
41 }
42 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);
43};
44/** @soyCompatible */
45export class TextFieldBase extends FormElement {
46 constructor() {
47 super(...arguments);
48 this.mdcFoundationClass = MDCTextFieldFoundation;
49 this.value = '';
50 this.type = 'text';
51 this.placeholder = '';
52 this.label = '';
53 this.icon = '';
54 this.iconTrailing = '';
55 this.disabled = false;
56 this.required = false;
57 this.minLength = -1;
58 this.maxLength = -1;
59 this.outlined = false;
60 this.helper = '';
61 this.validateOnInitialRender = false;
62 this.validationMessage = '';
63 this.autoValidate = false;
64 this.pattern = '';
65 this.min = '';
66 this.max = '';
67 /**
68 * step can be a number or the keyword "any".
69 *
70 * Use `String` typing to pass down the value as a string and let the native
71 * input cast internally as needed.
72 */
73 this.step = null;
74 this.size = null;
75 this.helperPersistent = false;
76 this.charCounter = false;
77 this.endAligned = false;
78 this.prefix = '';
79 this.suffix = '';
80 this.name = '';
81 this.readOnly = false;
82 this.autocapitalize = '';
83 this.outlineOpen = false;
84 this.outlineWidth = 0;
85 this.isUiValid = true;
86 this.focused = false;
87 this._validity = createValidityObj();
88 this.validityTransform = null;
89 }
90 get validity() {
91 this._checkValidity(this.value);
92 return this._validity;
93 }
94 get willValidate() {
95 return this.formElement.willValidate;
96 }
97 get selectionStart() {
98 return this.formElement.selectionStart;
99 }
100 get selectionEnd() {
101 return this.formElement.selectionEnd;
102 }
103 focus() {
104 const focusEvt = new CustomEvent('focus');
105 this.formElement.dispatchEvent(focusEvt);
106 this.formElement.focus();
107 }
108 blur() {
109 const blurEvt = new CustomEvent('blur');
110 this.formElement.dispatchEvent(blurEvt);
111 this.formElement.blur();
112 }
113 select() {
114 this.formElement.select();
115 }
116 setSelectionRange(selectionStart, selectionEnd, selectionDirection) {
117 this.formElement.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
118 }
119 update(changedProperties) {
120 if (changedProperties.has('autoValidate') && this.mdcFoundation) {
121 this.mdcFoundation.setValidateOnValueChange(this.autoValidate);
122 }
123 if (changedProperties.has('value') && typeof this.value !== 'string') {
124 this.value = `${this.value}`;
125 }
126 super.update(changedProperties);
127 }
128 setFormData(formData) {
129 if (this.name) {
130 formData.append(this.name, this.value);
131 }
132 }
133 /** @soyTemplate */
134 render() {
135 const shouldRenderCharCounter = this.charCounter && this.maxLength !== -1;
136 const shouldRenderHelperText = !!this.helper || !!this.validationMessage || shouldRenderCharCounter;
137 /** @classMap */
138 const classes = {
139 'mdc-text-field--disabled': this.disabled,
140 'mdc-text-field--no-label': !this.label,
141 'mdc-text-field--filled': !this.outlined,
142 'mdc-text-field--outlined': this.outlined,
143 'mdc-text-field--with-leading-icon': this.icon,
144 'mdc-text-field--with-trailing-icon': this.iconTrailing,
145 'mdc-text-field--end-aligned': this.endAligned,
146 };
147 return html `
148 <label class="mdc-text-field ${classMap(classes)}">
149 ${this.renderRipple()}
150 ${this.outlined ? this.renderOutline() : this.renderLabel()}
151 ${this.renderLeadingIcon()}
152 ${this.renderPrefix()}
153 ${this.renderInput(shouldRenderHelperText)}
154 ${this.renderSuffix()}
155 ${this.renderTrailingIcon()}
156 ${this.renderLineRipple()}
157 </label>
158 ${this.renderHelperText(shouldRenderHelperText, shouldRenderCharCounter)}
159 `;
160 }
161 updated(changedProperties) {
162 if (changedProperties.has('value') &&
163 changedProperties.get('value') !== undefined) {
164 this.mdcFoundation.setValue(this.value);
165 if (this.autoValidate) {
166 this.reportValidity();
167 }
168 }
169 }
170 /** @soyTemplate */
171 renderRipple() {
172 return this.outlined ? '' : html `
173 <span class="mdc-text-field__ripple"></span>
174 `;
175 }
176 /** @soyTemplate */
177 renderOutline() {
178 return !this.outlined ? '' : html `
179 <mwc-notched-outline
180 .width=${this.outlineWidth}
181 .open=${this.outlineOpen}
182 class="mdc-notched-outline">
183 ${this.renderLabel()}
184 </mwc-notched-outline>`;
185 }
186 /** @soyTemplate */
187 renderLabel() {
188 return !this.label ?
189 '' :
190 html `
191 <span
192 .floatingLabelFoundation=${floatingLabel(this.label)}
193 id="label">${this.label}</span>
194 `;
195 }
196 /** @soyTemplate */
197 renderLeadingIcon() {
198 return this.icon ? this.renderIcon(this.icon) : '';
199 }
200 /** @soyTemplate */
201 renderTrailingIcon() {
202 return this.iconTrailing ? this.renderIcon(this.iconTrailing, true) : '';
203 }
204 /** @soyTemplate */
205 renderIcon(icon, isTrailingIcon = false) {
206 /** @classMap */
207 const classes = {
208 'mdc-text-field__icon--leading': !isTrailingIcon,
209 'mdc-text-field__icon--trailing': isTrailingIcon
210 };
211 return html `<i class="material-icons mdc-text-field__icon ${classMap(classes)}">${icon}</i>`;
212 }
213 /** @soyTemplate */
214 renderPrefix() {
215 return this.prefix ? this.renderAffix(this.prefix) : '';
216 }
217 /** @soyTemplate */
218 renderSuffix() {
219 return this.suffix ? this.renderAffix(this.suffix, true) : '';
220 }
221 /** @soyTemplate */
222 renderAffix(content, isSuffix = false) {
223 /** @classMap */
224 const classes = {
225 'mdc-text-field__affix--prefix': !isSuffix,
226 'mdc-text-field__affix--suffix': isSuffix
227 };
228 return html `<span class="mdc-text-field__affix ${classMap(classes)}">
229 ${content}</span>`;
230 }
231 /** @soyTemplate */
232 renderInput(shouldRenderHelperText) {
233 const minOrUndef = this.minLength === -1 ? undefined : this.minLength;
234 const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
235 const autocapitalizeOrUndef = this.autocapitalize ?
236 this.autocapitalize :
237 undefined;
238 const showValidationMessage = this.validationMessage && !this.isUiValid;
239 const ariaLabelledbyOrUndef = !!this.label ? 'label' : undefined;
240 const ariaControlsOrUndef = shouldRenderHelperText ? 'helper-text' : undefined;
241 const ariaDescribedbyOrUndef = this.focused || this.helperPersistent || showValidationMessage ?
242 'helper-text' :
243 undefined;
244 // TODO: live() directive needs casting for lit-analyzer
245 // https://github.com/runem/lit-analyzer/pull/91/files
246 // TODO: lit-analyzer labels min/max as (number|string) instead of string
247 return html `
248 <input
249 aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
250 aria-controls="${ifDefined(ariaControlsOrUndef)}"
251 aria-describedby="${ifDefined(ariaDescribedbyOrUndef)}"
252 class="mdc-text-field__input"
253 type="${this.type}"
254 .value="${live(this.value)}"
255 ?disabled="${this.disabled}"
256 placeholder="${this.placeholder}"
257 ?required="${this.required}"
258 ?readonly="${this.readOnly}"
259 minlength="${ifDefined(minOrUndef)}"
260 maxlength="${ifDefined(maxOrUndef)}"
261 pattern="${ifDefined(this.pattern ? this.pattern : undefined)}"
262 min="${ifDefined(this.min === '' ? undefined : this.min)}"
263 max="${ifDefined(this.max === '' ? undefined : this.max)}"
264 step="${ifDefined(this.step === null ? undefined : this.step)}"
265 size="${ifDefined(this.size === null ? undefined : this.size)}"
266 name="${ifDefined(this.name === '' ? undefined : this.name)}"
267 inputmode="${ifDefined(this.inputMode)}"
268 autocapitalize="${ifDefined(autocapitalizeOrUndef)}"
269 @input="${this.handleInputChange}"
270 @focus="${this.onInputFocus}"
271 @blur="${this.onInputBlur}">`;
272 }
273 /** @soyTemplate */
274 renderLineRipple() {
275 return this.outlined ?
276 '' :
277 html `
278 <span .lineRippleFoundation=${lineRipple()}></span>
279 `;
280 }
281 /** @soyTemplate */
282 renderHelperText(shouldRenderHelperText, shouldRenderCharCounter) {
283 const showValidationMessage = this.validationMessage && !this.isUiValid;
284 /** @classMap */
285 const classes = {
286 'mdc-text-field-helper-text--persistent': this.helperPersistent,
287 'mdc-text-field-helper-text--validation-msg': showValidationMessage,
288 };
289 const ariaHiddenOrUndef = this.focused || this.helperPersistent || showValidationMessage ?
290 undefined :
291 'true';
292 const helperText = showValidationMessage ? this.validationMessage : this.helper;
293 return !shouldRenderHelperText ? '' : html `
294 <div class="mdc-text-field-helper-line">
295 <div id="helper-text"
296 aria-hidden="${ifDefined(ariaHiddenOrUndef)}"
297 class="mdc-text-field-helper-text ${classMap(classes)}"
298 >${helperText}</div>
299 ${this.renderCharCounter(shouldRenderCharCounter)}
300 </div>`;
301 }
302 /** @soyTemplate */
303 renderCharCounter(shouldRenderCharCounter) {
304 const length = Math.min(this.value.length, this.maxLength);
305 return !shouldRenderCharCounter ? '' : html `
306 <span class="mdc-text-field-character-counter"
307 >${length} / ${this.maxLength}</span>`;
308 }
309 onInputFocus() {
310 this.focused = true;
311 }
312 onInputBlur() {
313 this.focused = false;
314 this.reportValidity();
315 }
316 checkValidity() {
317 const isValid = this._checkValidity(this.value);
318 if (!isValid) {
319 const invalidEvent = new Event('invalid', { bubbles: false, cancelable: true });
320 this.dispatchEvent(invalidEvent);
321 }
322 return isValid;
323 }
324 reportValidity() {
325 const isValid = this.checkValidity();
326 this.mdcFoundation.setValid(isValid);
327 this.isUiValid = isValid;
328 return isValid;
329 }
330 _checkValidity(value) {
331 const nativeValidity = this.formElement.validity;
332 let validity = createValidityObj(nativeValidity);
333 if (this.validityTransform) {
334 const customValidity = this.validityTransform(value, validity);
335 validity = Object.assign(Object.assign({}, validity), customValidity);
336 this.mdcFoundation.setUseNativeValidation(false);
337 }
338 else {
339 this.mdcFoundation.setUseNativeValidation(true);
340 }
341 this._validity = validity;
342 return this._validity.valid;
343 }
344 setCustomValidity(message) {
345 this.validationMessage = message;
346 this.formElement.setCustomValidity(message);
347 }
348 handleInputChange() {
349 this.value = this.formElement.value;
350 }
351 createAdapter() {
352 return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, this.getRootAdapterMethods()), this.getInputAdapterMethods()), this.getLabelAdapterMethods()), this.getLineRippleAdapterMethods()), this.getOutlineAdapterMethods());
353 }
354 getRootAdapterMethods() {
355 return Object.assign({ registerTextFieldInteractionHandler: (evtType, handler) => this.addEventListener(evtType, handler), deregisterTextFieldInteractionHandler: (evtType, handler) => this.removeEventListener(evtType, handler), registerValidationAttributeChangeHandler: (handler) => {
356 const getAttributesList = (mutationsList) => {
357 return mutationsList.map((mutation) => mutation.attributeName)
358 .filter((attributeName) => attributeName);
359 };
360 const observer = new MutationObserver((mutationsList) => {
361 handler(getAttributesList(mutationsList));
362 });
363 const config = { attributes: true };
364 observer.observe(this.formElement, config);
365 return observer;
366 }, deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect() }, addHasRemoveClass(this.mdcRoot));
367 }
368 getInputAdapterMethods() {
369 return {
370 getNativeInput: () => this.formElement,
371 // since HelperTextFoundation is not used, aria-describedby a11y logic
372 // is implemented in render method instead of these adapter methods
373 setInputAttr: () => undefined,
374 removeInputAttr: () => undefined,
375 isFocused: () => this.shadowRoot ?
376 this.shadowRoot.activeElement === this.formElement :
377 false,
378 registerInputInteractionHandler: (evtType, handler) => this.formElement.addEventListener(evtType, handler, { passive: evtType in passiveEvents }),
379 deregisterInputInteractionHandler: (evtType, handler) => this.formElement.removeEventListener(evtType, handler),
380 };
381 }
382 getLabelAdapterMethods() {
383 return {
384 floatLabel: (shouldFloat) => this.labelElement &&
385 this.labelElement.floatingLabelFoundation.float(shouldFloat),
386 getLabelWidth: () => {
387 return this.labelElement ?
388 this.labelElement.floatingLabelFoundation.getWidth() :
389 0;
390 },
391 hasLabel: () => Boolean(this.labelElement),
392 shakeLabel: (shouldShake) => this.labelElement &&
393 this.labelElement.floatingLabelFoundation.shake(shouldShake),
394 setLabelRequired: (isRequired) => {
395 if (this.labelElement) {
396 this.labelElement.floatingLabelFoundation.setRequired(isRequired);
397 }
398 },
399 };
400 }
401 getLineRippleAdapterMethods() {
402 return {
403 activateLineRipple: () => {
404 if (this.lineRippleElement) {
405 this.lineRippleElement.lineRippleFoundation.activate();
406 }
407 },
408 deactivateLineRipple: () => {
409 if (this.lineRippleElement) {
410 this.lineRippleElement.lineRippleFoundation.deactivate();
411 }
412 },
413 setLineRippleTransformOrigin: (normalizedX) => {
414 if (this.lineRippleElement) {
415 this.lineRippleElement.lineRippleFoundation.setRippleCenter(normalizedX);
416 }
417 },
418 };
419 }
420 // tslint:disable:ban-ts-ignore
421 async getUpdateComplete() {
422 var _a;
423 // @ts-ignore
424 const result = await super.getUpdateComplete();
425 await ((_a = this.outlineElement) === null || _a === void 0 ? void 0 : _a.updateComplete);
426 return result;
427 }
428 // tslint:enable:ban-ts-ignore
429 firstUpdated() {
430 var _a;
431 super.firstUpdated();
432 this.mdcFoundation.setValidateOnValueChange(this.autoValidate);
433 if (this.validateOnInitialRender) {
434 this.reportValidity();
435 }
436 // wait for the outline element to render to update the notch width
437 (_a = this.outlineElement) === null || _a === void 0 ? void 0 : _a.updateComplete.then(() => {
438 var _a;
439 // `foundation.notchOutline()` assumes the label isn't floating and
440 // multiplies by a constant, but the label is already is floating at this
441 // stage, therefore directly set the outline width to the label width
442 this.outlineWidth =
443 ((_a = this.labelElement) === null || _a === void 0 ? void 0 : _a.floatingLabelFoundation.getWidth()) || 0;
444 });
445 }
446 getOutlineAdapterMethods() {
447 return {
448 closeOutline: () => this.outlineElement && (this.outlineOpen = false),
449 hasOutline: () => Boolean(this.outlineElement),
450 notchOutline: (labelWidth) => {
451 const outlineElement = this.outlineElement;
452 if (outlineElement && !this.outlineOpen) {
453 this.outlineWidth = labelWidth;
454 this.outlineOpen = true;
455 }
456 }
457 };
458 }
459 async layout() {
460 await this.updateComplete;
461 const labelElement = this.labelElement;
462 if (!labelElement) {
463 this.outlineOpen = false;
464 return;
465 }
466 const shouldFloat = !!this.label && !!this.value;
467 labelElement.floatingLabelFoundation.float(shouldFloat);
468 if (!this.outlined) {
469 return;
470 }
471 this.outlineOpen = shouldFloat;
472 await this.updateComplete;
473 /* When the textfield automatically notches due to a value and label
474 * being defined, the textfield may be set to `display: none` by the user.
475 * this means that the notch is of size 0px. We provide this function so
476 * that the user may manually resize the notch to the floated label's
477 * width.
478 */
479 const labelWidth = labelElement.floatingLabelFoundation.getWidth();
480 if (this.outlineOpen) {
481 this.outlineWidth = labelWidth;
482 await this.updateComplete;
483 }
484 }
485}
486__decorate([
487 query('.mdc-text-field')
488], TextFieldBase.prototype, "mdcRoot", void 0);
489__decorate([
490 query('input')
491], TextFieldBase.prototype, "formElement", void 0);
492__decorate([
493 query('.mdc-floating-label')
494], TextFieldBase.prototype, "labelElement", void 0);
495__decorate([
496 query('.mdc-line-ripple')
497], TextFieldBase.prototype, "lineRippleElement", void 0);
498__decorate([
499 query('mwc-notched-outline')
500], TextFieldBase.prototype, "outlineElement", void 0);
501__decorate([
502 query('.mdc-notched-outline__notch')
503], TextFieldBase.prototype, "notchElement", void 0);
504__decorate([
505 property({ type: String })
506], TextFieldBase.prototype, "value", void 0);
507__decorate([
508 property({ type: String })
509], TextFieldBase.prototype, "type", void 0);
510__decorate([
511 property({ type: String })
512], TextFieldBase.prototype, "placeholder", void 0);
513__decorate([
514 property({ type: String }),
515 observer(function (_newVal, oldVal) {
516 if (oldVal !== undefined && this.label !== oldVal) {
517 this.layout();
518 }
519 })
520], TextFieldBase.prototype, "label", void 0);
521__decorate([
522 property({ type: String })
523], TextFieldBase.prototype, "icon", void 0);
524__decorate([
525 property({ type: String })
526], TextFieldBase.prototype, "iconTrailing", void 0);
527__decorate([
528 property({ type: Boolean, reflect: true })
529], TextFieldBase.prototype, "disabled", void 0);
530__decorate([
531 property({ type: Boolean })
532], TextFieldBase.prototype, "required", void 0);
533__decorate([
534 property({ type: Number })
535], TextFieldBase.prototype, "minLength", void 0);
536__decorate([
537 property({ type: Number })
538], TextFieldBase.prototype, "maxLength", void 0);
539__decorate([
540 property({ type: Boolean, reflect: true }),
541 observer(function (_newVal, oldVal) {
542 if (oldVal !== undefined && this.outlined !== oldVal) {
543 this.layout();
544 }
545 })
546], TextFieldBase.prototype, "outlined", void 0);
547__decorate([
548 property({ type: String })
549], TextFieldBase.prototype, "helper", void 0);
550__decorate([
551 property({ type: Boolean })
552], TextFieldBase.prototype, "validateOnInitialRender", void 0);
553__decorate([
554 property({ type: String })
555], TextFieldBase.prototype, "validationMessage", void 0);
556__decorate([
557 property({ type: Boolean })
558], TextFieldBase.prototype, "autoValidate", void 0);
559__decorate([
560 property({ type: String })
561], TextFieldBase.prototype, "pattern", void 0);
562__decorate([
563 property({ type: String })
564], TextFieldBase.prototype, "min", void 0);
565__decorate([
566 property({ type: String })
567], TextFieldBase.prototype, "max", void 0);
568__decorate([
569 property({ type: String })
570], TextFieldBase.prototype, "step", void 0);
571__decorate([
572 property({ type: Number })
573], TextFieldBase.prototype, "size", void 0);
574__decorate([
575 property({ type: Boolean })
576], TextFieldBase.prototype, "helperPersistent", void 0);
577__decorate([
578 property({ type: Boolean })
579], TextFieldBase.prototype, "charCounter", void 0);
580__decorate([
581 property({ type: Boolean })
582], TextFieldBase.prototype, "endAligned", void 0);
583__decorate([
584 property({ type: String })
585], TextFieldBase.prototype, "prefix", void 0);
586__decorate([
587 property({ type: String })
588], TextFieldBase.prototype, "suffix", void 0);
589__decorate([
590 property({ type: String })
591], TextFieldBase.prototype, "name", void 0);
592__decorate([
593 property({ type: String })
594], TextFieldBase.prototype, "inputMode", void 0);
595__decorate([
596 property({ type: Boolean })
597], TextFieldBase.prototype, "readOnly", void 0);
598__decorate([
599 property({ type: String })
600], TextFieldBase.prototype, "autocapitalize", void 0);
601__decorate([
602 state()
603], TextFieldBase.prototype, "outlineOpen", void 0);
604__decorate([
605 state()
606], TextFieldBase.prototype, "outlineWidth", void 0);
607__decorate([
608 state()
609], TextFieldBase.prototype, "isUiValid", void 0);
610__decorate([
611 state()
612], TextFieldBase.prototype, "focused", void 0);
613__decorate([
614 eventOptions({ passive: true })
615], TextFieldBase.prototype, "handleInputChange", null);
616//# sourceMappingURL=mwc-textfield-base.js.map
\No newline at end of file