diff --git a/packages/mdc-textfield/README.md b/packages/mdc-textfield/README.md index 5797228e527..67a5a74ddfa 100644 --- a/packages/mdc-textfield/README.md +++ b/packages/mdc-textfield/README.md @@ -374,9 +374,9 @@ Method Signature | Description `deregisterTextFieldInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event handler on the root element for a given event. `registerInputInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the native input element for a given event. `deregisterInputInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the native input element for a given event. -`registerValidationAttributeChangeHandler(handler: function(!Array) => undefined) => !MutationObserver` | Registers a validation attribute change listener on the input element. Handler accepts list of attribute changes. +`registerValidationAttributeChangeHandler(handler: (attributeNames: string[]) => void) => MutationObserver` | Registers a validation attribute change listener on the input element. Handler accepts list of attribute changes. `deregisterValidationAttributeChangeHandler(!MutationObserver) => void` | Disconnects a validation attribute observer on the input element. -`getNativeInput() => {value: string, disabled: boolean, badInput: boolean, checkValidity: () => boolean}?` | Returns an object representing the native text input element, with a similar API shape. +`getNativeInput() => NativeInputType \| null` | Returns an object representing the native text input element, with a similar API shape. See [types.ts](types.ts). `isFocused() => boolean` | Returns whether the input is focused. `hasOutline() => boolean` | Returns whether there is an outline element. `notchOutline(labelWidth: number) => void` | Updates the notched outline path to open the notch and update the notch width for the label element. @@ -384,7 +384,7 @@ Method Signature | Description #### `MDCTextFieldAdapter.getNativeInput()` -Returns an object representing the native text input element, with a similar API shape. The object returned should include the `value`, `disabled` and `badInput` properties, as well as the `checkValidity()` function. We _never_ alter the value within our code, however we _do_ update the disabled property, so if you choose to duck-type the return value for this method in your implementation it's important to keep this in mind. Also note that this method can return null, which the foundation will handle gracefully. +Returns an object representing the native text input element, with a similar API shape. We _never_ alter the value within our code, however we _do_ update the disabled property, so if you choose to duck-type the return value for this method in your implementation it's important to keep this in mind. Also note that this method can return null, which the foundation will handle gracefully. #### `MDCTextFieldAdapter.getIdleOutlineStyleValue(propertyName: string)` diff --git a/packages/mdc-textfield/adapter.js b/packages/mdc-textfield/adapter.js deleted file mode 100644 index 6d6102f8e1d..00000000000 --- a/packages/mdc-textfield/adapter.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * @license - * Copyright 2017 Google Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* eslint-disable no-unused-vars */ -import MDCTextFieldHelperTextFoundation from './helper-text/foundation'; -/* eslint-disable no-unused-vars */ -import MDCTextFieldCharacterCounterFoundation from './character-counter/foundation'; -import MDCTextFieldIconFoundation from './icon/foundation'; - -/* eslint no-unused-vars: [2, {"args": "none"}] */ - -/** - * @typedef {{ - * value: string, - * disabled: boolean, - * badInput: boolean, - * validity: { - * badInput: boolean, - * valid: boolean, - * }, - * }} - */ -let NativeInputType; - -/** - * @typedef {{ - * helperText: (!MDCTextFieldHelperTextFoundation|undefined), - * characterCounter: (!MDCTextFieldCharacterCounterFoundation|undefined), - * leadingIcon: (!MDCTextFieldIconFoundation|undefined), - * trailingIcon: (!MDCTextFieldIconFoundation|undefined), - * }} - */ -let FoundationMapType; - -/** - * Adapter for MDC Text Field. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Text Field into your framework. See - * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md - * for more information. - * - * @record - */ -class MDCTextFieldAdapter { - /** - * Adds a class to the root Element. - * @param {string} className - */ - addClass(className) {} - - /** - * Removes a class from the root Element. - * @param {string} className - */ - removeClass(className) {} - - /** - * Returns true if the root element contains the given class name. - * @param {string} className - * @return {boolean} - */ - hasClass(className) {} - - /** - * Registers an event handler on the root element for a given event. - * @param {string} type - * @param {function(!Event): undefined} handler - */ - registerTextFieldInteractionHandler(type, handler) {} - - /** - * Deregisters an event handler on the root element for a given event. - * @param {string} type - * @param {function(!Event): undefined} handler - */ - deregisterTextFieldInteractionHandler(type, handler) {} - - /** - * Registers an event listener on the native input element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - registerInputInteractionHandler(evtType, handler) {} - - /** - * Deregisters an event listener on the native input element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler - */ - deregisterInputInteractionHandler(evtType, handler) {} - - /** - * Registers a validation attribute change listener on the input element. - * Handler accepts list of attribute names. - * @param {function(!Array): undefined} handler - * @return {!MutationObserver} - */ - registerValidationAttributeChangeHandler(handler) {} - - /** - * Disconnects a validation attribute observer on the input element. - * @param {!MutationObserver} observer - */ - deregisterValidationAttributeChangeHandler(observer) {} - - /** - * Returns an object representing the native text input element, with a - * similar API shape. The object returned should include the value, disabled - * and badInput properties, as well as the checkValidity() function. We never - * alter the value within our code, however we do update the disabled - * property, so if you choose to duck-type the return value for this method - * in your implementation it's important to keep this in mind. Also note that - * this method can return null, which the foundation will handle gracefully. - * @return {?Element|?NativeInputType} - */ - getNativeInput() {} - - /** - * Returns true if the textfield is focused. - * We achieve this via `document.activeElement === this.root_`. - * @return {boolean} - */ - isFocused() {} - - /** - * Activates the line ripple. - */ - activateLineRipple() {} - - /** - * Deactivates the line ripple. - */ - deactivateLineRipple() {} - - /** - * Sets the transform origin of the line ripple. - * @param {number} normalizedX - */ - setLineRippleTransformOrigin(normalizedX) {} - - /** - * Only implement if label exists. - * Shakes label if shouldShake is true. - * @param {boolean} shouldShake - */ - shakeLabel(shouldShake) {} - - /** - * Only implement if label exists. - * Floats the label above the input element if shouldFloat is true. - * @param {boolean} shouldFloat - */ - floatLabel(shouldFloat) {} - - /** - * Returns true if label element exists, false if it doesn't. - * @return {boolean} - */ - hasLabel() {} - - /** - * Only implement if label exists. - * Returns width of label in pixels. - * @return {number} - */ - getLabelWidth() {} - - /** - * Returns true if outline element exists, false if it doesn't. - * @return {boolean} - */ - hasOutline() {} - - /** - * Only implement if outline element exists. - * @param {number} labelWidth - */ - notchOutline(labelWidth) {} - - /** - * Only implement if outline element exists. - * Closes notch in outline element. - */ - closeOutline() {} -} - -export {MDCTextFieldAdapter, NativeInputType, FoundationMapType}; diff --git a/packages/mdc-textfield/adapter.ts b/packages/mdc-textfield/adapter.ts new file mode 100644 index 00000000000..c8163f1f1c3 --- /dev/null +++ b/packages/mdc-textfield/adapter.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {EventType, SpecificEventListener} from '@material/base/types'; +import {NativeInputElement} from './types'; + +/** + * Defines the shape of the adapter expected by the foundation. + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md + */ +interface MDCTextFieldAdapter { + /** + * Adds a class to the root Element. + */ + addClass(className: string): void; + + /** + * Removes a class from the root Element. + */ + removeClass(className: string): void; + + /** + * @return true if the root element contains the given class name. + */ + hasClass(className: string): boolean; + + /** + * Registers an event handler on the root element for a given event. + */ + registerTextFieldInteractionHandler(evtType: E, handler: SpecificEventListener): void; + + /** + * Deregisters an event handler on the root element for a given event. + */ + deregisterTextFieldInteractionHandler(evtType: E, handler: SpecificEventListener): void; + + /** + * Registers an event listener on the native input element for a given event. + */ + registerInputInteractionHandler(evtType: E, handler: SpecificEventListener): void; + + /** + * Deregisters an event listener on the native input element for a given event. + */ + deregisterInputInteractionHandler(evtType: E, handler: SpecificEventListener): void; + + /** + * Registers a validation attribute change listener on the input element. + * Handler accepts list of attribute names. + */ + registerValidationAttributeChangeHandler(handler: (attributeNames: string[]) => void): MutationObserver; + + /** + * Disconnects a validation attribute observer on the input element. + */ + deregisterValidationAttributeChangeHandler(observer: MutationObserver): void; + + /** + * @return The native `` element, or an object with the same shape. + * Note that this method can return null, which the foundation will handle gracefully. + */ + getNativeInput(): NativeInputElement | null; + + /** + * @return true if the textfield is focused. We achieve this via `document.activeElement === this.root_`. + */ + isFocused(): boolean; + + /** + * Activates the line ripple. + */ + activateLineRipple(): void; + + /** + * Deactivates the line ripple. + */ + deactivateLineRipple(): void; + + /** + * Sets the transform origin of the line ripple. + */ + setLineRippleTransformOrigin(normalizedX: number): void; + + /** + * Only implement if label exists. + * Shakes label if shouldShake is true. + */ + shakeLabel(shouldShake: boolean): void; + + /** + * Only implement if label exists. + * Floats the label above the input element if shouldFloat is true. + */ + floatLabel(shouldFloat: boolean): void; + + /** + * @return true if label element exists, false if it doesn't. + */ + hasLabel(): boolean; + + /** + * Only implement if label exists. + * @return width of label in pixels. + */ + getLabelWidth(): number; + + /** + * @return true if outline element exists, false if it doesn't. + */ + hasOutline(): boolean; + + /** + * Only implement if outline element exists. + */ + notchOutline(labelWidth: number): void; + + /** + * Only implement if outline element exists. + * Closes notch in outline element. + */ + closeOutline(): void; +} + +export {MDCTextFieldAdapter as default, MDCTextFieldAdapter}; diff --git a/packages/mdc-textfield/character-counter/adapter.js b/packages/mdc-textfield/character-counter/adapter.ts similarity index 71% rename from packages/mdc-textfield/character-counter/adapter.js rename to packages/mdc-textfield/character-counter/adapter.ts index 50326252124..74351c64cd8 100644 --- a/packages/mdc-textfield/character-counter/adapter.js +++ b/packages/mdc-textfield/character-counter/adapter.ts @@ -21,24 +21,18 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - /** - * Adapter for MDC Text Field Character Counter. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the TextField character counter into your framework. See - * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md - * for more information. - * - * @record + * Defines the shape of the adapter expected by the foundation. + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -class MDCTextFieldCharacterCounterAdapter { +interface MDCTextFieldCharacterCounterAdapter { /** * Sets the text content of character counter element. - * @param {string} content */ - setContent(content) {} + setContent(content: string): void; } -export default MDCTextFieldCharacterCounterAdapter; +export {MDCTextFieldCharacterCounterAdapter as default, MDCTextFieldCharacterCounterAdapter}; diff --git a/packages/mdc-textfield/character-counter/constants.js b/packages/mdc-textfield/character-counter/constants.ts similarity index 96% rename from packages/mdc-textfield/character-counter/constants.js rename to packages/mdc-textfield/character-counter/constants.ts index 60d64e11d61..994fbbf5343 100644 --- a/packages/mdc-textfield/character-counter/constants.js +++ b/packages/mdc-textfield/character-counter/constants.ts @@ -21,12 +21,10 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { ROOT: 'mdc-text-field-character-counter', }; -/** @enum {string} */ const strings = { ROOT_SELECTOR: `.${cssClasses.ROOT}`, }; diff --git a/packages/mdc-textfield/character-counter/foundation.js b/packages/mdc-textfield/character-counter/foundation.ts similarity index 64% rename from packages/mdc-textfield/character-counter/foundation.js rename to packages/mdc-textfield/character-counter/foundation.ts index 6b810d579af..a9c7f8058ae 100644 --- a/packages/mdc-textfield/character-counter/foundation.js +++ b/packages/mdc-textfield/character-counter/foundation.ts @@ -22,50 +22,35 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCTextFieldCharacterCounterAdapter from './adapter'; +import {MDCTextFieldCharacterCounterAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTextFieldCharacterCounterFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCTextFieldCharacterCounterFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } /** - * {@see MDCTextFieldCharacterCounterAdapter} for typing information on parameters and return - * types. - * @return {!MDCTextFieldCharacterCounterAdapter} + * See {@link MDCTextFieldCharacterCounterAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCTextFieldCharacterCounterAdapter} */ ({ - setContent: () => {}, - }); + static get defaultAdapter(): MDCTextFieldCharacterCounterAdapter { + return { + setContent: () => undefined, + }; } - /** - * @param {!MDCTextFieldCharacterCounterAdapter} adapter - */ - constructor(adapter) { - super(Object.assign(MDCTextFieldCharacterCounterFoundation.defaultAdapter, adapter)); + constructor(adapter?: Partial) { + super({...MDCTextFieldCharacterCounterFoundation.defaultAdapter, ...adapter}); } - /** - * @param {number} currentLength - * @param {number} maxLength - */ - setCounterValue(currentLength, maxLength) { + setCounterValue(currentLength: number, maxLength: number) { currentLength = Math.min(currentLength, maxLength); this.adapter_.setContent(`${currentLength} / ${maxLength}`); } } -export default MDCTextFieldCharacterCounterFoundation; +export {MDCTextFieldCharacterCounterFoundation as default, MDCTextFieldCharacterCounterFoundation}; diff --git a/packages/mdc-textfield/character-counter/index.js b/packages/mdc-textfield/character-counter/index.ts similarity index 59% rename from packages/mdc-textfield/character-counter/index.js rename to packages/mdc-textfield/character-counter/index.ts index 0195905032f..d2a39a22c18 100644 --- a/packages/mdc-textfield/character-counter/index.js +++ b/packages/mdc-textfield/character-counter/index.ts @@ -22,41 +22,24 @@ */ import {MDCComponent} from '@material/base/component'; +import {MDCTextFieldCharacterCounterFoundation} from './foundation'; -import MDCTextFieldCharacterCounterAdapter from './adapter'; -import MDCTextFieldCharacterCounterFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTextFieldCharacterCounter extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCTextFieldCharacterCounter} - */ - static attachTo(root) { +class MDCTextFieldCharacterCounter extends MDCComponent { + static attachTo(root: Element): MDCTextFieldCharacterCounter { return new MDCTextFieldCharacterCounter(root); } - /** - * @return {!MDCTextFieldCharacterCounterFoundation} - */ - get foundation() { + get foundation(): MDCTextFieldCharacterCounterFoundation { return this.foundation_; } - /** - * @return {!MDCTextFieldCharacterCounterFoundation} - */ - getDefaultFoundation() { - return new MDCTextFieldCharacterCounterFoundation( - /** @type {!MDCTextFieldCharacterCounterAdapter} */ (Object.assign({ - setContent: (content) => { - this.root_.textContent = content; - }, - }))); + getDefaultFoundation(): MDCTextFieldCharacterCounterFoundation { + return new MDCTextFieldCharacterCounterFoundation({ + setContent: (content) => { this.root_.textContent = content; }, + }); } } -export {MDCTextFieldCharacterCounter, MDCTextFieldCharacterCounterFoundation}; +export {MDCTextFieldCharacterCounter as default, MDCTextFieldCharacterCounter}; +export * from './adapter'; +export * from './foundation'; diff --git a/packages/mdc-textfield/constants.js b/packages/mdc-textfield/constants.ts similarity index 90% rename from packages/mdc-textfield/constants.js rename to packages/mdc-textfield/constants.ts index f26c0922394..9ba0a9495b3 100644 --- a/packages/mdc-textfield/constants.js +++ b/packages/mdc-textfield/constants.ts @@ -21,42 +21,43 @@ * THE SOFTWARE. */ -/** @enum {string} */ const strings = { ARIA_CONTROLS: 'aria-controls', + ICON_SELECTOR: '.mdc-text-field__icon', INPUT_SELECTOR: '.mdc-text-field__input', LABEL_SELECTOR: '.mdc-floating-label', - ICON_SELECTOR: '.mdc-text-field__icon', - OUTLINE_SELECTOR: '.mdc-notched-outline', LINE_RIPPLE_SELECTOR: '.mdc-line-ripple', + OUTLINE_SELECTOR: '.mdc-notched-outline', }; -/** @enum {string} */ const cssClasses = { - ROOT: 'mdc-text-field', - DISABLED: 'mdc-text-field--disabled', DENSE: 'mdc-text-field--dense', + DISABLED: 'mdc-text-field--disabled', FOCUSED: 'mdc-text-field--focused', + HELPER_LINE: 'mdc-text-field-helper-line', INVALID: 'mdc-text-field--invalid', - TEXTAREA: 'mdc-text-field--textarea', OUTLINED: 'mdc-text-field--outlined', + ROOT: 'mdc-text-field', + TEXTAREA: 'mdc-text-field--textarea', WITH_LEADING_ICON: 'mdc-text-field--with-leading-icon', - HELPER_LINE: 'mdc-text-field-helper-line', }; -/** @enum {number} */ const numbers = { - LABEL_SCALE: 0.75, DENSE_LABEL_SCALE: 0.923, + LABEL_SCALE: 0.75, }; -// whitelist based off of https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation -// under section: `Validation-related attributes` +/** + * Whitelist based off of https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation + * under the "Validation-related attributes" section. + */ const VALIDATION_ATTR_WHITELIST = [ 'pattern', 'min', 'max', 'required', 'step', 'minlength', 'maxlength', ]; -// Label should always float for these types as they show some UI even if value is empty. +/** + * Label should always float for these types as they show some UI even if value is empty. + */ const ALWAYS_FLOAT_TYPES = [ 'color', 'date', 'datetime-local', 'month', 'range', 'time', 'week', ]; diff --git a/packages/mdc-textfield/foundation.js b/packages/mdc-textfield/foundation.ts similarity index 59% rename from packages/mdc-textfield/foundation.js rename to packages/mdc-textfield/foundation.ts index 185993ccdfe..acdbf0c840d 100644 --- a/packages/mdc-textfield/foundation.js +++ b/packages/mdc-textfield/foundation.ts @@ -22,127 +22,113 @@ */ import {MDCFoundation} from '@material/base/foundation'; -/* eslint-disable no-unused-vars */ -import MDCTextFieldHelperTextFoundation from './helper-text/foundation'; -import MDCTextFieldCharacterCounterFoundation from './character-counter/foundation'; -import MDCTextFieldIconFoundation from './icon/foundation'; -/* eslint-enable no-unused-vars */ -import {MDCTextFieldAdapter, NativeInputType, FoundationMapType} from './adapter'; -import {cssClasses, strings, numbers, VALIDATION_ATTR_WHITELIST, ALWAYS_FLOAT_TYPES} from './constants'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCTextFieldAdapter} from './adapter'; +import {MDCTextFieldCharacterCounterFoundation} from './character-counter'; +import {ALWAYS_FLOAT_TYPES, cssClasses, numbers, strings, VALIDATION_ATTR_WHITELIST} from './constants'; +import {MDCTextFieldHelperTextFoundation} from './helper-text'; +import {MDCTextFieldIconFoundation} from './icon'; +import {FoundationMapType, NativeInputElement} from './types'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTextFieldFoundation extends MDCFoundation { - /** @return enum {string} */ +type PointerDownEventType = 'mousedown' | 'touchstart'; +type InteractionEventType = 'click' | 'keydown'; + +const POINTERDOWN_EVENTS: PointerDownEventType[] = ['mousedown', 'touchstart']; +const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; + +class MDCTextFieldFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } - /** @return enum {string} */ static get numbers() { return numbers; } - /** @return {boolean} */ - get shouldShake() { - return !this.isValid() && !this.isFocused_ && !!this.getValue(); + get shouldShake(): boolean { + return !this.isFocused_ && !this.isValid() && Boolean(this.getValue()); } - /** - * @return {boolean} - * @private - */ - get shouldAlwaysFloat_() { + private get shouldAlwaysFloat_(): boolean { const type = this.getNativeInput_().type; return ALWAYS_FLOAT_TYPES.indexOf(type) >= 0; } - /** @return {boolean} */ - get shouldFloat() { - return this.shouldAlwaysFloat_ || this.isFocused_ || !!this.getValue() || this.isBadInput_(); + get shouldFloat(): boolean { + return this.shouldAlwaysFloat_ || this.isFocused_ || Boolean(this.getValue()) || this.isBadInput_(); } /** - * {@see MDCTextFieldAdapter} for typing information on parameters and return - * types. - * @return {!MDCTextFieldAdapter} + * See {@link MDCTextFieldAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCTextFieldAdapter} */ ({ - addClass: () => {}, - removeClass: () => {}, - hasClass: () => {}, - registerTextFieldInteractionHandler: () => {}, - deregisterTextFieldInteractionHandler: () => {}, - registerInputInteractionHandler: () => {}, - deregisterInputInteractionHandler: () => {}, - registerValidationAttributeChangeHandler: () => {}, - deregisterValidationAttributeChangeHandler: () => {}, - getNativeInput: () => {}, - isFocused: () => {}, - activateLineRipple: () => {}, - deactivateLineRipple: () => {}, - setLineRippleTransformOrigin: () => {}, - shakeLabel: () => {}, - floatLabel: () => {}, - hasLabel: () => {}, - getLabelWidth: () => {}, - hasOutline: () => {}, - notchOutline: () => {}, - closeOutline: () => {}, - }); - } + static get defaultAdapter(): MDCTextFieldAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: () => undefined, + removeClass: () => undefined, + hasClass: () => true, + registerTextFieldInteractionHandler: () => undefined, + deregisterTextFieldInteractionHandler: () => undefined, + registerInputInteractionHandler: () => undefined, + deregisterInputInteractionHandler: () => undefined, + registerValidationAttributeChangeHandler: () => new MutationObserver(() => undefined), + deregisterValidationAttributeChangeHandler: () => undefined, + getNativeInput: () => null, + isFocused: () => false, + activateLineRipple: () => undefined, + deactivateLineRipple: () => undefined, + setLineRippleTransformOrigin: () => undefined, + shakeLabel: () => undefined, + floatLabel: () => undefined, + hasLabel: () => false, + getLabelWidth: () => 0, + hasOutline: () => false, + notchOutline: () => undefined, + closeOutline: () => undefined, + }; + // tslint:enable:object-literal-sort-keys + } + + private isFocused_ = false; + private receivedUserInput_ = false; + private isValid_ = true; + private useNativeValidation_ = true; + + private readonly inputFocusHandler_: () => void; + private readonly inputBlurHandler_: SpecificEventListener<'blur'>; + private readonly inputInputHandler_: SpecificEventListener<'input'>; + private readonly setPointerXOffset_: SpecificEventListener; + private readonly textFieldInteractionHandler_: SpecificEventListener; + private readonly validationAttributeChangeHandler_: (attributesList: string[]) => void; + private validationObserver_!: MutationObserver; // assigned in init() + + private readonly helperText_?: MDCTextFieldHelperTextFoundation; + private readonly characterCounter_?: MDCTextFieldCharacterCounterFoundation; + private readonly leadingIcon_?: MDCTextFieldIconFoundation; + private readonly trailingIcon_?: MDCTextFieldIconFoundation; /** - * @param {!MDCTextFieldAdapter} adapter - * @param {!FoundationMapType=} foundationMap Map from subcomponent names to their subfoundations. + * @param adapter + * @param foundationMap Map from subcomponent names to their subfoundations. */ - constructor(adapter, foundationMap = /** @type {!FoundationMapType} */ ({})) { - super(Object.assign(MDCTextFieldFoundation.defaultAdapter, adapter)); + constructor(adapter?: Partial, foundationMap: Partial = {}) { + super({...MDCTextFieldFoundation.defaultAdapter, ...adapter}); - /** @type {!MDCTextFieldHelperTextFoundation|undefined} */ this.helperText_ = foundationMap.helperText; - /** @type {!MDCTextFieldCharacterCounterFoundation|undefined} */ this.characterCounter_ = foundationMap.characterCounter; - /** @type {!MDCTextFieldIconFoundation|undefined} */ this.leadingIcon_ = foundationMap.leadingIcon; - /** @type {!MDCTextFieldIconFoundation|undefined} */ this.trailingIcon_ = foundationMap.trailingIcon; - /** @private {boolean} */ - this.isFocused_ = false; - /** @private {boolean} */ - this.receivedUserInput_ = false; - /** @private {boolean} */ - this.useCustomValidityChecking_ = false; - /** @private {boolean} */ - this.isValid_ = true; - - /** @private {boolean} */ - this.useNativeValidation_ = true; - - /** @private {function(): undefined} */ this.inputFocusHandler_ = () => this.activateFocus(); - /** @private {function(): undefined} */ this.inputBlurHandler_ = () => this.deactivateFocus(); - /** @private {function(): undefined} */ this.inputInputHandler_ = () => this.handleInput(); - /** @private {function(!Event): undefined} */ this.setPointerXOffset_ = (evt) => this.setTransformOrigin(evt); - /** @private {function(!Event): undefined} */ this.textFieldInteractionHandler_ = () => this.handleTextFieldInteraction(); - /** @private {function(!Array): undefined} */ this.validationAttributeChangeHandler_ = (attributesList) => this.handleValidationAttributeChange(attributesList); - - /** @private {!MutationObserver} */ - this.validationObserver_; } init() { @@ -156,10 +142,10 @@ class MDCTextFieldFoundation extends MDCFoundation { this.adapter_.registerInputInteractionHandler('focus', this.inputFocusHandler_); this.adapter_.registerInputInteractionHandler('blur', this.inputBlurHandler_); this.adapter_.registerInputInteractionHandler('input', this.inputInputHandler_); - ['mousedown', 'touchstart'].forEach((evtType) => { + POINTERDOWN_EVENTS.forEach((evtType) => { this.adapter_.registerInputInteractionHandler(evtType, this.setPointerXOffset_); }); - ['click', 'keydown'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.registerTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_); }); this.validationObserver_ = @@ -171,10 +157,10 @@ class MDCTextFieldFoundation extends MDCFoundation { this.adapter_.deregisterInputInteractionHandler('focus', this.inputFocusHandler_); this.adapter_.deregisterInputInteractionHandler('blur', this.inputBlurHandler_); this.adapter_.deregisterInputInteractionHandler('input', this.inputInputHandler_); - ['mousedown', 'touchstart'].forEach((evtType) => { + POINTERDOWN_EVENTS.forEach((evtType) => { this.adapter_.deregisterInputInteractionHandler(evtType, this.setPointerXOffset_); }); - ['click', 'keydown'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.deregisterTextFieldInteractionHandler(evtType, this.textFieldInteractionHandler_); }); this.adapter_.deregisterValidationAttributeChangeHandler(this.validationObserver_); @@ -184,7 +170,8 @@ class MDCTextFieldFoundation extends MDCFoundation { * Handles user interactions with the Text Field. */ handleTextFieldInteraction() { - if (this.adapter_.getNativeInput().disabled) { + const nativeInput = this.adapter_.getNativeInput(); + if (nativeInput && nativeInput.disabled) { return; } this.receivedUserInput_ = true; @@ -192,14 +179,14 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Handles validation attribute changes - * @param {!Array} attributesList */ - handleValidationAttributeChange(attributesList) { + handleValidationAttributeChange(attributesList: string[]): void { attributesList.some((attributeName) => { if (VALIDATION_ATTR_WHITELIST.indexOf(attributeName) > -1) { this.styleValidity_(true); return true; } + return false; }); if (attributesList.indexOf('maxlength') > -1) { @@ -209,9 +196,8 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Opens/closes the notched outline. - * @param {boolean} openNotch */ - notchOutline(openNotch) { + notchOutline(openNotch: boolean) { if (!this.adapter_.hasOutline()) { return; } @@ -246,17 +232,12 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Sets the line ripple's transform origin, so that the line ripple activate * animation will animate out from the user's click location. - * @param {!Event} evt */ - setTransformOrigin(evt) { - let targetEvent; - if (evt.touches) { - targetEvent = evt.touches[0]; - } else { - targetEvent = evt; - } - const targetClientRect = targetEvent.target.getBoundingClientRect(); - const normalizedX = targetEvent.clientX - targetClientRect.left; + setTransformOrigin(evt: TouchEvent | MouseEvent): void { + const touches = (evt as TouchEvent).touches; + const targetEvent = touches ? touches[0] : evt; + const targetClientRect = (targetEvent.target as Element).getBoundingClientRect(); + const normalizedX = (targetEvent as MouseEvent).clientX - targetClientRect.left; this.adapter_.setLineRippleTransformOrigin(normalizedX); } @@ -270,7 +251,7 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Activates the Text Field's focus state in cases when the input value - * changes without user input (e.g. programatically). + * changes without user input (e.g. programmatically). */ autoCompleteFocus() { if (!this.receivedUserInput_) { @@ -297,17 +278,14 @@ class MDCTextFieldFoundation extends MDCFoundation { } } - /** - * @return {string} The value of the input Element. - */ - getValue() { + getValue(): string { return this.getNativeInput_().value; } /** - * @param {string} value The value to set on the input Element. + * @param value The value to set on the input Element. */ - setValue(value) { + setValue(value: string): void { // Prevent Safari from moving the caret to the end of the input when the value has not changed. if (this.getValue() !== value) { this.getNativeInput_().value = value; @@ -322,18 +300,17 @@ class MDCTextFieldFoundation extends MDCFoundation { } /** - * @return {boolean} If a custom validity is set, returns that value. - * Otherwise, returns the result of native validity checks. + * @return The custom validity state, if set; otherwise, the result of a native validity check. */ - isValid() { + isValid(): boolean { return this.useNativeValidation_ ? this.isNativeInputValid_() : this.isValid_; } /** - * @param {boolean} isValid Sets the validity state of the Text Field. + * @param isValid Sets the custom validity state of the Text Field. */ - setValid(isValid) { + setValid(isValid: boolean): void { this.isValid_ = isValid; this.styleValidity_(isValid); @@ -345,57 +322,37 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Enables or disables the use of native validation. Use this for custom validation. - * @param {boolean} useNativeValidation Set this to false to ignore native input validation. + * @param useNativeValidation Set this to false to ignore native input validation. */ - setUseNativeValidation(useNativeValidation) { + setUseNativeValidation(useNativeValidation: boolean): void { this.useNativeValidation_ = useNativeValidation; } - /** - * @return {boolean} True if the Text Field is disabled. - */ - isDisabled() { + isDisabled(): boolean { return this.getNativeInput_().disabled; } /** - * @param {boolean} disabled Sets the text-field disabled or enabled. + * @param disabled Sets the text-field disabled or enabled. */ - setDisabled(disabled) { + setDisabled(disabled: boolean): void { this.getNativeInput_().disabled = disabled; this.styleDisabled_(disabled); } /** - * @param {string} content Sets the content of the helper text. + * @param content Sets the content of the helper text. */ - setHelperTextContent(content) { + setHelperTextContent(content: string): void { if (this.helperText_) { this.helperText_.setContent(content); } } - /** - * Sets character counter values that shows characters used and the total character limit. - * @param {number} currentLength - * @private - */ - setCharacterCounter_(currentLength) { - if (!this.characterCounter_) return; - - const maxLength = this.getNativeInput_().maxLength; - if (maxLength === -1) { - throw new Error('MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.'); - } - - this.characterCounter_.setCounterValue(currentLength, maxLength); - } - /** * Sets the aria label of the leading icon. - * @param {string} label */ - setLeadingIconAriaLabel(label) { + setLeadingIconAriaLabel(label: string): void { if (this.leadingIcon_) { this.leadingIcon_.setAriaLabel(label); } @@ -403,9 +360,8 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Sets the text content of the leading icon. - * @param {string} content */ - setLeadingIconContent(content) { + setLeadingIconContent(content: string): void { if (this.leadingIcon_) { this.leadingIcon_.setContent(content); } @@ -413,9 +369,8 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Sets the aria label of the trailing icon. - * @param {string} label */ - setTrailingIconAriaLabel(label) { + setTrailingIconAriaLabel(label: string): void { if (this.trailingIcon_) { this.trailingIcon_.setAriaLabel(label); } @@ -423,37 +378,46 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Sets the text content of the trailing icon. - * @param {string} content */ - setTrailingIconContent(content) { + setTrailingIconContent(content: string): void { if (this.trailingIcon_) { this.trailingIcon_.setContent(content); } } /** - * @return {boolean} True if the Text Field input fails in converting the - * user-supplied value. - * @private + * Sets character counter values that shows characters used and the total character limit. */ - isBadInput_() { - return this.getNativeInput_().validity.badInput; + private setCharacterCounter_(currentLength: number): void { + if (!this.characterCounter_) return; + + const maxLength = this.getNativeInput_().maxLength; + if (maxLength === -1) { + throw new Error('MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.'); + } + + this.characterCounter_.setCounterValue(currentLength, maxLength); } /** - * @return {boolean} The result of native validity checking - * (ValidityState.valid). + * @return True if the Text Field input fails in converting the user-supplied value. */ - isNativeInputValid_() { + private isBadInput_(): boolean { + // The badInput property is not supported in IE 11 💩. + return this.getNativeInput_().validity.badInput || false; + } + + /** + * @return The result of native validity checking (ValidityState.valid). + */ + private isNativeInputValid_(): boolean { return this.getNativeInput_().validity.valid; } /** * Styles the component based on the validity state. - * @param {boolean} isValid - * @private */ - styleValidity_(isValid) { + private styleValidity_(isValid: boolean): void { const {INVALID} = MDCTextFieldFoundation.cssClasses; if (isValid) { this.adapter_.removeClass(INVALID); @@ -467,10 +431,8 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Styles the component based on the focused state. - * @param {boolean} isFocused - * @private */ - styleFocused_(isFocused) { + private styleFocused_(isFocused: boolean): void { const {FOCUSED} = MDCTextFieldFoundation.cssClasses; if (isFocused) { this.adapter_.addClass(FOCUSED); @@ -481,10 +443,8 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * Styles the component based on the disabled state. - * @param {boolean} isDisabled - * @private */ - styleDisabled_(isDisabled) { + private styleDisabled_(isDisabled: boolean): void { const {DISABLED, INVALID} = MDCTextFieldFoundation.cssClasses; if (isDisabled) { this.adapter_.addClass(DISABLED); @@ -503,21 +463,24 @@ class MDCTextFieldFoundation extends MDCFoundation { } /** - * @return {!Element|!NativeInputType} The native text input from the - * host environment, or a dummy if none exists. - * @private + * @return The native text input element from the host environment, or an object with the same shape for unit tests. */ - getNativeInput_() { - return this.adapter_.getNativeInput() || - /** @type {!NativeInputType} */ ({ - value: '', + private getNativeInput_(): NativeInputElement { + // this.adapter_ may be undefined in foundation unit tests. This happens when testdouble is creating a mock object + // and invokes the shouldShake/shouldFloat getters (which in turn call getValue(), which calls this method) before + // init() has been called from the MDCTextField constructor. To work around that issue, we return a dummy object. + const nativeInput = this.adapter_ ? this.adapter_.getNativeInput() : null; + return nativeInput || { disabled: false, + maxLength: -1, + type: 'input', validity: { badInput: false, valid: true, }, - }); + value: '', + }; } } -export default MDCTextFieldFoundation; +export {MDCTextFieldFoundation as default, MDCTextFieldFoundation}; diff --git a/packages/mdc-textfield/helper-text/adapter.js b/packages/mdc-textfield/helper-text/adapter.ts similarity index 67% rename from packages/mdc-textfield/helper-text/adapter.js rename to packages/mdc-textfield/helper-text/adapter.ts index a6cf4768833..bdc90c6c5d5 100644 --- a/packages/mdc-textfield/helper-text/adapter.js +++ b/packages/mdc-textfield/helper-text/adapter.ts @@ -21,56 +21,43 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - /** - * Adapter for MDC Text Field Helper Text. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the TextField helper text into your framework. See - * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md - * for more information. - * - * @record + * Defines the shape of the adapter expected by the foundation. + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -class MDCTextFieldHelperTextAdapter { +interface MDCTextFieldHelperTextAdapter { /** * Adds a class to the helper text element. - * @param {string} className */ - addClass(className) {} + addClass(className: string): void; /** * Removes a class from the helper text element. - * @param {string} className */ - removeClass(className) {} + removeClass(className: string): void; /** * Returns whether or not the helper text element contains the given class. - * @param {string} className - * @return {boolean} */ - hasClass(className) {} + hasClass(className: string): boolean; /** * Sets an attribute with a given value on the helper text element. - * @param {string} attr - * @param {string} value */ - setAttr(attr, value) {} + setAttr(attr: string, value: string): void; /** * Removes an attribute from the helper text element. - * @param {string} attr */ - removeAttr(attr) {} + removeAttr(attr: string): void; /** * Sets the text content for the helper text element. - * @param {string} content */ - setContent(content) {} + setContent(content: string): void; } -export default MDCTextFieldHelperTextAdapter; +export {MDCTextFieldHelperTextAdapter as default, MDCTextFieldHelperTextAdapter}; diff --git a/packages/mdc-textfield/helper-text/constants.js b/packages/mdc-textfield/helper-text/constants.ts similarity index 97% rename from packages/mdc-textfield/helper-text/constants.js rename to packages/mdc-textfield/helper-text/constants.ts index ef091170cf8..8cf4ff39f05 100644 --- a/packages/mdc-textfield/helper-text/constants.js +++ b/packages/mdc-textfield/helper-text/constants.ts @@ -21,14 +21,12 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { - ROOT: 'mdc-text-field-helper-text', HELPER_TEXT_PERSISTENT: 'mdc-text-field-helper-text--persistent', HELPER_TEXT_VALIDATION_MSG: 'mdc-text-field-helper-text--validation-msg', + ROOT: 'mdc-text-field-helper-text', }; -/** @enum {string} */ const strings = { ARIA_HIDDEN: 'aria-hidden', ROLE: 'role', diff --git a/packages/mdc-textfield/helper-text/foundation.js b/packages/mdc-textfield/helper-text/foundation.ts similarity index 66% rename from packages/mdc-textfield/helper-text/foundation.js rename to packages/mdc-textfield/helper-text/foundation.ts index 8f8860232d8..291b173bbd5 100644 --- a/packages/mdc-textfield/helper-text/foundation.js +++ b/packages/mdc-textfield/helper-text/foundation.ts @@ -22,58 +22,49 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCTextFieldHelperTextAdapter from './adapter'; +import {MDCTextFieldHelperTextAdapter} from './adapter'; import {cssClasses, strings} from './constants'; - -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTextFieldHelperTextFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCTextFieldHelperTextFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } /** - * {@see MDCTextFieldHelperTextAdapter} for typing information on parameters and return - * types. - * @return {!MDCTextFieldHelperTextAdapter} + * See {@link MDCTextFieldHelperTextAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCTextFieldHelperTextAdapter} */ ({ - addClass: () => {}, - removeClass: () => {}, - hasClass: () => {}, - setAttr: () => {}, - removeAttr: () => {}, - setContent: () => {}, - }); + static get defaultAdapter(): MDCTextFieldHelperTextAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: () => undefined, + removeClass: () => undefined, + hasClass: () => false, + setAttr: () => undefined, + removeAttr: () => undefined, + setContent: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } - /** - * @param {!MDCTextFieldHelperTextAdapter} adapter - */ - constructor(adapter) { - super(Object.assign(MDCTextFieldHelperTextFoundation.defaultAdapter, adapter)); + constructor(adapter?: Partial) { + super({...MDCTextFieldHelperTextFoundation.defaultAdapter, ...adapter}); } /** * Sets the content of the helper text field. - * @param {string} content */ - setContent(content) { + setContent(content: string) { this.adapter_.setContent(content); } - /** @param {boolean} isPersistent Sets the persistency of the helper text. */ - setPersistent(isPersistent) { + /** + * @param isPersistent Sets the persistency of the helper text. + */ + setPersistent(isPersistent: boolean) { if (isPersistent) { this.adapter_.addClass(cssClasses.HELPER_TEXT_PERSISTENT); } else { @@ -82,10 +73,9 @@ class MDCTextFieldHelperTextFoundation extends MDCFoundation { } /** - * @param {boolean} isValidation True to make the helper text act as an - * error validation message. + * @param isValidation True to make the helper text act as an error validation message. */ - setValidation(isValidation) { + setValidation(isValidation: boolean) { if (isValidation) { this.adapter_.addClass(cssClasses.HELPER_TEXT_VALIDATION_MSG); } else { @@ -93,16 +83,17 @@ class MDCTextFieldHelperTextFoundation extends MDCFoundation { } } - /** Makes the helper text visible to the screen reader. */ + /** + * Makes the helper text visible to the screen reader. + */ showToScreenReader() { this.adapter_.removeAttr(strings.ARIA_HIDDEN); } /** * Sets the validity of the helper text based on the input validity. - * @param {boolean} inputIsValid */ - setValidity(inputIsValid) { + setValidity(inputIsValid: boolean) { const helperTextIsPersistent = this.adapter_.hasClass(cssClasses.HELPER_TEXT_PERSISTENT); const helperTextIsValidationMsg = this.adapter_.hasClass(cssClasses.HELPER_TEXT_VALIDATION_MSG); const validationMsgNeedsDisplay = helperTextIsValidationMsg && !inputIsValid; @@ -120,11 +111,10 @@ class MDCTextFieldHelperTextFoundation extends MDCFoundation { /** * Hides the help text from screen readers. - * @private */ - hide_() { + private hide_() { this.adapter_.setAttr(strings.ARIA_HIDDEN, 'true'); } } -export default MDCTextFieldHelperTextFoundation; +export {MDCTextFieldHelperTextFoundation as default, MDCTextFieldHelperTextFoundation}; diff --git a/packages/mdc-textfield/helper-text/index.js b/packages/mdc-textfield/helper-text/index.ts similarity index 67% rename from packages/mdc-textfield/helper-text/index.js rename to packages/mdc-textfield/helper-text/index.ts index 7d59dfe190c..8bd08413b02 100644 --- a/packages/mdc-textfield/helper-text/index.js +++ b/packages/mdc-textfield/helper-text/index.ts @@ -22,45 +22,31 @@ */ import {MDCComponent} from '@material/base/component'; +import {MDCTextFieldHelperTextFoundation} from './foundation'; -import MDCTextFieldHelperTextAdapter from './adapter'; -import MDCTextFieldHelperTextFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTextFieldHelperText extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCTextFieldHelperText} - */ - static attachTo(root) { +class MDCTextFieldHelperText extends MDCComponent { + static attachTo(root: Element): MDCTextFieldHelperText { return new MDCTextFieldHelperText(root); } - /** - * @return {!MDCTextFieldHelperTextFoundation} - */ - get foundation() { + get foundation(): MDCTextFieldHelperTextFoundation { return this.foundation_; } - /** - * @return {!MDCTextFieldHelperTextFoundation} - */ - getDefaultFoundation() { - return new MDCTextFieldHelperTextFoundation(/** @type {!MDCTextFieldHelperTextAdapter} */ (Object.assign({ + getDefaultFoundation(): MDCTextFieldHelperTextFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCTextFieldHelperTextFoundation({ addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), hasClass: (className) => this.root_.classList.contains(className), setAttr: (attr, value) => this.root_.setAttribute(attr, value), removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { - this.root_.textContent = content; - }, - }))); + setContent: (content) => { this.root_.textContent = content; }, + }); + // tslint:enable:object-literal-sort-keys } } -export {MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation}; +export {MDCTextFieldHelperText as default, MDCTextFieldHelperText}; +export * from './adapter'; +export * from './foundation'; diff --git a/packages/mdc-textfield/icon/adapter.js b/packages/mdc-textfield/icon/adapter.ts similarity index 65% rename from packages/mdc-textfield/icon/adapter.js rename to packages/mdc-textfield/icon/adapter.ts index 82e705a9cf4..0516ed5a70f 100644 --- a/packages/mdc-textfield/icon/adapter.js +++ b/packages/mdc-textfield/icon/adapter.ts @@ -21,63 +21,50 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ +import {EventType, SpecificEventListener} from '@material/base/types'; /** - * Adapter for MDC Text Field Icon. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the text field icon into your framework. See - * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md - * for more information. - * - * @record + * Defines the shape of the adapter expected by the foundation. + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -class MDCTextFieldIconAdapter { +interface MDCTextFieldIconAdapter { /** * Gets the value of an attribute on the icon element. - * @param {string} attr - * @return {string} */ - getAttr(attr) {} + getAttr(attr: string): string | null; /** * Sets an attribute on the icon element. - * @param {string} attr - * @param {string} value */ - setAttr(attr, value) {} + setAttr(attr: string, value: string): void; /** * Removes an attribute from the icon element. - * @param {string} attr */ - removeAttr(attr) {} + removeAttr(attr: string): void; /** * Sets the text content of the icon element. - * @param {string} content */ - setContent(content) {} + setContent(content: string): void; /** * Registers an event listener on the icon element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler */ - registerInteractionHandler(evtType, handler) {} + registerInteractionHandler(evtType: E, handler: SpecificEventListener): void; /** * Deregisters an event listener on the icon element for a given event. - * @param {string} evtType - * @param {function(!Event): undefined} handler */ - deregisterInteractionHandler(evtType, handler) {} + deregisterInteractionHandler(evtType: E, handler: SpecificEventListener): void; /** * Emits a custom event "MDCTextField:icon" denoting a user has clicked the icon. */ - notifyIconAction() {} + notifyIconAction(): void; } -export default MDCTextFieldIconAdapter; +export {MDCTextFieldIconAdapter as default, MDCTextFieldIconAdapter}; diff --git a/packages/mdc-textfield/icon/constants.js b/packages/mdc-textfield/icon/constants.ts similarity index 98% rename from packages/mdc-textfield/icon/constants.js rename to packages/mdc-textfield/icon/constants.ts index e29697c6193..81712f35b57 100644 --- a/packages/mdc-textfield/icon/constants.js +++ b/packages/mdc-textfield/icon/constants.ts @@ -21,7 +21,6 @@ * THE SOFTWARE. */ -/** @enum {string} */ const strings = { ICON_EVENT: 'MDCTextField:icon', ICON_ROLE: 'button', diff --git a/packages/mdc-textfield/icon/foundation.js b/packages/mdc-textfield/icon/foundation.ts similarity index 56% rename from packages/mdc-textfield/icon/foundation.js rename to packages/mdc-textfield/icon/foundation.ts index 5dd28c66aee..7b0dd71edc0 100644 --- a/packages/mdc-textfield/icon/foundation.js +++ b/packages/mdc-textfield/icon/foundation.ts @@ -22,66 +22,60 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCTextFieldIconAdapter from './adapter'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCTextFieldIconAdapter} from './adapter'; import {strings} from './constants'; +type InteractionEventType = 'click' | 'keydown'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTextFieldIconFoundation extends MDCFoundation { - /** @return enum {string} */ +const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; + +class MDCTextFieldIconFoundation extends MDCFoundation { static get strings() { return strings; } /** - * {@see MDCTextFieldIconAdapter} for typing information on parameters and return - * types. - * @return {!MDCTextFieldIconAdapter} + * See {@link MDCTextFieldIconAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCTextFieldIconAdapter} */ ({ - getAttr: () => {}, - setAttr: () => {}, - removeAttr: () => {}, - setContent: () => {}, - registerInteractionHandler: () => {}, - deregisterInteractionHandler: () => {}, - notifyIconAction: () => {}, - }); + static get defaultAdapter(): MDCTextFieldIconAdapter { + // tslint:disable:object-literal-sort-keys + return { + getAttr: () => null, + setAttr: () => undefined, + removeAttr: () => undefined, + setContent: () => undefined, + registerInteractionHandler: () => undefined, + deregisterInteractionHandler: () => undefined, + notifyIconAction: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } - /** - * @param {!MDCTextFieldIconAdapter} adapter - */ - constructor(adapter) { - super(Object.assign(MDCTextFieldIconFoundation.defaultAdapter, adapter)); + private savedTabIndex_: string | null = null; + private readonly interactionHandler_: SpecificEventListener; - /** @private {string?} */ - this.savedTabIndex_ = null; + constructor(adapter?: Partial) { + super({...MDCTextFieldIconFoundation.defaultAdapter, ...adapter}); - /** @private {function(!Event): undefined} */ this.interactionHandler_ = (evt) => this.handleInteraction(evt); } init() { this.savedTabIndex_ = this.adapter_.getAttr('tabindex'); - ['click', 'keydown'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.registerInteractionHandler(evtType, this.interactionHandler_); }); } destroy() { - ['click', 'keydown'].forEach((evtType) => { + INTERACTION_EVENTS.forEach((evtType) => { this.adapter_.deregisterInteractionHandler(evtType, this.interactionHandler_); }); } - /** @param {boolean} disabled */ - setDisabled(disabled) { + setDisabled(disabled: boolean) { if (!this.savedTabIndex_) { return; } @@ -95,25 +89,20 @@ class MDCTextFieldIconFoundation extends MDCFoundation { } } - /** @param {string} label */ - setAriaLabel(label) { + setAriaLabel(label: string) { this.adapter_.setAttr('aria-label', label); } - /** @param {string} content */ - setContent(content) { + setContent(content: string) { this.adapter_.setContent(content); } - /** - * Handles an interaction event - * @param {!Event} evt - */ - handleInteraction(evt) { - if (evt.type === 'click' || evt.key === 'Enter' || evt.keyCode === 13) { + handleInteraction(evt: MouseEvent | KeyboardEvent) { + const isEnterKey = (evt as KeyboardEvent).key === 'Enter' || (evt as KeyboardEvent).keyCode === 13; + if (evt.type === 'click' || isEnterKey) { this.adapter_.notifyIconAction(); } } } -export default MDCTextFieldIconFoundation; +export {MDCTextFieldIconFoundation as default, MDCTextFieldIconFoundation}; diff --git a/packages/mdc-textfield/icon/index.js b/packages/mdc-textfield/icon/index.ts similarity index 60% rename from packages/mdc-textfield/icon/index.js rename to packages/mdc-textfield/icon/index.ts index 08c0d9c94bf..2d19a8d5e1f 100644 --- a/packages/mdc-textfield/icon/index.js +++ b/packages/mdc-textfield/icon/index.ts @@ -22,47 +22,38 @@ */ import {MDCComponent} from '@material/base/component'; +import {EventType, SpecificEventListener} from '@material/base/types'; +import {MDCTextFieldIconFoundation} from './foundation'; -import MDCTextFieldIconAdapter from './adapter'; -import MDCTextFieldIconFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTextFieldIcon extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCTextFieldIcon} - */ - static attachTo(root) { +class MDCTextFieldIcon extends MDCComponent { + static attachTo(root: Element): MDCTextFieldIcon { return new MDCTextFieldIcon(root); } - /** - * @return {!MDCTextFieldIconFoundation} - */ - get foundation() { + get foundation(): MDCTextFieldIconFoundation { return this.foundation_; } - /** - * @return {!MDCTextFieldIconFoundation} - */ - getDefaultFoundation() { - return new MDCTextFieldIconFoundation(/** @type {!MDCTextFieldIconAdapter} */ (Object.assign({ + getDefaultFoundation(): MDCTextFieldIconFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCTextFieldIconFoundation({ getAttr: (attr) => this.root_.getAttribute(attr), setAttr: (attr, value) => this.root_.setAttribute(attr, value), removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { - this.root_.textContent = content; + setContent: (content) => { this.root_.textContent = content; }, + registerInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.root_.addEventListener(evtType, handler); + }, + deregisterInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.root_.removeEventListener(evtType, handler); }, - registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), notifyIconAction: () => this.emit( MDCTextFieldIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), - }))); + }); + // tslint:enable:object-literal-sort-keys } } -export {MDCTextFieldIcon, MDCTextFieldIconFoundation}; +export {MDCTextFieldIcon as default, MDCTextFieldIcon}; +export * from './adapter'; +export * from './foundation'; diff --git a/packages/mdc-textfield/index.js b/packages/mdc-textfield/index.js deleted file mode 100644 index a47cf2d16c2..00000000000 --- a/packages/mdc-textfield/index.js +++ /dev/null @@ -1,533 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -import {MDCComponent} from '@material/base/component'; -/* eslint-disable no-unused-vars */ -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -/* eslint-enable no-unused-vars */ -import {getMatchesProperty} from '@material/ripple/util'; - - -import {cssClasses, strings} from './constants'; -import {MDCTextFieldAdapter, FoundationMapType} from './adapter'; -import MDCTextFieldFoundation from './foundation'; -/* eslint-disable no-unused-vars */ -import {MDCLineRipple, MDCLineRippleFoundation} from '@material/line-ripple/index'; -import {MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation} from './helper-text/index'; -import {MDCTextFieldCharacterCounter, MDCTextFieldCharacterCounterFoundation} from './character-counter/index'; -import {MDCTextFieldIcon, MDCTextFieldIconFoundation} from './icon/index'; -import {MDCFloatingLabel, MDCFloatingLabelFoundation} from '@material/floating-label/index'; -import {MDCNotchedOutline, MDCNotchedOutlineFoundation} from '@material/notched-outline/index'; -/* eslint-enable no-unused-vars */ - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTextField extends MDCComponent { - /** - * @param {...?} args - */ - constructor(...args) { - super(...args); - /** @private {?Element} */ - this.input_; - /** @type {?MDCRipple} */ - this.ripple; - /** @private {?MDCLineRipple} */ - this.lineRipple_; - /** @private {?MDCTextFieldHelperText} */ - this.helperText_; - /** @private {?MDCTextFieldCharacterCounter} */ - this.characterCounter_; - /** @private {?MDCTextFieldIcon} */ - this.leadingIcon_; - /** @private {?MDCTextFieldIcon} */ - this.trailingIcon_; - /** @private {?MDCFloatingLabel} */ - this.label_; - /** @private {?MDCNotchedOutline} */ - this.outline_; - } - - /** - * @param {!Element} root - * @return {!MDCTextField} - */ - static attachTo(root) { - return new MDCTextField(root); - } - - /** - * @param {(function(!Element, MDCRippleFoundation): !MDCRipple)=} rippleFactory A function which - * creates a new MDCRipple. - * @param {(function(!Element): !MDCLineRipple)=} lineRippleFactory A function which - * creates a new MDCLineRipple. - * @param {(function(!Element): !MDCTextFieldHelperText)=} helperTextFactory A function which - * creates a new MDCTextFieldHelperText. - * @param {(function(!Element): !MDCTextFieldCharacterCounter)=} characterCounterFactory A function which - * creates a new MDCTextFieldCharacterCounter. - * @param {(function(!Element): !MDCTextFieldIcon)=} iconFactory A function which - * creates a new MDCTextFieldIcon. - * @param {(function(!Element): !MDCFloatingLabel)=} labelFactory A function which - * creates a new MDCFloatingLabel. - * @param {(function(!Element): !MDCNotchedOutline)=} outlineFactory A function which - * creates a new MDCNotchedOutline. - */ - initialize( - rippleFactory = (el, foundation) => new MDCRipple(el, foundation), - lineRippleFactory = (el) => new MDCLineRipple(el), - helperTextFactory = (el) => new MDCTextFieldHelperText(el), - characterCounterFactory = (el) => new MDCTextFieldCharacterCounter(el), - iconFactory = (el) => new MDCTextFieldIcon(el), - labelFactory = (el) => new MDCFloatingLabel(el), - outlineFactory = (el) => new MDCNotchedOutline(el)) { - this.input_ = this.root_.querySelector(strings.INPUT_SELECTOR); - const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); - if (labelElement) { - this.label_ = labelFactory(labelElement); - } - const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); - if (lineRippleElement) { - this.lineRipple_ = lineRippleFactory(lineRippleElement); - } - const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); - if (outlineElement) { - this.outline_ = outlineFactory(outlineElement); - } - - // Helper text - const helperTextStrings = MDCTextFieldHelperTextFoundation.strings; - const nextElementSibling = this.root_.nextElementSibling; - const hasHelperLine = (nextElementSibling && nextElementSibling.classList.contains(cssClasses.HELPER_LINE)); - const helperTextEl = hasHelperLine && nextElementSibling.querySelector(helperTextStrings.ROOT_SELECTOR); - if (helperTextEl) { - this.helperText_ = helperTextFactory(helperTextEl); - } - - // Character counter - const characterCounterStrings = MDCTextFieldCharacterCounterFoundation.strings; - let characterCounterEl = this.root_.querySelector(characterCounterStrings.ROOT_SELECTOR); - // If character counter is not found in root element search in sibling element. - if (!characterCounterEl && hasHelperLine) { - characterCounterEl = nextElementSibling.querySelector(characterCounterStrings.ROOT_SELECTOR); - } - - if (characterCounterEl) { - this.characterCounter_ = characterCounterFactory(characterCounterEl); - } - - const iconElements = this.root_.querySelectorAll(strings.ICON_SELECTOR); - if (iconElements.length > 0) { - if (iconElements.length > 1) { // Has both icons. - this.leadingIcon_ = iconFactory(iconElements[0]); - this.trailingIcon_ = iconFactory(iconElements[1]); - } else { - if (this.root_.classList.contains(cssClasses.WITH_LEADING_ICON)) { - this.leadingIcon_ = iconFactory(iconElements[0]); - } else { - this.trailingIcon_ = iconFactory(iconElements[0]); - } - } - } - - this.ripple = null; - if (!this.root_.classList.contains(cssClasses.TEXTAREA) && !this.root_.classList.contains(cssClasses.OUTLINED)) { - const MATCHES = getMatchesProperty(HTMLElement.prototype); - const adapter = - Object.assign(MDCRipple.createAdapter(/** @type {!RippleCapableSurface} */ (this)), { - isSurfaceActive: () => this.input_[MATCHES](':active'), - registerInteractionHandler: (type, handler) => this.input_.addEventListener(type, handler), - deregisterInteractionHandler: (type, handler) => this.input_.removeEventListener(type, handler), - }); - const foundation = new MDCRippleFoundation(adapter); - this.ripple = rippleFactory(this.root_, foundation); - } - } - - destroy() { - if (this.ripple) { - this.ripple.destroy(); - } - if (this.lineRipple_) { - this.lineRipple_.destroy(); - } - if (this.helperText_) { - this.helperText_.destroy(); - } - if (this.characterCounter_) { - this.characterCounter_.destroy(); - } - if (this.leadingIcon_) { - this.leadingIcon_.destroy(); - } - if (this.trailingIcon_) { - this.trailingIcon_.destroy(); - } - if (this.label_) { - this.label_.destroy(); - } - if (this.outline_) { - this.outline_.destroy(); - } - super.destroy(); - } - - /** - * Initiliazes the Text Field's internal state based on the environment's - * state. - */ - initialSyncWithDom() { - this.disabled = this.input_.disabled; - } - - /** - * @return {string} The value of the input. - */ - get value() { - return this.foundation_.getValue(); - } - - /** - * @param {string} value The value to set on the input. - */ - set value(value) { - this.foundation_.setValue(value); - } - - /** - * @return {boolean} True if the Text Field is disabled. - */ - get disabled() { - return this.foundation_.isDisabled(); - } - - /** - * @param {boolean} disabled Sets the Text Field disabled or enabled. - */ - set disabled(disabled) { - this.foundation_.setDisabled(disabled); - } - - /** - * @return {boolean} valid True if the Text Field is valid. - */ - get valid() { - return this.foundation_.isValid(); - } - - /** - * @param {boolean} valid Sets the Text Field valid or invalid. - */ - set valid(valid) { - this.foundation_.setValid(valid); - } - - /** - * @return {boolean} True if the Text Field is required. - */ - get required() { - return this.input_.required; - } - - /** - * @param {boolean} required Sets the Text Field to required. - */ - set required(required) { - this.input_.required = required; - } - - /** - * @return {string} The input element's validation pattern. - */ - get pattern() { - return this.input_.pattern; - } - - /** - * @param {string} pattern Sets the input element's validation pattern. - */ - set pattern(pattern) { - this.input_.pattern = pattern; - } - - /** - * @return {number} The input element's minLength. - */ - get minLength() { - return this.input_.minLength; - } - - /** - * @param {number} minLength Sets the input element's minLength. - */ - set minLength(minLength) { - this.input_.minLength = minLength; - } - - /** - * @return {number} The input element's maxLength. - */ - get maxLength() { - return this.input_.maxLength; - } - - /** - * @param {number} maxLength Sets the input element's maxLength. - */ - set maxLength(maxLength) { - // Chrome throws exception if maxLength is set < 0 - if (maxLength < 0) { - this.input_.removeAttribute('maxLength'); - } else { - this.input_.maxLength = maxLength; - } - } - - /** - * @return {string} The input element's min. - */ - get min() { - return this.input_.min; - } - - /** - * @param {string} min Sets the input element's min. - */ - set min(min) { - this.input_.min = min; - } - - /** - * @return {string} The input element's max. - */ - get max() { - return this.input_.max; - } - - /** - * @param {string} max Sets the input element's max. - */ - set max(max) { - this.input_.max = max; - } - - /** - * @return {string} The input element's step. - */ - get step() { - return this.input_.step; - } - - /** - * @param {string} step Sets the input element's step. - */ - set step(step) { - this.input_.step = step; - } - - /** - * Sets the helper text element content. - * @param {string} content - */ - set helperTextContent(content) { - this.foundation_.setHelperTextContent(content); - } - - /** - * Sets the aria label of the leading icon. - * @param {string} label - */ - set leadingIconAriaLabel(label) { - this.foundation_.setLeadingIconAriaLabel(label); - } - - /** - * Sets the text content of the leading icon. - * @param {string} content - */ - set leadingIconContent(content) { - this.foundation_.setLeadingIconContent(content); - } - - /** - * Sets the aria label of the trailing icon. - * @param {string} label - */ - set trailingIconAriaLabel(label) { - this.foundation_.setTrailingIconAriaLabel(label); - } - - /** - * Sets the text content of the trailing icon. - * @param {string} content - */ - set trailingIconContent(content) { - this.foundation_.setTrailingIconContent(content); - } - - /** - * Enables or disables the use of native validation. Use this for custom validation. - * @param {boolean} useNativeValidation Set this to false to ignore native input validation. - */ - set useNativeValidation(useNativeValidation) { - this.foundation_.setUseNativeValidation(useNativeValidation); - } - - /** - * Focuses the input element. - */ - focus() { - this.input_.focus(); - } - - /** - * Recomputes the outline SVG path for the outline element. - */ - layout() { - const openNotch = this.foundation_.shouldFloat; - this.foundation_.notchOutline(openNotch); - } - - /** - * @return {!MDCTextFieldFoundation} - */ - getDefaultFoundation() { - return new MDCTextFieldFoundation( - /** @type {!MDCTextFieldAdapter} */ (Object.assign({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - registerTextFieldInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterTextFieldInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - registerValidationAttributeChangeHandler: (handler) => { - const getAttributesList = (mutationsList) => mutationsList.map((mutation) => mutation.attributeName); - const observer = new MutationObserver((mutationsList) => handler(getAttributesList(mutationsList))); - const targetNode = this.root_.querySelector(strings.INPUT_SELECTOR); - const config = {attributes: true}; - observer.observe(targetNode, config); - return observer; - }, - deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect(), - isFocused: () => { - return document.activeElement === this.root_.querySelector(strings.INPUT_SELECTOR); - }, - }, - this.getInputAdapterMethods_(), - this.getLabelAdapterMethods_(), - this.getLineRippleAdapterMethods_(), - this.getOutlineAdapterMethods_())), - this.getFoundationMap_()); - } - - /** - * @return {!{ - * shakeLabel: function(boolean): undefined, - * floatLabel: function(boolean): undefined, - * hasLabel: function(): boolean, - * getLabelWidth: function(): number, - * }} - */ - getLabelAdapterMethods_() { - return { - shakeLabel: (shouldShake) => this.label_.shake(shouldShake), - floatLabel: (shouldFloat) => this.label_.float(shouldFloat), - hasLabel: () => !!this.label_, - getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, - }; - } - - /** - * @return {!{ - * activateLineRipple: function(): undefined, - * deactivateLineRipple: function(): undefined, - * setLineRippleTransformOrigin: function(number): undefined, - * }} - */ - getLineRippleAdapterMethods_() { - return { - activateLineRipple: () => { - if (this.lineRipple_) { - this.lineRipple_.activate(); - } - }, - deactivateLineRipple: () => { - if (this.lineRipple_) { - this.lineRipple_.deactivate(); - } - }, - setLineRippleTransformOrigin: (normalizedX) => { - if (this.lineRipple_) { - this.lineRipple_.setRippleCenter(normalizedX); - } - }, - }; - } - - /** - * @return {!{ - * notchOutline: function(number, boolean): undefined, - * hasOutline: function(): boolean, - * }} - */ - getOutlineAdapterMethods_() { - return { - notchOutline: (labelWidth) => this.outline_.notch(labelWidth), - closeOutline: () => this.outline_.closeNotch(), - hasOutline: () => !!this.outline_, - }; - } - - /** - * @return {!{ - * registerInputInteractionHandler: function(string, function()): undefined, - * deregisterInputInteractionHandler: function(string, function()): undefined, - * getNativeInput: function(): ?Element, - * }} - */ - getInputAdapterMethods_() { - return { - registerInputInteractionHandler: (evtType, handler) => this.input_.addEventListener(evtType, handler), - deregisterInputInteractionHandler: (evtType, handler) => this.input_.removeEventListener(evtType, handler), - getNativeInput: () => this.input_, - }; - } - - /** - * Returns a map of all subcomponents to subfoundations. - * @return {!FoundationMapType} - */ - getFoundationMap_() { - return { - helperText: this.helperText_ ? this.helperText_.foundation : undefined, - characterCounter: this.characterCounter_ ? this.characterCounter_.foundation : undefined, - leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, - trailingIcon: this.trailingIcon_ ? this.trailingIcon_.foundation : undefined, - }; - } -} - -export {MDCTextField, MDCTextFieldFoundation, - MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation, - MDCTextFieldCharacterCounter, MDCTextFieldCharacterCounterFoundation, - MDCTextFieldIcon, MDCTextFieldIconFoundation}; diff --git a/packages/mdc-textfield/index.ts b/packages/mdc-textfield/index.ts new file mode 100644 index 00000000000..78870c12eb1 --- /dev/null +++ b/packages/mdc-textfield/index.ts @@ -0,0 +1,447 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {EventType, SpecificEventListener} from '@material/base/types'; +import * as ponyfill from '@material/dom/ponyfill'; +import {MDCFloatingLabel} from '@material/floating-label/index'; +import {MDCLineRipple} from '@material/line-ripple/index'; +import {MDCNotchedOutline} from '@material/notched-outline/index'; +import {MDCRippleFoundation} from '@material/ripple/foundation'; +import {MDCRipple} from '@material/ripple/index'; +import {RippleCapableSurface} from '@material/ripple/types'; +import {MDCTextFieldCharacterCounter, MDCTextFieldCharacterCounterFoundation} from './character-counter'; +import {cssClasses, strings} from './constants'; +import {MDCTextFieldFoundation} from './foundation'; +import {MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation} from './helper-text'; +import {MDCTextFieldIcon} from './icon'; +import { + CharacterCounterFactory, + FoundationMapType, + HelperTextFactory, + IconFactory, + LabelFactory, + LineRippleFactory, + OutlineFactory, + RippleFactory, +} from './types'; + +class MDCTextField extends MDCComponent implements RippleCapableSurface { + static attachTo(root: Element): MDCTextField { + return new MDCTextField(root); + } + + // Public visibility for these properties is required by RippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + ripple!: MDCRipple | null; // assigned in initialize() + + // The only required sub-element. + private input_!: HTMLInputElement; // assigned in initialize() + + // Optional sub-elements. + private characterCounter_!: MDCTextFieldCharacterCounter | null; // assigned in initialize() + private helperText_!: MDCTextFieldHelperText | null; // assigned in initialize() + private label_!: MDCFloatingLabel | null; // assigned in initialize() + private leadingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() + private lineRipple_!: MDCLineRipple | null; // assigned in initialize() + private outline_!: MDCNotchedOutline | null; // assigned in initialize() + private trailingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() + + initialize( + rippleFactory: RippleFactory = (el, foundation) => new MDCRipple(el, foundation), + lineRippleFactory: LineRippleFactory = (el) => new MDCLineRipple(el), + helperTextFactory: HelperTextFactory = (el) => new MDCTextFieldHelperText(el), + characterCounterFactory: CharacterCounterFactory = (el) => new MDCTextFieldCharacterCounter(el), + iconFactory: IconFactory = (el) => new MDCTextFieldIcon(el), + labelFactory: LabelFactory = (el) => new MDCFloatingLabel(el), + outlineFactory: OutlineFactory = (el) => new MDCNotchedOutline(el), + ) { + this.input_ = this.root_.querySelector(strings.INPUT_SELECTOR)!; + + const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); + this.label_ = labelElement ? labelFactory(labelElement) : null; + + const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); + this.lineRipple_ = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; + + const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); + this.outline_ = outlineElement ? outlineFactory(outlineElement) : null; + + // Helper text + const helperTextStrings = MDCTextFieldHelperTextFoundation.strings; + const nextElementSibling = this.root_.nextElementSibling; + const hasHelperLine = (nextElementSibling && nextElementSibling.classList.contains(cssClasses.HELPER_LINE)); + const helperTextEl = + hasHelperLine && nextElementSibling && nextElementSibling.querySelector(helperTextStrings.ROOT_SELECTOR); + this.helperText_ = helperTextEl ? helperTextFactory(helperTextEl) : null; + + // Character counter + const characterCounterStrings = MDCTextFieldCharacterCounterFoundation.strings; + let characterCounterEl = this.root_.querySelector(characterCounterStrings.ROOT_SELECTOR); + // If character counter is not found in root element search in sibling element. + if (!characterCounterEl && hasHelperLine && nextElementSibling) { + characterCounterEl = nextElementSibling.querySelector(characterCounterStrings.ROOT_SELECTOR); + } + this.characterCounter_ = characterCounterEl ? characterCounterFactory(characterCounterEl) : null; + + this.leadingIcon_ = null; + this.trailingIcon_ = null; + const iconElements = this.root_.querySelectorAll(strings.ICON_SELECTOR); + if (iconElements.length > 0) { + if (iconElements.length > 1) { // Has both icons. + this.leadingIcon_ = iconFactory(iconElements[0]); + this.trailingIcon_ = iconFactory(iconElements[1]); + } else { + if (this.root_.classList.contains(cssClasses.WITH_LEADING_ICON)) { + this.leadingIcon_ = iconFactory(iconElements[0]); + } else { + this.trailingIcon_ = iconFactory(iconElements[0]); + } + } + } + + const isTextArea = this.root_.classList.contains(cssClasses.TEXTAREA); + const isOutlined = this.root_.classList.contains(cssClasses.OUTLINED); + this.ripple = (isTextArea || isOutlined) ? null : rippleFactory(this.root_, new MDCRippleFoundation({ + ...MDCRipple.createAdapter(this), + ...({ + // tslint:disable:object-literal-sort-keys + isSurfaceActive: () => ponyfill.matches(this.input_, ':active'), + registerInteractionHandler: (evtType, handler) => this.input_.addEventListener(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.input_.removeEventListener(evtType, handler), + // tslint:enable:object-literal-sort-keys + }), + })); + } + + destroy() { + if (this.ripple) { + this.ripple.destroy(); + } + if (this.lineRipple_) { + this.lineRipple_.destroy(); + } + if (this.helperText_) { + this.helperText_.destroy(); + } + if (this.characterCounter_) { + this.characterCounter_.destroy(); + } + if (this.leadingIcon_) { + this.leadingIcon_.destroy(); + } + if (this.trailingIcon_) { + this.trailingIcon_.destroy(); + } + if (this.label_) { + this.label_.destroy(); + } + if (this.outline_) { + this.outline_.destroy(); + } + super.destroy(); + } + + /** + * Initializes the Text Field's internal state based on the environment's + * state. + */ + initialSyncWithDom() { + this.disabled = this.input_.disabled; + } + + get value(): string { + return this.foundation_.getValue(); + } + + /** + * @param value The value to set on the input. + */ + set value(value: string) { + this.foundation_.setValue(value); + } + + get disabled(): boolean { + return this.foundation_.isDisabled(); + } + + /** + * @param disabled Sets the Text Field disabled or enabled. + */ + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + get valid(): boolean { + return this.foundation_.isValid(); + } + + /** + * @param valid Sets the Text Field valid or invalid. + */ + set valid(valid: boolean) { + this.foundation_.setValid(valid); + } + + get required(): boolean { + return this.input_.required; + } + + /** + * @param required Sets the Text Field to required. + */ + set required(required: boolean) { + this.input_.required = required; + } + + get pattern(): string { + return this.input_.pattern; + } + + /** + * @param pattern Sets the input element's validation pattern. + */ + set pattern(pattern: string) { + this.input_.pattern = pattern; + } + + get minLength(): number { + return this.input_.minLength; + } + + /** + * @param minLength Sets the input element's minLength. + */ + set minLength(minLength: number) { + this.input_.minLength = minLength; + } + + get maxLength(): number { + return this.input_.maxLength; + } + + /** + * @param maxLength Sets the input element's maxLength. + */ + set maxLength(maxLength: number) { + // Chrome throws exception if maxLength is set to a value less than zero + if (maxLength < 0) { + this.input_.removeAttribute('maxLength'); + } else { + this.input_.maxLength = maxLength; + } + } + + get min(): string { + return this.input_.min; + } + + /** + * @param min Sets the input element's min. + */ + set min(min: string) { + this.input_.min = min; + } + + get max(): string { + return this.input_.max; + } + + /** + * @param max Sets the input element's max. + */ + set max(max: string) { + this.input_.max = max; + } + + get step(): string { + return this.input_.step; + } + + /** + * @param step Sets the input element's step. + */ + set step(step: string) { + this.input_.step = step; + } + + /** + * Sets the helper text element content. + */ + set helperTextContent(content: string) { + this.foundation_.setHelperTextContent(content); + } + + /** + * Sets the aria label of the leading icon. + */ + set leadingIconAriaLabel(label: string) { + this.foundation_.setLeadingIconAriaLabel(label); + } + + /** + * Sets the text content of the leading icon. + */ + set leadingIconContent(content: string) { + this.foundation_.setLeadingIconContent(content); + } + + /** + * Sets the aria label of the trailing icon. + */ + set trailingIconAriaLabel(label: string) { + this.foundation_.setTrailingIconAriaLabel(label); + } + + /** + * Sets the text content of the trailing icon. + */ + set trailingIconContent(content: string) { + this.foundation_.setTrailingIconContent(content); + } + + /** + * Enables or disables the use of native validation. Use this for custom validation. + * @param useNativeValidation Set this to false to ignore native input validation. + */ + set useNativeValidation(useNativeValidation: boolean) { + this.foundation_.setUseNativeValidation(useNativeValidation); + } + + /** + * Focuses the input element. + */ + focus() { + this.input_.focus(); + } + + /** + * Recomputes the outline SVG path for the outline element. + */ + layout() { + const openNotch = this.foundation_.shouldFloat; + this.foundation_.notchOutline(openNotch); + } + + getDefaultFoundation(): MDCTextFieldFoundation { + return new MDCTextFieldFoundation({ + ...({ + // tslint:disable:object-literal-sort-keys + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + registerTextFieldInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), + deregisterTextFieldInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), + registerValidationAttributeChangeHandler: (handler) => { + const getAttributesList = (mutationsList: MutationRecord[]): string[] => { + return mutationsList + .map((mutation) => mutation.attributeName) + .filter((attributeName) => attributeName) as string[]; + }; + const observer = new MutationObserver((mutationsList) => handler(getAttributesList(mutationsList))); + const config = {attributes: true}; + observer.observe(this.input_, config); + return observer; + }, + deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect(), + isFocused: () => document.activeElement === this.input_, + // tslint:enable:object-literal-sort-keys + }), + ...this.getInputAdapterMethods_(), + ...this.getLabelAdapterMethods_(), + ...this.getLineRippleAdapterMethods_(), + ...this.getOutlineAdapterMethods_(), + }, this.getFoundationMap_()); + } + + private getLabelAdapterMethods_() { + return { + floatLabel: (shouldFloat: boolean) => this.label_ && this.label_.float(shouldFloat), + getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, + hasLabel: () => Boolean(this.label_), + shakeLabel: (shouldShake: boolean) => this.label_ && this.label_.shake(shouldShake), + }; + } + + private getLineRippleAdapterMethods_() { + return { + activateLineRipple: () => { + if (this.lineRipple_) { + this.lineRipple_.activate(); + } + }, + deactivateLineRipple: () => { + if (this.lineRipple_) { + this.lineRipple_.deactivate(); + } + }, + setLineRippleTransformOrigin: (normalizedX: number) => { + if (this.lineRipple_) { + this.lineRipple_.setRippleCenter(normalizedX); + } + }, + }; + } + + private getOutlineAdapterMethods_() { + return { + closeOutline: () => this.outline_ && this.outline_.closeNotch(), + hasOutline: () => Boolean(this.outline_), + notchOutline: (labelWidth: number) => this.outline_ && this.outline_.notch(labelWidth), + }; + } + + private getInputAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + registerInputInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.input_.addEventListener(evtType, handler); + }, + deregisterInputInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.input_.removeEventListener(evtType, handler); + }, + getNativeInput: () => this.input_, + }; + // tslint:enable:object-literal-sort-keys + } + + /** + * @return A map of all subcomponents to subfoundations. + */ + private getFoundationMap_(): Partial { + return { + characterCounter: this.characterCounter_ ? this.characterCounter_.foundation : undefined, + helperText: this.helperText_ ? this.helperText_.foundation : undefined, + leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, + trailingIcon: this.trailingIcon_ ? this.trailingIcon_.foundation : undefined, + }; + } +} + +export {MDCTextField as default, MDCTextField}; +export * from './adapter'; +export * from './foundation'; +export * from './types'; +export * from './character-counter/index'; +export * from './helper-text/index'; +export * from './icon/index'; diff --git a/packages/mdc-textfield/package.json b/packages/mdc-textfield/package.json index aab6c6027cf..4b5aa7e97ff 100644 --- a/packages/mdc-textfield/package.json +++ b/packages/mdc-textfield/package.json @@ -17,6 +17,7 @@ "dependencies": { "@material/animation": "^0.41.0", "@material/base": "^0.41.0", + "@material/dom": "^0.41.0", "@material/floating-label": "^0.44.0", "@material/line-ripple": "^0.43.0", "@material/notched-outline": "^0.44.0", diff --git a/packages/mdc-textfield/types.ts b/packages/mdc-textfield/types.ts new file mode 100644 index 00000000000..7ac8a921d06 --- /dev/null +++ b/packages/mdc-textfield/types.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCFloatingLabel} from '@material/floating-label/index'; +import {MDCLineRipple} from '@material/line-ripple/index'; +import {MDCNotchedOutline} from '@material/notched-outline/index'; +import {MDCRippleFoundation} from '@material/ripple/foundation'; +import {MDCRipple} from '@material/ripple/index'; +import {MDCTextFieldCharacterCounterFoundation} from './character-counter/foundation'; +import {MDCTextFieldCharacterCounter} from './character-counter/index'; +import {MDCTextFieldHelperTextFoundation} from './helper-text/foundation'; +import {MDCTextFieldHelperText} from './helper-text/index'; +import {MDCTextFieldIconFoundation} from './icon/foundation'; +import {MDCTextFieldIcon} from './icon/index'; + +export type NativeInputElement = Pick & { + validity: Pick; +}; + +export interface FoundationMapType { + helperText: MDCTextFieldHelperTextFoundation; + characterCounter: MDCTextFieldCharacterCounterFoundation; + leadingIcon: MDCTextFieldIconFoundation; + trailingIcon: MDCTextFieldIconFoundation; +} + +export type RippleFactory = (el: Element, foundation: MDCRippleFoundation) => MDCRipple; +export type LineRippleFactory = (el: Element) => MDCLineRipple; +export type HelperTextFactory = (el: Element) => MDCTextFieldHelperText; +export type CharacterCounterFactory = (el: Element) => MDCTextFieldCharacterCounter; +export type IconFactory = (el: Element) => MDCTextFieldIcon; +export type LabelFactory = (el: Element) => MDCFloatingLabel; +export type OutlineFactory = (el: Element) => MDCNotchedOutline; diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js index 5f65c1d57b0..c47c8c5260c 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -183,7 +183,7 @@ class JsBundleFactory { tabIndicator: getAbsolutePath('/packages/mdc-tab-indicator/index.ts'), tabScroller: getAbsolutePath('/packages/mdc-tab-scroller/index.ts'), tabs: getAbsolutePath('/packages/mdc-tabs/index.js'), - textfield: getAbsolutePath('/packages/mdc-textfield/index.js'), + textfield: getAbsolutePath('/packages/mdc-textfield/index.ts'), toolbar: getAbsolutePath('/packages/mdc-toolbar/index.js'), topAppBar: getAbsolutePath('/packages/mdc-top-app-bar/index.js'), }, diff --git a/test/unit/mdc-textfield/foundation.test.js b/test/unit/mdc-textfield/foundation.test.js index 178462377d5..6f0bfab0c14 100644 --- a/test/unit/mdc-textfield/foundation.test.js +++ b/test/unit/mdc-textfield/foundation.test.js @@ -56,32 +56,37 @@ test('defaultAdapter returns a complete adapter implementation', () => { ]); }); -const setupTest = () => { +const setupTest = ({ + useCharacterCounter = false, + useHelperText = false, + useLeadingIcon = false, + useTrailingIcon = false, +} = {}) => { const mockAdapter = td.object(MDCTextFieldFoundation.defaultAdapter); - const helperText = td.object({ + const helperText = useHelperText ? td.object({ setContent: () => {}, showToScreenReader: () => {}, setValidity: () => {}, - }); - const characterCounter = td.object({ + }) : undefined; + const characterCounter = useCharacterCounter ? td.object({ setCounterValue: () => {}, - }); - const leadingIcon = td.object({ + }) : undefined; + const leadingIcon = useLeadingIcon ? td.object({ setDisabled: () => {}, setAriaLabel: () => {}, setContent: () => {}, registerInteractionHandler: () => {}, deregisterInteractionHandler: () => {}, handleInteraction: () => {}, - }); - const trailingIcon = td.object({ + }) : undefined; + const trailingIcon = useTrailingIcon ? td.object({ setDisabled: () => {}, setAriaLabel: () => {}, setContent: () => {}, registerInteractionHandler: () => {}, deregisterInteractionHandler: () => {}, handleInteraction: () => {}, - }); + }) : undefined; const foundationMap = { helperText, characterCounter, @@ -97,13 +102,24 @@ test('#constructor sets disabled to false', () => { assert.isNotOk(foundation.isDisabled()); }); -const setupValueTest = (value, optIsValid, optIsBadInput, hasLabel) => { - const {foundation, mockAdapter, helperText} = setupTest(); +const setupValueTest = ({ + value, + optIsValid, + optIsBadInput, + hasLabel, + useCharacterCounter = false, + useHelperText = false, + useLeadingIcon = false, + useTrailingIcon = false, +} = {}) => { + const {foundation, mockAdapter, helperText} = setupTest({ + useCharacterCounter, useHelperText, useLeadingIcon, useTrailingIcon, + }); const nativeInput = { value: value, validity: { - valid: optIsValid === undefined ? true : !!optIsValid, - badInput: optIsBadInput === undefined ? false : !!optIsBadInput, + valid: optIsValid === undefined ? true : Boolean(optIsValid), + badInput: optIsBadInput === undefined ? false : Boolean(optIsBadInput), }, }; if (hasLabel) { @@ -126,7 +142,7 @@ test('#getValue returns the field\'s value', () => { test('#setValue with non-empty value styles the label', () => { const value = 'new value'; - const {foundation, nativeInput, mockAdapter} = setupValueTest('', undefined, undefined, true); + const {foundation, nativeInput, mockAdapter} = setupValueTest({value: '', hasLabel: true}); // Initial empty value should not float label. td.verify(mockAdapter.floatLabel(false), {times: 0}); nativeInput.value = value; @@ -136,7 +152,7 @@ test('#setValue with non-empty value styles the label', () => { }); test('#setValue with empty value styles the label', () => { - const {foundation, nativeInput, mockAdapter} = setupValueTest('old value', undefined, undefined, true); + const {foundation, nativeInput, mockAdapter} = setupValueTest({value: 'old value', hasLabel: true}); // Initial value should float the label. td.verify(mockAdapter.floatLabel(true)); nativeInput.value = ''; @@ -147,7 +163,7 @@ test('#setValue with empty value styles the label', () => { test('#setValue valid and invalid input', () => { const {foundation, mockAdapter, nativeInput, helperText} = - setupValueTest('', /* isValid */ false, undefined, true); + setupValueTest({value: '', optIsValid: false, hasLabel: true, useHelperText: true}); foundation.setValue('invalid'); td.verify(mockAdapter.addClass(cssClasses.INVALID)); @@ -165,7 +181,7 @@ test('#setValue valid and invalid input', () => { test('#setValue with invalid status and empty value does not shake the label', () => { const {foundation, mockAdapter, helperText} = - setupValueTest('', /* isValid */ false, undefined, true); + setupValueTest({value: '', optIsValid: false, hasLabel: true, useHelperText: true}); foundation.setValue(''); td.verify(mockAdapter.addClass(cssClasses.INVALID)); @@ -175,14 +191,14 @@ test('#setValue with invalid status and empty value does not shake the label', ( }); test('#setValue does not affect focused state', () => { - const {foundation, mockAdapter} = setupValueTest(''); + const {foundation, mockAdapter} = setupValueTest({value: ''}); foundation.setValue(''); td.verify(mockAdapter.addClass(cssClasses.FOCUSED), {times: 0}); td.verify(mockAdapter.removeClass(cssClasses.FOCUSED), {times: 0}); }); test('#setValue does not affect disabled state', () => { - const {foundation, mockAdapter} = setupValueTest(''); + const {foundation, mockAdapter} = setupValueTest({value: ''}); foundation.setValue(''); td.verify(mockAdapter.addClass(cssClasses.DISABLED), {times: 0}); td.verify(mockAdapter.removeClass(cssClasses.DISABLED), {times: 0}); @@ -191,7 +207,7 @@ test('#setValue does not affect disabled state', () => { }); test('#isValid for native validation', () => { - const {foundation, nativeInput} = setupValueTest('', /* isValid */ true); + const {foundation, nativeInput} = setupValueTest({value: '', optIsValid: true}); assert.isOk(foundation.isValid()); nativeInput.validity.valid = false; @@ -199,7 +215,7 @@ test('#isValid for native validation', () => { }); test('#setValid overrides native validation when useNativeValidation set to false', () => { - const {foundation, nativeInput} = setupValueTest('', /* isValid */ false); + const {foundation, nativeInput} = setupValueTest({value: '', optIsValid: false}); foundation.setUseNativeValidation(false); foundation.setValid(true); assert.isOk(foundation.isValid()); @@ -210,7 +226,7 @@ test('#setValid overrides native validation when useNativeValidation set to fals }); test('#setValid updates classes', () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); td.when(mockAdapter.hasLabel()).thenReturn(true); foundation.setValid(false); @@ -231,7 +247,7 @@ test('#setValid updates classes', () => { }); test('#setValid updates classes, but not label methods when hasLabel is false', () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); foundation.setValid(false); td.verify(mockAdapter.addClass(cssClasses.INVALID)); @@ -296,13 +312,13 @@ test('#setDisabled removes mdc-text-field--disabled when set to false', () => { }); test('#setDisabled sets disabled on leading icon', () => { - const {foundation, leadingIcon} = setupTest(); + const {foundation, leadingIcon} = setupTest({useLeadingIcon: true}); foundation.setDisabled(true); td.verify(leadingIcon.setDisabled(true)); }); test('#setDisabled sets disabled on trailing icon', () => { - const {foundation, trailingIcon} = setupTest(); + const {foundation, trailingIcon} = setupTest({useTrailingIcon: true}); foundation.setDisabled(true); td.verify(trailingIcon.setDisabled(true)); }); @@ -322,15 +338,17 @@ test('#setValid removes mdc-textfied--invalid when set to true', () => { test('#init focuses on input if adapter.isFocused is true', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.isFocused()).thenReturn(true); + foundation.activateFocus = td.func('activateFocus'); foundation.init(); - td.verify(foundation.inputFocusHandler_()); + td.verify(foundation.activateFocus(), {times: 1}); }); test('#init does not focus if adapter.isFocused is false', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.isFocused()).thenReturn(false); foundation.init(); - td.verify(foundation.inputFocusHandler_(), {times: 0}); + foundation.activateFocus = td.func('activateFocus'); + td.verify(foundation.activateFocus(), {times: 0}); }); test('#init adds event listeners', () => { @@ -376,7 +394,7 @@ test('#init floats label if the input contains a value', () => { td.verify(mockAdapter.floatLabel(true)); }); -test('#init doesnot call floatLabel if there is no label and the input contains a value', () => { +test('#init does not call floatLabel if there is no label and the input contains a value', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.getNativeInput()).thenReturn({ value: 'Pre-filled value', @@ -403,35 +421,55 @@ test('#init does not float label if the input does not contain a value', () => { }); test('#setHelperTextContent sets the content of the helper text element', () => { - const {foundation, helperText} = setupTest(); + const {foundation, helperText} = setupTest({useHelperText: true}); foundation.setHelperTextContent('foo'); td.verify(helperText.setContent('foo')); }); test('#setLeadingIconAriaLabel sets the aria-label of the leading icon element', () => { - const {foundation, leadingIcon} = setupTest(); + const {foundation, leadingIcon} = setupTest({useLeadingIcon: true}); foundation.setLeadingIconAriaLabel('foo'); td.verify(leadingIcon.setAriaLabel('foo')); }); +test('#setLeadingIconAriaLabel does nothing if element is not present', () => { + const {foundation} = setupTest({useLeadingIcon: false}); + assert.doesNotThrow(() => foundation.setLeadingIconAriaLabel('foo')); +}); + test('#setLeadingIconContent sets the content of the leading icon element', () => { - const {foundation, leadingIcon} = setupTest(); + const {foundation, leadingIcon} = setupTest({useLeadingIcon: true}); foundation.setLeadingIconContent('foo'); td.verify(leadingIcon.setContent('foo')); }); +test('#setLeadingIconContent does nothing if element is not present', () => { + const {foundation} = setupTest({useLeadingIcon: false}); + assert.doesNotThrow(() => foundation.setLeadingIconContent('foo')); +}); + test('#setTrailingIconAriaLabel sets the aria-label of the trailing icon element', () => { - const {foundation, trailingIcon} = setupTest(); + const {foundation, trailingIcon} = setupTest({useTrailingIcon: true}); foundation.setTrailingIconAriaLabel('foo'); td.verify(trailingIcon.setAriaLabel('foo')); }); +test('#setTrailingIconAriaLabel does nothing if element is not present', () => { + const {foundation} = setupTest({useTrailingIcon: false}); + assert.doesNotThrow(() => foundation.setTrailingIconAriaLabel('foo')); +}); + test('#setTrailingIconContent sets the content of the trailing icon element', () => { - const {foundation, trailingIcon} = setupTest(); + const {foundation, trailingIcon} = setupTest({useTrailingIcon: true}); foundation.setTrailingIconContent('foo'); td.verify(trailingIcon.setContent('foo')); }); +test('#setTrailingIconContent does nothing if element is not present', () => { + const {foundation} = setupTest({useTrailingIcon: false}); + assert.doesNotThrow(() => foundation.setTrailingIconContent('foo')); +}); + test('#notchOutline updates the width of the outline element', () => { const {foundation, mockAdapter} = setupTest(); td.when(mockAdapter.getLabelWidth()).thenReturn(LABEL_WIDTH); @@ -593,7 +631,7 @@ test('on focus do not styles label if hasLabel is false', () => { }); test('on focus makes helper text visible to the screen reader', () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); let focus; td.when(mockAdapter.registerInputInteractionHandler('focus', td.matchers.isA(Function))) .thenDo((evtType, handler) => { @@ -605,7 +643,7 @@ test('on focus makes helper text visible to the screen reader', () => { }); const setupBlurTest = () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); let blur; td.when(mockAdapter.registerInputInteractionHandler('blur', td.matchers.isA(Function))).thenDo((evtType, handler) => { blur = handler; @@ -804,7 +842,7 @@ test('touchstart on the input sets the line ripple origin', () => { }); test('on validation attribute change calls styleValidity_', () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); let attributeChange; td.when(mockAdapter.registerValidationAttributeChangeHandler(td.matchers.isA(Function))) .thenDo((handler) => attributeChange = handler); @@ -821,7 +859,7 @@ test('on validation attribute change calls styleValidity_', () => { }); test('should not call styleValidity_ on non-whitelisted attribute change', () => { - const {foundation, mockAdapter, helperText} = setupTest(); + const {foundation, mockAdapter, helperText} = setupTest({useHelperText: true}); let attributeChange; td.when(mockAdapter.registerValidationAttributeChangeHandler(td.matchers.isA(Function))) .thenDo((handler) => attributeChange = handler); @@ -838,7 +876,7 @@ test('should not call styleValidity_ on non-whitelisted attribute change', () => }); test('label floats on invalid input even if value is empty', () => { - const {mockAdapter} = setupValueTest('', false, true, true); + const {mockAdapter} = setupValueTest({value: '', optIsValid: false, optIsBadInput: true, hasLabel: true}); td.verify(mockAdapter.floatLabel(true)); }); @@ -866,7 +904,7 @@ test('#handleInput activates focus state', () => { }); test('#handleInput updates character counter on text input', () => { - const {foundation, mockAdapter, characterCounter} = setupTest(); + const {foundation, mockAdapter, characterCounter} = setupTest({useCharacterCounter: true}); const nativeInput = { type: 'text', @@ -884,7 +922,7 @@ test('#handleInput updates character counter on text input', () => { test('#handleInput throws error when maxLength HTML attribute is not found in input element', () => { - const {foundation, mockAdapter} = setupTest(); + const {foundation, mockAdapter} = setupTest({useCharacterCounter: true}); const nativeInput = { type: 'text', @@ -901,7 +939,7 @@ test('#handleInput throws error when maxLength HTML attribute is not found in in test('#handleValidationAttributeChange sets character counter when maxlength attribute value is changed in input ' + 'element', () => { - const {foundation, mockAdapter, characterCounter} = setupTest(); + const {foundation, mockAdapter, characterCounter} = setupTest({useCharacterCounter: true}); const nativeInput = { type: 'text', diff --git a/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js index 0025a472847..2561f42b99e 100644 --- a/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js @@ -46,6 +46,10 @@ test('defaultAdapter returns a complete adapter implementation', () => { const setupTest = () => setupFoundationTest(MDCTextFieldCharacterCounterFoundation); +test('istanbul code coverage', () => { + assert.doesNotThrow(() => new MDCTextFieldCharacterCounterFoundation()); +}); + test('#setContent sets the content of the character counter element', () => { const {foundation, mockAdapter} = setupTest(); foundation.setCounterValue(12, 20); diff --git a/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js index a4560fd79b8..1bb4d634f0b 100644 --- a/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js @@ -48,6 +48,10 @@ test('defaultAdapter returns a complete adapter implementation', () => { const setupTest = () => setupFoundationTest(MDCTextFieldHelperTextFoundation); +test('istanbul code coverage', () => { + assert.doesNotThrow(() => new MDCTextFieldHelperTextFoundation()); +}); + test('#setContent sets the content of the helper text element', () => { const {foundation, mockAdapter} = setupTest(); foundation.setContent('foo'); diff --git a/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js index 4bcaa7ba49a..914860a2ce7 100644 --- a/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js @@ -44,6 +44,10 @@ test('defaultAdapter returns a complete adapter implementation', () => { const setupTest = () => setupFoundationTest(MDCTextFieldIconFoundation); +test('istanbul code coverage', () => { + assert.doesNotThrow(() => new MDCTextFieldIconFoundation()); +}); + test('#init adds event listeners', () => { const {foundation, mockAdapter} = setupTest(); foundation.init(); diff --git a/test/unit/mdc-textfield/mdc-text-field.test.js b/test/unit/mdc-textfield/mdc-text-field.test.js index 543fd12d157..ccb3a353fcf 100644 --- a/test/unit/mdc-textfield/mdc-text-field.test.js +++ b/test/unit/mdc-textfield/mdc-text-field.test.js @@ -30,8 +30,9 @@ import {MDCRipple} from '../../../packages/mdc-ripple/index'; import {MDCLineRipple} from '../../../packages/mdc-line-ripple/index'; import {MDCFloatingLabel} from '../../../packages/mdc-floating-label/index'; import {MDCNotchedOutline} from '../../../packages/mdc-notched-outline/index'; -import {MDCTextField, MDCTextFieldFoundation, MDCTextFieldHelperText, MDCTextFieldCharacterCounter, - MDCTextFieldIcon} from '../../../packages/mdc-textfield/index'; +import { + MDCTextField, MDCTextFieldFoundation, MDCTextFieldHelperText, MDCTextFieldCharacterCounter, MDCTextFieldIcon, +} from '../../../packages/mdc-textfield/index'; import {cssClasses as helperTextCssClasses} from '../../../packages/mdc-textfield/helper-text/constants'; import {cssClasses as characterCounterCssClasses} from '../../../packages/mdc-textfield/character-counter/constants'; @@ -218,6 +219,50 @@ test('#constructor handles undefined optional sub-elements gracefully', () => { assert.doesNotThrow(() => new MDCTextField(root)); }); +test('default adapter methods handle sub-elements when present', () => { + const root = getFixture(); + const component = new MDCTextField(root); + const adapter = component.getDefaultFoundation().adapter_; + assert.isFalse(adapter.hasClass('foo')); + assert.equal(adapter.getLabelWidth(), 0); + assert.doesNotThrow(() => adapter.floatLabel(true)); +}); + +test('default adapter methods handle undefined optional sub-elements gracefully', () => { + const root = bel` +
+ +
+ `; + const component = new MDCTextField(root); + const adapter = component.getDefaultFoundation().adapter_; + assert.equal(adapter.getLabelWidth(), 0); + assert.isFalse(adapter.hasLabel()); + assert.isFalse(adapter.hasOutline()); + assert.doesNotThrow(() => adapter.floatLabel(true)); + assert.doesNotThrow(() => adapter.shakeLabel(true)); + assert.doesNotThrow(() => adapter.activateLineRipple()); + assert.doesNotThrow(() => adapter.deactivateLineRipple()); + assert.doesNotThrow(() => adapter.setLineRippleTransformOrigin(0)); + assert.doesNotThrow(() => adapter.closeOutline()); + assert.doesNotThrow(() => adapter.notchOutline(0)); +}); + +/** + * @param {!HTMLElement=} root + * @return {{ + * root: HTMLElement, + * component: MDCTextField, + * foundation: MDCTextFieldFoundation, + * adapter: MDCTextFieldAdapter, + * outline: MDCNotchedOutline, + * icon: MDCTextFieldIcon, + * lineRipple: MDCLineRipple, + * label: MDCFloatingLabel, + * helperText: MDCTextFieldHelperText, + * characterCounter: MDCTextFieldCharacterCounter, + * }} + */ function setupTest(root = getFixture()) { const lineRipple = new FakeLineRipple(); const helperText = new FakeHelperText(); @@ -225,6 +270,7 @@ function setupTest(root = getFixture()) { const icon = new FakeIcon(); const label = new FakeLabel(); const outline = new FakeOutline(); + const component = new MDCTextField( root, undefined, @@ -236,7 +282,11 @@ function setupTest(root = getFixture()) { () => label, () => outline ); - return {root, component, lineRipple, helperText, characterCounter, icon, label, outline}; + + const foundation = component.foundation_; + const adapter = foundation.adapter_; + + return {root, component, foundation, adapter, lineRipple, helperText, characterCounter, icon, label, outline}; } test('#destroy cleans up the ripple if present', () => { @@ -307,6 +357,25 @@ test('#destroy handles undefined optional sub-elements gracefully', () => { assert.doesNotThrow(() => component.destroy()); }); +test('#destroy handles undefined optional ripple gracefully', () => { + const root = getFixture(); + const component = new MDCTextField(root); + component.ripple = null; + assert.doesNotThrow(() => component.destroy()); +}); + +test('#destroy calls destroy for both icon elements if present', () => { + const root = getFixture(true); + root.classList.add('mdc-text-field--with-trailing-icon'); + root.appendChild(bel`3d_rotations`); + const component = new MDCTextField(root); + component.leadingIcon_.destroy = td.func('leadingIcon_.destroy'); + component.trailingIcon_.destroy = td.func('trailingIcon_.destroy'); + component.destroy(); + td.verify(component.leadingIcon_.destroy()); + td.verify(component.trailingIcon_.destroy()); +}); + test('#initialSyncWithDom sets disabled if input element is not disabled', () => { const {component} = setupTest(); component.initialSyncWithDom(); @@ -374,6 +443,13 @@ test('#adapter.addClass adds a class to the root element', () => { assert.isOk(root.classList.contains('foo')); }); +test('layout calls foundation notchOutline', () => { + const {component, foundation} = setupTest(); + foundation.notchOutline = td.func('notchOutline'); + component.layout(); + td.verify(foundation.notchOutline(false)); +}); + test('#adapter.removeClass removes a class from the root element', () => { const {root, component} = setupTest(); root.classList.add('foo'); @@ -467,9 +543,7 @@ test('#adapter.setLineRippleTransformOrigin calls the setRippleCenter method on function setupMockFoundationTest(root = getFixture()) { const MockFoundationConstructor = td.constructor(MDCTextFieldFoundation); const mockFoundation = new MockFoundationConstructor(); - const component = new MDCTextField( - root, - mockFoundation); + const component = new MDCTextField(root, mockFoundation); return {root, component, mockFoundation}; }