diff --git a/.gitignore b/.gitignore index 89776a7dc01..f0a6e5eb0f1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ out .idea *.sw* .closure-tmp -.vscode +.site-generator-tmp .typescript-tmp +.vscode diff --git a/packages/mdc-base/component.ts b/packages/mdc-base/component.ts index d39a498f570..80b0402fb1a 100644 --- a/packages/mdc-base/component.ts +++ b/packages/mdc-base/component.ts @@ -100,8 +100,7 @@ class MDCComponent { } /** - * Fires a cross-browser-compatible custom event from the component root of the given type, - * with the given data. + * Fires a cross-browser-compatible custom event from the component root of the given type, with the given data. */ emit(evtType: string, evtData: T, shouldBubble = false) { let evt: CustomEvent; diff --git a/packages/mdc-list/index.ts b/packages/mdc-list/index.ts index cf2379589fd..5bcc61eeda5 100644 --- a/packages/mdc-list/index.ts +++ b/packages/mdc-list/index.ts @@ -135,7 +135,7 @@ class MDCList extends MDCComponent { } }, focusItemAtIndex: (index) => { - const element = this.listElements[index] as HTMLElement; + const element = this.listElements[index] as HTMLElement | undefined; if (element) { element.focus(); } diff --git a/packages/mdc-menu/types.ts b/packages/mdc-menu/types.ts index 5a96a05b009..e40d4421ae7 100644 --- a/packages/mdc-menu/types.ts +++ b/packages/mdc-menu/types.ts @@ -24,6 +24,9 @@ import {MDCList} from '@material/list/index'; import {MDCMenuSurface} from '@material/menu-surface/index'; +export type MenuItemEvent = CustomEvent; +export type DefaultMenuItemEvent = CustomEvent; + /** * Event properties used by the adapter and foundation. */ diff --git a/packages/mdc-select/README.md b/packages/mdc-select/README.md index f30eb1254a7..28c34c55493 100644 --- a/packages/mdc-select/README.md +++ b/packages/mdc-select/README.md @@ -407,14 +407,14 @@ The `MDCSelect` component API is modeled after a subset of the `HTMLSelectElemen | Property | Type | Description | | --- | --- | --- | -| `value` | string | The `value`/`data-value` of the currently selected option. | -| `selectedIndex` | number | The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element. | -| `disabled` | boolean | Whether or not the component is disabled. Setting this sets the disabled state on the component. | -| `valid` | boolean | Whether or not the component is in a valid state. Setting this updates styles on the component, but does not affect the native validity state. | -| `required` | boolean | Whether or not the component is required. Setting this updates the `required` or `aria-required` attribute on the component and enables validation. | -| `leadingIconAriaLabel` | string (write-only) | Proxies to the foundation's `setLeadingIconAriaLabel` method. | -| `leadingIconContent` | string (write-only) | Proxies to the foundation's `setLeadingIconContent` method. | -| `helperTextContent` | string (write-only)| Proxies to the foundation's `setHelperTextContent` method when set. | +| `value` | `string` | The `value`/`data-value` of the currently selected option. | +| `selectedIndex` | `number` | The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element. | +| `disabled` | `boolean` | Whether or not the component is disabled. Setting this sets the disabled state on the component. | +| `valid` | `boolean` | Whether or not the component is in a valid state. Setting this updates styles on the component, but does not affect the native validity state. | +| `required` | `boolean` | Whether or not the component is required. Setting this updates the `required` or `aria-required` attribute on the component and enables validation. | +| `leadingIconAriaLabel` | `string` (write-only) | Proxies to the foundation's `setLeadingIconAriaLabel` method. | +| `leadingIconContent` | `string` (write-only) | Proxies to the foundation's `setLeadingIconContent` method. | +| `helperTextContent` | `string` (write-only)| Proxies to the foundation's `setHelperTextContent` method when set. | ### Events @@ -462,7 +462,7 @@ If you are using a JavaScript framework, such as React or Angular, you can creat | `handleBlur() => void` | Handles a blur event on the `select` element. | | `handleClick(normalizedX: number) => void` | Sets the line ripple center to the normalizedX for the line ripple. | | `handleChange() => void` | Handles a change to the `select` element's value. This must be called both for `change` events and programmatic changes requested via the component API. | -| `handleKeydown(event: Event) => void` | Handles opening the menu (enhanced select) when the `mdc-select__selected-text` element is focused and the user presses the `Enter` or `Space` key. | +| `handleKeydown(event: KeyboardEvent) => void` | Handles opening the menu (enhanced select) when the `mdc-select__selected-text` element is focused and the user presses the `Enter` or `Space` key. | | `setSelectedIndex(index: number) => void` | Handles setting the `mdc-select__selected-text` element and closing the menu (enhanced select only). Also causes the label to float and outline to notch if needed. | | `setValue(value: string) => void` | Handles setting the value through the adapter and causes the label to float and outline to notch if needed. | | `getValue() => string` | Handles getting the value through the adapter. | diff --git a/packages/mdc-select/adapter.js b/packages/mdc-select/adapter.ts similarity index 61% rename from packages/mdc-select/adapter.js rename to packages/mdc-select/adapter.ts index be1ab512076..a217c11fc1a 100644 --- a/packages/mdc-select/adapter.js +++ b/packages/mdc-select/adapter.ts @@ -21,159 +21,118 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ -/* eslint-disable no-unused-vars */ -import {MDCSelectIconFoundation} from './icon/index'; -import {MDCSelectHelperTextFoundation} from './helper-text/index'; -/* eslint-enable no-unused-vars */ - -/** - * @typedef {{ - * leadingIcon: (!MDCSelectIconFoundation|undefined), - * helperText: (!MDCSelectHelperTextFoundation|undefined), - * }} - */ -let FoundationMapType; - /** - * Adapter for MDC Select. Provides an interface for managing - * - classes - * - dom - * - event handlers - * - * Additionally, provides type information for the adapter to the Closure - * compiler. - * + * 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 - * - * @record */ - -class MDCSelectAdapter { +interface MDCSelectAdapter { /** * Adds class to root element. - * @param {string} className */ - addClass(className) {} + addClass(className: string): void; /** * Removes a class from the root element. - * @param {string} className */ - removeClass(className) {} + removeClass(className: string): void; /** * Returns true if the root element contains the given class name. - * @param {string} className - * @return {boolean} */ - hasClass(className) {} + hasClass(className: string): boolean; /** * Activates the bottom line, showing a focused state. */ - activateBottomLine() {} + activateBottomLine(): void; /** * Deactivates the bottom line. */ - deactivateBottomLine() {} + deactivateBottomLine(): void; /** * Sets the value of the select. - * @param {string} value */ - setValue(value) {} + setValue(value: string): void; /** * Returns the selected value of the select element. - * @return {string} */ - getValue() {} + getValue(): string; /** * Floats label determined based off of the shouldFloat argument. - * @param {boolean} shouldFloat */ - floatLabel(shouldFloat) {} + floatLabel(shouldFloat: boolean): void; /** * Returns width of label in pixels, if the label exists. - * @return {number} */ - getLabelWidth() {} + getLabelWidth(): number; /** * Returns true if outline element exists, false if it doesn't. - * @return {boolean} */ - hasOutline() {} + hasOutline(): boolean; /** * Only implement if outline element exists. - * @param {number} labelWidth */ - notchOutline(labelWidth) {} + notchOutline(labelWidth: number): void; /** * Closes notch in outline element, if the outline exists. */ - closeOutline() {} + closeOutline(): void; /** * Opens the menu. */ - openMenu() {} + openMenu(): void; /** * Closes the menu. */ - closeMenu() {} + closeMenu(): void; /** * Returns true if the menu is currently open. - * @return {boolean} */ - isMenuOpen() {} + isMenuOpen(): boolean; /** * Sets the selected index of the select to the index provided. - * @param {number} index */ - setSelectedIndex(index) {} + setSelectedIndex(index: number): void; /** * Sets the select to disabled. - * @param {boolean} isDisabled */ - setDisabled(isDisabled) {} + setDisabled(isDisabled: boolean): void; /** * Sets the line ripple transform origin center. - * @param {number} normalizedX */ - setRippleCenter(normalizedX) {} + setRippleCenter(normalizedX: number): void; /** * Emits a change event when an element is selected. - * @param {string} value */ - notifyChange(value) {} + notifyChange(value: string): void; /** * Checks if the select is currently valid. - * @return {boolean} isValid */ - checkValidity() {} + checkValidity(): boolean; /** * Adds/Removes the invalid class. - * @param {boolean} isValid */ - setValid(isValid) {} + setValid(isValid: boolean): void; } -export {MDCSelectAdapter, FoundationMapType}; +export {MDCSelectAdapter as default, MDCSelectAdapter}; diff --git a/packages/mdc-select/constants.js b/packages/mdc-select/constants.ts similarity index 97% rename from packages/mdc-select/constants.js rename to packages/mdc-select/constants.ts index bb9d343630f..585a88dbbea 100644 --- a/packages/mdc-select/constants.js +++ b/packages/mdc-select/constants.ts @@ -21,36 +21,33 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { DISABLED: 'mdc-select--disabled', - ROOT: 'mdc-select', - OUTLINED: 'mdc-select--outlined', FOCUSED: 'mdc-select--focused', - SELECTED_ITEM_CLASS: 'mdc-list-item--selected', - WITH_LEADING_ICON: 'mdc-select--with-leading-icon', INVALID: 'mdc-select--invalid', + OUTLINED: 'mdc-select--outlined', REQUIRED: 'mdc-select--required', + ROOT: 'mdc-select', + SELECTED_ITEM_CLASS: 'mdc-list-item--selected', + WITH_LEADING_ICON: 'mdc-select--with-leading-icon', }; -/** @enum {string} */ const strings = { ARIA_CONTROLS: 'aria-controls', + ARIA_SELECTED_ATTR: 'aria-selected', CHANGE_EVENT: 'MDCSelect:change', - SELECTED_ITEM_SELECTOR: `.${cssClasses.SELECTED_ITEM_CLASS}`, - LEADING_ICON_SELECTOR: '.mdc-select__icon', - SELECTED_TEXT_SELECTOR: '.mdc-select__selected-text', + ENHANCED_VALUE_ATTR: 'data-value', HIDDEN_INPUT_SELECTOR: 'input[type="hidden"]', - MENU_SELECTOR: '.mdc-select__menu', - LINE_RIPPLE_SELECTOR: '.mdc-line-ripple', LABEL_SELECTOR: '.mdc-floating-label', + LEADING_ICON_SELECTOR: '.mdc-select__icon', + LINE_RIPPLE_SELECTOR: '.mdc-line-ripple', + MENU_SELECTOR: '.mdc-select__menu', NATIVE_CONTROL_SELECTOR: '.mdc-select__native-control', OUTLINE_SELECTOR: '.mdc-notched-outline', - ENHANCED_VALUE_ATTR: 'data-value', - ARIA_SELECTED_ATTR: 'aria-selected', + SELECTED_ITEM_SELECTOR: `.${cssClasses.SELECTED_ITEM_CLASS}`, + SELECTED_TEXT_SELECTOR: '.mdc-select__selected-text', }; -/** @enum {number} */ const numbers = { LABEL_SCALE: 0.75, }; diff --git a/packages/mdc-select/foundation.js b/packages/mdc-select/foundation.ts similarity index 65% rename from packages/mdc-select/foundation.js rename to packages/mdc-select/foundation.ts index d295783a7b0..0722968ddcb 100644 --- a/packages/mdc-select/foundation.js +++ b/packages/mdc-select/foundation.ts @@ -22,85 +22,79 @@ */ import {MDCFoundation} from '@material/base/foundation'; -/* eslint-disable no-unused-vars */ -import {MDCSelectAdapter, FoundationMapType} from './adapter'; -import {MDCSelectIconFoundation} from './icon/index'; -import {MDCSelectHelperTextFoundation} from './helper-text/index'; -/* eslint-enable no-unused-vars */ -import {cssClasses, strings, numbers} from './constants'; +import {MDCSelectAdapter} from './adapter'; +import {cssClasses, numbers, strings} from './constants'; +import {MDCSelectHelperTextFoundation} from './helper-text/foundation'; +import {MDCSelectIconFoundation} from './icon/foundation'; +import {FoundationMapType} from './types'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCSelectFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCSelectFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {number} */ static get numbers() { return numbers; } - /** @return enum {string} */ static get strings() { return strings; } /** - * {@see MDCSelectAdapter} for typing information on parameters and return - * types. - * @return {!MDCSelectAdapter} + * See {@link MDCSelectAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCSelectAdapter} */ ({ - addClass: (/* className: string */) => {}, - removeClass: (/* className: string */) => {}, - hasClass: (/* className: string */) => false, - activateBottomLine: () => {}, - deactivateBottomLine: () => {}, - setValue: () => {}, - getValue: () => {}, - floatLabel: (/* value: boolean */) => {}, - getLabelWidth: () => {}, + static get defaultAdapter(): MDCSelectAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: () => undefined, + removeClass: () => undefined, + hasClass: () => false, + activateBottomLine: () => undefined, + deactivateBottomLine: () => undefined, + setValue: () => undefined, + getValue: () => '', + floatLabel: () => undefined, + getLabelWidth: () => 0, hasOutline: () => false, - notchOutline: (/* labelWidth: number, */) => {}, - closeOutline: () => {}, - openMenu: () => {}, - closeMenu: () => {}, - isMenuOpen: () => {}, - setSelectedIndex: () => {}, - setDisabled: () => {}, - setRippleCenter: () => {}, - notifyChange: () => {}, - checkValidity: () => {}, - setValid: () => {}, - }); + notchOutline: () => undefined, + closeOutline: () => undefined, + openMenu: () => undefined, + closeMenu: () => undefined, + isMenuOpen: () => false, + setSelectedIndex: () => undefined, + setDisabled: () => undefined, + setRippleCenter: () => undefined, + notifyChange: () => undefined, + checkValidity: () => false, + setValid: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } + private readonly leadingIcon_: MDCSelectIconFoundation | undefined; + private readonly helperText_: MDCSelectHelperTextFoundation | undefined; + + /* istanbul ignore next: optional argument is not a branch statement */ /** - * @param {!MDCSelectAdapter} 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(MDCSelectFoundation.defaultAdapter, adapter)); + constructor(adapter?: Partial, foundationMap: Partial = {}) { + super({...MDCSelectFoundation.defaultAdapter, ...adapter}); - /** @type {!MDCSelectIconFoundation|undefined} */ this.leadingIcon_ = foundationMap.leadingIcon; - /** @type {!MDCSelectHelperTextFoundation|undefined} */ this.helperText_ = foundationMap.helperText; } - setSelectedIndex(index) { + setSelectedIndex(index: number) { this.adapter_.setSelectedIndex(index); this.adapter_.closeMenu(); const didChange = true; this.handleChange(didChange); } - setValue(value) { + setValue(value: string) { this.adapter_.setValue(value); const didChange = true; this.handleChange(didChange); @@ -110,8 +104,12 @@ class MDCSelectFoundation extends MDCFoundation { return this.adapter_.getValue(); } - setDisabled(isDisabled) { - isDisabled ? this.adapter_.addClass(cssClasses.DISABLED) : this.adapter_.removeClass(cssClasses.DISABLED); + setDisabled(isDisabled: boolean) { + if (isDisabled) { + this.adapter_.addClass(cssClasses.DISABLED); + } else { + this.adapter_.removeClass(cssClasses.DISABLED); + } this.adapter_.setDisabled(isDisabled); this.adapter_.closeMenu(); @@ -121,9 +119,9 @@ class MDCSelectFoundation extends MDCFoundation { } /** - * @param {string} content Sets the content of the helper text. + * @param content Sets the content of the helper text. */ - setHelperTextContent(content) { + setHelperTextContent(content: string) { if (this.helperText_) { this.helperText_.setContent(content); } @@ -177,7 +175,9 @@ class MDCSelectFoundation extends MDCFoundation { * Handles blur events from select element. */ handleBlur() { - if (this.adapter_.isMenuOpen()) return; + if (this.adapter_.isMenuOpen()) { + return; + } this.adapter_.removeClass(cssClasses.FOCUSED); this.handleChange(false); this.adapter_.deactivateBottomLine(); @@ -192,15 +192,19 @@ class MDCSelectFoundation extends MDCFoundation { } } - handleClick(normalizedX) { - if (this.adapter_.isMenuOpen()) return; + handleClick(normalizedX: number) { + if (this.adapter_.isMenuOpen()) { + return; + } this.adapter_.setRippleCenter(normalizedX); this.adapter_.openMenu(); } - handleKeydown(event) { - if (this.adapter_.isMenuOpen()) return; + handleKeydown(event: KeyboardEvent) { + if (this.adapter_.isMenuOpen()) { + return; + } const isEnter = event.key === 'Enter' || event.keyCode === 13; const isSpace = event.key === 'Space' || event.keyCode === 32; @@ -215,9 +219,8 @@ class MDCSelectFoundation extends MDCFoundation { /** * Opens/closes the notched outline. - * @param {boolean} openNotch */ - notchOutline(openNotch) { + notchOutline(openNotch: boolean) { if (!this.adapter_.hasOutline()) { return; } @@ -234,9 +237,8 @@ class MDCSelectFoundation extends MDCFoundation { /** * Sets the aria label of the leading icon. - * @param {string} label */ - setLeadingIconAriaLabel(label) { + setLeadingIconAriaLabel(label: string) { if (this.leadingIcon_) { this.leadingIcon_.setAriaLabel(label); } @@ -244,15 +246,14 @@ class MDCSelectFoundation extends MDCFoundation { /** * Sets the text content of the leading icon. - * @param {string} content */ - setLeadingIconContent(content) { + setLeadingIconContent(content: string) { if (this.leadingIcon_) { this.leadingIcon_.setContent(content); } } - setValid(isValid) { + setValid(isValid: boolean) { this.adapter_.setValid(isValid); } @@ -261,4 +262,4 @@ class MDCSelectFoundation extends MDCFoundation { } } -export default MDCSelectFoundation; +export {MDCSelectFoundation as default, MDCSelectFoundation}; diff --git a/packages/mdc-select/helper-text/adapter.js b/packages/mdc-select/helper-text/adapter.ts similarity index 67% rename from packages/mdc-select/helper-text/adapter.js rename to packages/mdc-select/helper-text/adapter.ts index ce0f8c75780..e62a94cece1 100644 --- a/packages/mdc-select/helper-text/adapter.js +++ b/packages/mdc-select/helper-text/adapter.ts @@ -21,56 +21,43 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - /** - * Adapter for MDC Select Helper Text. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Select 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 MDCSelectHelperTextAdapter { +interface MDCSelectHelperTextAdapter { /** * 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 MDCSelectHelperTextAdapter; +export {MDCSelectHelperTextAdapter as default, MDCSelectHelperTextAdapter}; diff --git a/packages/mdc-select/helper-text/constants.js b/packages/mdc-select/helper-text/constants.ts similarity index 96% rename from packages/mdc-select/helper-text/constants.js rename to packages/mdc-select/helper-text/constants.ts index d35cce736ea..6cac34f4189 100644 --- a/packages/mdc-select/helper-text/constants.js +++ b/packages/mdc-select/helper-text/constants.ts @@ -21,13 +21,11 @@ * THE SOFTWARE. */ -/** @enum {string} */ const strings = { ARIA_HIDDEN: 'aria-hidden', ROLE: 'role', }; -/** @enum {string} */ const cssClasses = { HELPER_TEXT_PERSISTENT: 'mdc-select-helper-text--persistent', HELPER_TEXT_VALIDATION_MSG: 'mdc-select-helper-text--validation-msg', diff --git a/packages/mdc-select/helper-text/foundation.js b/packages/mdc-select/helper-text/foundation.ts similarity index 67% rename from packages/mdc-select/helper-text/foundation.js rename to packages/mdc-select/helper-text/foundation.ts index 9a3622880e9..bb7ee871642 100644 --- a/packages/mdc-select/helper-text/foundation.js +++ b/packages/mdc-select/helper-text/foundation.ts @@ -22,58 +22,49 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCSelectHelperTextAdapter from './adapter'; +import {MDCSelectHelperTextAdapter} from './adapter'; import {cssClasses, strings} from './constants'; - -/** - * @extends {MDCFoundation} - * @final - */ -class MDCSelectHelperTextFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCSelectHelperTextFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } /** - * {@see MDCSelectHelperTextAdapter} for typing information on parameters and return - * types. - * @return {!MDCSelectHelperTextAdapter} + * See {@link MDCSelectHelperTextAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCSelectHelperTextAdapter} */ ({ - addClass: () => {}, - removeClass: () => {}, - hasClass: () => {}, - setAttr: () => {}, - removeAttr: () => {}, - setContent: () => {}, - }); + static get defaultAdapter(): MDCSelectHelperTextAdapter { + // 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 {!MDCSelectHelperTextAdapter} adapter - */ - constructor(adapter) { - super(Object.assign(MDCSelectHelperTextFoundation.defaultAdapter, adapter)); - } + constructor(adapter?: Partial) { + super({...MDCSelectHelperTextFoundation.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) { + /** + * 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 MDCSelectHelperTextFoundation 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 MDCSelectHelperTextFoundation extends MDCFoundation { } } - /** Makes the helper text visible to the screen reader. */ + /** + * Makes the helper text visible to screen readers. + */ showToScreenReader() { this.adapter_.removeAttr(strings.ARIA_HIDDEN); } /** * Sets the validity of the helper text based on the select validity. - * @param {boolean} selectIsValid */ - setValidity(selectIsValid) { + setValidity(selectIsValid: boolean) { const helperTextIsPersistent = this.adapter_.hasClass(cssClasses.HELPER_TEXT_PERSISTENT); const helperTextIsValidationMsg = this.adapter_.hasClass(cssClasses.HELPER_TEXT_VALIDATION_MSG); const validationMsgNeedsDisplay = helperTextIsValidationMsg && !selectIsValid; @@ -120,11 +111,10 @@ class MDCSelectHelperTextFoundation extends MDCFoundation { /** * Hides the help text from screen readers. - * @private */ - hide_() { + private hide_() { this.adapter_.setAttr(strings.ARIA_HIDDEN, 'true'); } } -export default MDCSelectHelperTextFoundation; +export {MDCSelectHelperTextFoundation as default, MDCSelectHelperTextFoundation}; diff --git a/packages/mdc-select/helper-text/index.js b/packages/mdc-select/helper-text/index.ts similarity index 67% rename from packages/mdc-select/helper-text/index.js rename to packages/mdc-select/helper-text/index.ts index 7cc2e65233f..a9ee1f208f5 100644 --- a/packages/mdc-select/helper-text/index.js +++ b/packages/mdc-select/helper-text/index.ts @@ -22,45 +22,31 @@ */ import {MDCComponent} from '@material/base/component'; +import {MDCSelectHelperTextFoundation} from './foundation'; -import MDCSelectHelperTextAdapter from './adapter'; -import MDCSelectHelperTextFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCSelectHelperText extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCSelectHelperText} - */ - static attachTo(root) { +class MDCSelectHelperText extends MDCComponent { + static attachTo(root: Element): MDCSelectHelperText { return new MDCSelectHelperText(root); } - /** - * @return {!MDCSelectHelperTextFoundation} - */ - get foundation() { + get foundation(): MDCSelectHelperTextFoundation { return this.foundation_; } - /** - * @return {!MDCSelectHelperTextFoundation} - */ - getDefaultFoundation() { - return new MDCSelectHelperTextFoundation(/** @type {!MDCSelectHelperTextAdapter} */ (Object.assign({ + getDefaultFoundation(): MDCSelectHelperTextFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCSelectHelperTextFoundation({ 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 {MDCSelectHelperText, MDCSelectHelperTextFoundation}; +export {MDCSelectHelperText as default, MDCSelectHelperText}; +export * from './adapter'; +export * from './foundation'; diff --git a/packages/mdc-select/icon/adapter.js b/packages/mdc-select/icon/adapter.ts similarity index 65% rename from packages/mdc-select/icon/adapter.js rename to packages/mdc-select/icon/adapter.ts index 7da053db045..f9cf4d5325f 100644 --- a/packages/mdc-select/icon/adapter.js +++ b/packages/mdc-select/icon/adapter.ts @@ -21,63 +21,50 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ +import {EventType, SpecificEventListener} from '@material/base/index'; /** - * Adapter for MDC Select Icon. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the select 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 MDCSelectIconAdapter { +interface MDCSelectIconAdapter { /** * 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 "MDCSelect:icon" denoting a user has clicked the icon. */ - notifyIconAction() {} + notifyIconAction(): void; } -export default MDCSelectIconAdapter; +export {MDCSelectIconAdapter as default, MDCSelectIconAdapter}; diff --git a/packages/mdc-select/icon/constants.js b/packages/mdc-select/icon/constants.ts similarity index 98% rename from packages/mdc-select/icon/constants.js rename to packages/mdc-select/icon/constants.ts index 5a61d074ad7..50a45780fa4 100644 --- a/packages/mdc-select/icon/constants.js +++ b/packages/mdc-select/icon/constants.ts @@ -21,7 +21,6 @@ * THE SOFTWARE. */ -/** @enum {string} */ const strings = { ICON_EVENT: 'MDCSelect:icon', ICON_ROLE: 'button', diff --git a/packages/mdc-select/icon/foundation.js b/packages/mdc-select/icon/foundation.ts similarity index 56% rename from packages/mdc-select/icon/foundation.js rename to packages/mdc-select/icon/foundation.ts index 3524488224c..69a4613b238 100644 --- a/packages/mdc-select/icon/foundation.js +++ b/packages/mdc-select/icon/foundation.ts @@ -22,66 +22,62 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCSelectIconAdapter from './adapter'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCSelectIconAdapter} from './adapter'; import {strings} from './constants'; +type InteractionEventType = 'click' | 'keydown'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCSelectIconFoundation extends MDCFoundation { - /** @return enum {string} */ +const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; + +class MDCSelectIconFoundation extends MDCFoundation { static get strings() { return strings; } /** - * {@see MDCSelectIconAdapter} for typing information on parameters and return - * types. - * @return {!MDCSelectIconAdapter} + * See {@link MDCSelectIconAdapter} for typing information on parameters and return types. */ - static get defaultAdapter() { - return /** @type {!MDCSelectIconAdapter} */ ({ - getAttr: () => {}, - setAttr: () => {}, - removeAttr: () => {}, - setContent: () => {}, - registerInteractionHandler: () => {}, - deregisterInteractionHandler: () => {}, - notifyIconAction: () => {}, - }); + static get defaultAdapter(): MDCSelectIconAdapter { + // 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 {!MDCSelectIconAdapter} adapter - */ - constructor(adapter) { - super(Object.assign(MDCSelectIconFoundation.defaultAdapter, adapter)); + private savedTabIndex_: string | null = null; + + // assigned in initialSyncWithDOM() + private readonly interactionHandler_!: SpecificEventListener; - /** @private {string?} */ - this.savedTabIndex_ = null; + constructor(adapter?: Partial) { + super({...MDCSelectIconFoundation.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 +91,20 @@ class MDCSelectIconFoundation 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 MDCSelectIconFoundation; +export {MDCSelectIconFoundation as default, MDCSelectIconFoundation}; diff --git a/packages/mdc-select/icon/index.js b/packages/mdc-select/icon/index.ts similarity index 68% rename from packages/mdc-select/icon/index.js rename to packages/mdc-select/icon/index.ts index bfce7f22818..58c27f72220 100644 --- a/packages/mdc-select/icon/index.js +++ b/packages/mdc-select/icon/index.ts @@ -22,47 +22,33 @@ */ import {MDCComponent} from '@material/base/component'; +import {MDCSelectIconFoundation} from './foundation'; -import MDCSelectIconAdapter from './adapter'; -import MDCSelectIconFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCSelectIcon extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCSelectIcon} - */ - static attachTo(root) { +class MDCSelectIcon extends MDCComponent { + static attachTo(root: Element): MDCSelectIcon { return new MDCSelectIcon(root); } - /** - * @return {!MDCSelectIconFoundation} - */ - get foundation() { + get foundation(): MDCSelectIconFoundation { return this.foundation_; } - /** - * @return {!MDCSelectIconFoundation} - */ - getDefaultFoundation() { - return new MDCSelectIconFoundation(/** @type {!MDCSelectIconAdapter} */ (Object.assign({ + getDefaultFoundation(): MDCSelectIconFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCSelectIconFoundation({ 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, handler) => this.root_.addEventListener(evtType, handler), deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), notifyIconAction: () => this.emit( - MDCSelectIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), - }))); + MDCSelectIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), + }); + // tslint:enable:object-literal-sort-keys } } -export {MDCSelectIcon, MDCSelectIconFoundation}; +export {MDCSelectIcon as default, MDCSelectIcon}; +export * from './adapter'; +export * from './foundation'; diff --git a/packages/mdc-select/index.js b/packages/mdc-select/index.js deleted file mode 100644 index 27514921e1f..00000000000 --- a/packages/mdc-select/index.js +++ /dev/null @@ -1,706 +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'; -import {MDCFloatingLabel} from '@material/floating-label/index'; -import {MDCLineRipple} from '@material/line-ripple/index'; -import {MDCMenu} from '@material/menu/index.ts'; -import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; -import {MDCNotchedOutline} from '@material/notched-outline/index'; -import MDCSelectFoundation from './foundation'; -import {cssClasses, strings} from './constants'; - -/* eslint-disable no-unused-vars */ -import {MDCSelectAdapter, FoundationMapType} from './adapter'; -import {MDCSelectIcon, MDCSelectIconFoundation} from './icon/index'; -import {MDCSelectHelperText, MDCSelectHelperTextFoundation} from './helper-text/index'; -/* eslint-enable no-unused-vars */ - -// Closure has issues with {this as that} syntax. -import * as menuSurfaceConstants from '@material/menu-surface/constants.ts'; -import * as menuConstants from '@material/menu/constants.ts'; - -const VALIDATION_ATTR_WHITELIST = ['required', 'aria-required']; - -/** - * @extends MDCComponent - */ -class MDCSelect extends MDCComponent { - /** - * @param {...?} args - */ - constructor(...args) { - super(...args); - /** @private {?Element} */ - this.nativeControl_; - /** @private {?Element} */ - this.selectedText_; - /** @private {?Element} */ - this.hiddenInput_; - /** @private {?MDCSelectIcon} */ - this.leadingIcon_; - /** @private {?MDCSelectHelperText} */ - this.helperText_; - /** @private {?Element} */ - this.menuElement_; - /** @type {?MDCMenu} */ - this.menu_; - /** @type {?MDCRipple} */ - this.ripple; - /** @private {?MDCLineRipple} */ - this.lineRipple_; - /** @private {?MDCFloatingLabel} */ - this.label_; - /** @private {?MDCNotchedOutline} */ - this.outline_; - /** @private {!Function} */ - this.handleChange_; - /** @private {!Function} */ - this.handleFocus_; - /** @private {!Function} */ - this.handleBlur_; - /** @private {!Function} */ - this.handleClick_; - /** @private {!Function} */ - this.handleKeydown_; - /** @private {!Function} */ - this.handleMenuOpened_; - /** @private {!Function} */ - this.handleMenuClosed_; - /** @private {!Function} */ - this.handleMenuSelected_; - /** @private {boolean} */ - this.menuOpened_ = false; - /** @private {!MutationObserver} */ - this.validationObserver_; - } - - /** - * @param {!Element} root - * @return {!MDCSelect} - */ - static attachTo(root) { - return new MDCSelect(root); - } - - /** - * @return {string} The value of the select. - */ - get value() { - return this.foundation_.getValue(); - } - - /** - * @param {string} value The value to set on the select. - */ - set value(value) { - this.foundation_.setValue(value); - } - - /** - * @return {number} The selected index of the select. - */ - get selectedIndex() { - let selectedIndex; - if (this.menuElement_) { - const selectedEl = /** @type {!HTMLElement} */ (this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)); - selectedIndex = this.menu_.items.indexOf(selectedEl); - } else { - selectedIndex = this.nativeControl_.selectedIndex; - } - return selectedIndex; - } - - /** - * @param {number} selectedIndex The index of the option to be set on the select. - */ - set selectedIndex(selectedIndex) { - this.foundation_.setSelectedIndex(selectedIndex); - } - - /** - * @return {boolean} True if the select is disabled. - */ - get disabled() { - return this.root_.classList.contains(cssClasses.DISABLED) || - (this.nativeControl_ ? this.nativeControl_.disabled : false); - } - - /** - * @param {boolean} disabled Sets the select disabled or enabled. - */ - set disabled(disabled) { - this.foundation_.setDisabled(disabled); - } - - /** - * 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 text content of the helper text. - * @param {string} content - */ - set helperTextContent(content) { - this.foundation_.setHelperTextContent(content); - } - - /** - * Sets the current invalid state of the select. - * @param {boolean} isValid - */ - set valid(isValid) { - this.foundation_.setValid(isValid); - } - - /** - * Checks if the select is in a valid state. - * @return {boolean} - */ - get valid() { - return this.foundation_.isValid(); - } - - /** - * Sets the control to the required state. - * @param {boolean} isRequired - */ - set required(isRequired) { - if (this.nativeControl_) { - this.nativeControl_.required = isRequired; - } else { - if (isRequired) { - this.selectedText_.setAttribute('aria-required', isRequired.toString()); - } else { - this.selectedText_.removeAttribute('aria-required'); - } - } - } - - /** - * Returns whether the select is required. - * @return {boolean} - */ - get required() { - if (this.nativeControl_) { - return this.nativeControl_.required; - } else { - return this.selectedText_.getAttribute('aria-required') === 'true'; - } - } - - /** - * Recomputes the outline SVG path for the outline element. - */ - layout() { - this.foundation_.layout(); - } - - - /** - * @param {(function(!Element): !MDCLineRipple)=} lineRippleFactory A function which creates a new MDCLineRipple. - * @param {(function(!Element): !MDCFloatingLabel)=} labelFactory A function which creates a new MDCFloatingLabel. - * @param {(function(!Element): !MDCNotchedOutline)=} outlineFactory A function which creates a new MDCNotchedOutline. - * @param {(function(!Element): !MDCMenu)=} menuFactory A function which creates a new MDCMenu. - * @param {(function(!Element): !MDCSelectIcon)=} iconFactory A function which creates a new MDCSelectIcon. - * @param {(function(!Element): !MDCSelectHelperText)=} helperTextFactory A function which creates a new - * MDCSelectHelperText. - */ - initialize( - labelFactory = (el) => new MDCFloatingLabel(el), - lineRippleFactory = (el) => new MDCLineRipple(el), - outlineFactory = (el) => new MDCNotchedOutline(el), - menuFactory = (el) => new MDCMenu(el), - iconFactory = (el) => new MDCSelectIcon(el), - helperTextFactory = (el) => new MDCSelectHelperText(el)) { - this.nativeControl_ = /** @type {HTMLElement} */ (this.root_.querySelector(strings.NATIVE_CONTROL_SELECTOR)); - this.selectedText_ = /** @type {HTMLElement} */ (this.root_.querySelector(strings.SELECTED_TEXT_SELECTOR)); - - if (this.selectedText_) { - this.enhancedSelectSetup_(menuFactory); - } - - 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); - } - - const leadingIcon = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); - if (leadingIcon) { - this.root_.classList.add(cssClasses.WITH_LEADING_ICON); - this.leadingIcon_ = iconFactory(leadingIcon); - - if (this.menuElement_) { - this.menuElement_.classList.add(cssClasses.WITH_LEADING_ICON); - } - } - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - if (element.hasAttribute(strings.ARIA_CONTROLS)) { - const helperTextElement = document.getElementById(element.getAttribute(strings.ARIA_CONTROLS)); - if (helperTextElement) { - this.helperText_ = helperTextFactory(helperTextElement); - } - } - - if (!this.root_.classList.contains(cssClasses.OUTLINED)) { - this.ripple = this.initRipple_(); - } - - // The required state needs to be sync'd before the mutation observer is added. - this.initialSyncRequiredState_(); - this.addMutationObserverForRequired_(); - } - - /** - * Handles setup for the enhanced menu. - * @private - */ - enhancedSelectSetup_(menuFactory) { - const isDisabled = this.root_.classList.contains(cssClasses.DISABLED); - this.selectedText_.setAttribute('tabindex', isDisabled ? '-1' : '0'); - this.hiddenInput_ = this.root_.querySelector(strings.HIDDEN_INPUT_SELECTOR); - this.menuElement_ = /** @type {HTMLElement} */ (this.root_.querySelector(strings.MENU_SELECTOR)); - this.menu_ = menuFactory(this.menuElement_); - this.menu_.hoistMenuToBody(); - this.menu_.setAnchorElement(this.root_); - this.menu_.setAnchorCorner(menuSurfaceConstants.Corner.BOTTOM_START); - this.menu_.wrapFocus = false; - } - - /** - * @private - * @return {!MDCRipple} - */ - initRipple_() { - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - const adapter = Object.assign(MDCRipple.createAdapter(this), { - registerInteractionHandler: (type, handler) => element.addEventListener(type, handler), - deregisterInteractionHandler: (type, handler) => element.removeEventListener(type, handler), - }); - const foundation = new MDCRippleFoundation(adapter); - return new MDCRipple(this.root_, foundation); - } - - /** - * Initializes the select's event listeners and internal state based - * on the environment's state. - */ - initialSyncWithDOM() { - this.handleChange_ = () => this.foundation_.handleChange(/* didChange */ true); - this.handleFocus_ = () => this.foundation_.handleFocus(); - this.handleBlur_ = () => this.foundation_.handleBlur(); - this.handleClick_ = (evt) => { - if (this.selectedText_) this.selectedText_.focus(); - this.foundation_.handleClick(this.getNormalizedXCoordinate_(evt)); - }; - this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); - this.handleMenuSelected_ = (evtData) => this.selectedIndex = evtData.detail.index; - this.handleMenuOpened_ = () => { - // Menu should open to the last selected element. - if (this.selectedIndex >= 0) { - this.menu_.items[this.selectedIndex].focus(); - } - }; - this.handleMenuClosed_ = () => { - // menuOpened_ is used to track the state of the menu opening or closing since the menu.open function - // will return false if the menu is still closing and this method listens to the closed event which - // occurs after the menu is already closed. - this.menuOpened_ = false; - this.selectedText_.removeAttribute('aria-expanded'); - if (document.activeElement !== this.selectedText_) { - this.foundation_.handleBlur(); - } - }; - - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - - element.addEventListener('change', this.handleChange_); - element.addEventListener('focus', this.handleFocus_); - element.addEventListener('blur', this.handleBlur_); - - ['mousedown', 'touchstart'].forEach((evtType) => { - element.addEventListener(evtType, this.handleClick_); - }); - - if (this.menuElement_) { - this.selectedText_.addEventListener('keydown', this.handleKeydown_); - this.menu_.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); - this.menu_.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); - this.menu_.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); - - if (this.hiddenInput_ && this.hiddenInput_.value) { - // If the hidden input already has a value, use it to restore the select's value. - // This can happen e.g. if the user goes back or (in some browsers) refreshes the page. - const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); - enhancedAdapterMethods.setValue(this.hiddenInput_.value); - } else if (this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)) { - // If an element is selected, the select should set the initial selected text. - const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); - enhancedAdapterMethods.setValue(enhancedAdapterMethods.getValue()); - } - } - - // Initially sync floating label - this.foundation_.handleChange(/* didChange */ false); - - if (this.root_.classList.contains(cssClasses.DISABLED) - || (this.nativeControl_ && this.nativeControl_.disabled)) { - this.disabled = true; - } - } - - destroy() { - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - - element.removeEventListener('change', this.handleChange_); - element.removeEventListener('focus', this.handleFocus_); - element.removeEventListener('blur', this.handleBlur_); - element.removeEventListener('keydown', this.handleKeydown_); - ['mousedown', 'touchstart'].forEach((evtType) => { - element.removeEventListener(evtType, this.handleClick_); - }); - - if (this.menu_) { - this.menu_.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); - this.menu_.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); - this.menu_.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); - this.menu_.destroy(); - } - - if (this.ripple) { - this.ripple.destroy(); - } - if (this.outline_) { - this.outline_.destroy(); - } - if (this.leadingIcon_) { - this.leadingIcon_.destroy(); - } - if (this.helperText_) { - this.helperText_.destroy(); - } - if (this.validationObserver_) { - this.validationObserver_.disconnect(); - } - - super.destroy(); - } - - /** - * @return {!MDCSelectFoundation} - */ - getDefaultFoundation() { - return new MDCSelectFoundation( - /** @type {!MDCSelectAdapter} */ (Object.assign( - this.nativeControl_ ? this.getNativeSelectAdapterMethods_() : this.getEnhancedSelectAdapterMethods_(), - this.getCommonAdapterMethods_(), - this.getOutlineAdapterMethods_(), - this.getLabelAdapterMethods_()) - ), - this.getFoundationMap_() - ); - } - - /** - * @return {!{ - * getValue: function(): string, - * setValue: function(string): string, - * openMenu: function(): void, - * closeMenu: function(): void, - * isMenuOpen: function(): boolean, - * setSelectedIndex: function(number): void, - * setDisabled: function(boolean): void - * }} - * @private - */ - getNativeSelectAdapterMethods_() { - return { - getValue: () => this.nativeControl_.value, - setValue: (value) => this.nativeControl_.value = value, - openMenu: () => {}, - closeMenu: () => {}, - isMenuOpen: () => false, - setSelectedIndex: (index) => { - this.nativeControl_.selectedIndex = index; - }, - setDisabled: (isDisabled) => this.nativeControl_.disabled = isDisabled, - setValid: (isValid) => { - isValid ? this.root_.classList.remove(cssClasses.INVALID) : this.root_.classList.add(cssClasses.INVALID); - }, - checkValidity: () => this.nativeControl_.checkValidity(), - }; - } - - /** - * @return {!{ - * getValue: function(): string, - * setValue: function(string): string, - * openMenu: function(): void, - * closeMenu: function(): void, - * isMenuOpen: function(): boolean, - * setSelectedIndex: function(number): void, - * setDisabled: function(boolean): void - * }} - * @private - */ - getEnhancedSelectAdapterMethods_() { - return { - getValue: () => { - const listItem = this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR); - if (listItem && listItem.hasAttribute(strings.ENHANCED_VALUE_ATTR)) { - return listItem.getAttribute(strings.ENHANCED_VALUE_ATTR); - } - return ''; - }, - setValue: (value) => { - const element = - /** @type {HTMLElement} */ (this.menuElement_.querySelector(`[${strings.ENHANCED_VALUE_ATTR}="${value}"]`)); - this.setEnhancedSelectedIndex_(element ? this.menu_.items.indexOf(element) : -1); - }, - openMenu: () => { - if (this.menu_ && !this.menu_.open) { - this.menu_.open = true; - this.menuOpened_ = true; - this.selectedText_.setAttribute('aria-expanded', 'true'); - } - }, - closeMenu: () => { - if (this.menu_ && this.menu_.open) { - this.menu_.open = false; - } - }, - isMenuOpen: () => this.menu_ && this.menuOpened_, - setSelectedIndex: (index) => { - this.setEnhancedSelectedIndex_(index); - }, - setDisabled: (isDisabled) => { - this.selectedText_.setAttribute('tabindex', isDisabled ? '-1' : '0'); - this.selectedText_.setAttribute('aria-disabled', isDisabled.toString()); - if (this.hiddenInput_) { - this.hiddenInput_.disabled = isDisabled; - } - }, - checkValidity: () => { - const classList = this.root_.classList; - if (classList.contains(cssClasses.REQUIRED) && !classList.contains(cssClasses.DISABLED)) { - // See notes for required attribute under https://www.w3.org/TR/html52/sec-forms.html#the-select-element - // TL;DR: Invalid if no index is selected, or if the first index is selected and has an empty value. - return this.selectedIndex !== -1 && (this.selectedIndex !== 0 || this.value); - } else { - return true; - } - }, - setValid: (isValid) => { - this.selectedText_.setAttribute('aria-invalid', (!isValid).toString()); - isValid ? this.root_.classList.remove(cssClasses.INVALID) : this.root_.classList.add(cssClasses.INVALID); - }, - }; - } - - /** - * @return {!{ - * addClass: function(string): void, - * removeClass: function(string): void, - * hasClass: function(string): void, - * setRippleCenter: function(number): void, - * activateBottomLine: function(): void, - * deactivateBottomLine: function(): void, - * notifyChange: function(string): void - * }} - * @private - */ - getCommonAdapterMethods_() { - return { - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - setRippleCenter: (normalizedX) => this.lineRipple_ && this.lineRipple_.setRippleCenter(normalizedX), - activateBottomLine: () => this.lineRipple_ && this.lineRipple_.activate(), - deactivateBottomLine: () => this.lineRipple_ && this.lineRipple_.deactivate(), - notifyChange: (value) => { - const index = this.selectedIndex; - this.emit(strings.CHANGE_EVENT, {value, index}, true /* shouldBubble */); - }, - }; - } - - /** - * @return {!{ - * hasOutline: function(): boolean, - * notchOutline: function(number, boolean): undefined, - * closeOutline: function(): undefined, - * }} - */ - getOutlineAdapterMethods_() { - return { - hasOutline: () => !!this.outline_, - notchOutline: (labelWidth) => { - if (this.outline_) { - this.outline_.notch(labelWidth); - } - }, - closeOutline: () => { - if (this.outline_) { - this.outline_.closeNotch(); - } - }, - }; - } - - /** - * @return {!{ - * floatLabel: function(boolean): undefined, - * getLabelWidth: function(): number, - * }} - */ - getLabelAdapterMethods_() { - return { - floatLabel: (shouldFloat) => { - if (this.label_) { - this.label_.float(shouldFloat); - } - }, - getLabelWidth: () => { - return this.label_ ? this.label_.getWidth() : 0; - }, - }; - } - - /** - * Calculates where the line ripple should start based on the x coordinate within the component. - * @param {!(MouseEvent|TouchEvent)} evt - * @return {number} normalizedX - */ - getNormalizedXCoordinate_(evt) { - const targetClientRect = evt.target.getBoundingClientRect(); - const xCoordinate = evt.clientX; - return xCoordinate - targetClientRect.left; - } - - /** - * Returns a map of all subcomponents to subfoundations. - * @return {!FoundationMapType} - */ - getFoundationMap_() { - return { - leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, - helperText: this.helperText_ ? this.helperText_.foundation : undefined, - }; - } - - /** - * Sets the selected index of the enhanced menu. - * @param {number} index - * @private - */ - setEnhancedSelectedIndex_(index) { - const selectedItem = this.menu_.items[index]; - this.selectedText_.textContent = selectedItem ? selectedItem.textContent.trim() : ''; - const previouslySelected = this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR); - - if (previouslySelected) { - previouslySelected.classList.remove(cssClasses.SELECTED_ITEM_CLASS); - previouslySelected.removeAttribute(strings.ARIA_SELECTED_ATTR); - } - - if (selectedItem) { - selectedItem.classList.add(cssClasses.SELECTED_ITEM_CLASS); - selectedItem.setAttribute(strings.ARIA_SELECTED_ATTR, 'true'); - } - - // Synchronize hidden input's value with data-value attribute of selected item. - // This code path is also followed when setting value directly, so this covers all cases. - if (this.hiddenInput_) { - this.hiddenInput_.value = selectedItem ? selectedItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || '' : ''; - } - - this.layout(); - } - - initialSyncRequiredState_() { - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - const isRequired = element.required || element.getAttribute('aria-required') === 'true' - || this.root_.classList.contains(cssClasses.REQUIRED); - if (isRequired) { - if (this.nativeControl_) { - this.nativeControl_.required = true; - } else { - this.selectedText_.setAttribute('aria-required', 'true'); - } - this.root_.classList.add(cssClasses.REQUIRED); - } - } - - addMutationObserverForRequired_() { - const observerHandler = (attributesList) => { - attributesList.some((attributeName) => { - if (VALIDATION_ATTR_WHITELIST.indexOf(attributeName) > -1) { - if (this.selectedText_) { - if (this.selectedText_.getAttribute('aria-required') === 'true') { - this.root_.classList.add(cssClasses.REQUIRED); - } else { - this.root_.classList.remove(cssClasses.REQUIRED); - } - } else { - if (this.nativeControl_.required) { - this.root_.classList.add(cssClasses.REQUIRED); - } else { - this.root_.classList.remove(cssClasses.REQUIRED); - } - } - return true; - } - }); - }; - - const getAttributesList = (mutationsList) => mutationsList.map((mutation) => mutation.attributeName); - const observer = new MutationObserver((mutationsList) => observerHandler(getAttributesList(mutationsList))); - const element = this.nativeControl_ ? this.nativeControl_ : this.selectedText_; - observer.observe(element, {attributes: true}); - this.validationObserver_ = observer; - }; -} - -export {MDCSelect, MDCSelectFoundation, - MDCSelectHelperText, MDCSelectHelperTextFoundation, - MDCSelectIcon, MDCSelectIconFoundation}; diff --git a/packages/mdc-select/index.ts b/packages/mdc-select/index.ts new file mode 100644 index 00000000000..a69222aab70 --- /dev/null +++ b/packages/mdc-select/index.ts @@ -0,0 +1,603 @@ +/** + * @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 {CustomEventListener, MDCComponent} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/index'; +import {MDCFloatingLabel} from '@material/floating-label/index'; +import {MDCLineRipple} from '@material/line-ripple/index'; +import * as menuSurfaceConstants from '@material/menu-surface/constants'; +import * as menuConstants from '@material/menu/constants'; +import {MDCMenu, MenuItemEvent} from '@material/menu/index'; +import {MDCNotchedOutline} from '@material/notched-outline/index'; +import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; +import {cssClasses, strings} from './constants'; +import {MDCSelectFoundation} from './foundation'; +import {MDCSelectHelperText} from './helper-text'; +import {MDCSelectIcon} from './icon'; +import { + FoundationMapType, + HelperTextFactory, IconFactory, + LabelFactory, + LineRippleFactory, MenuFactory, + OutlineFactory, SelectEventDetail, +} from './types'; + +type PointerEventType = 'mousedown' | 'touchstart'; + +const POINTER_EVENTS: PointerEventType[] = ['mousedown', 'touchstart']; +const VALIDATION_ATTR_WHITELIST = ['required', 'aria-required']; + +class MDCSelect extends MDCComponent implements RippleCapableSurface { + static attachTo(root: Element): MDCSelect { + return new MDCSelect(root); + } + + // Public visibility for this property is required by RippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + + private menu_!: MDCMenu | null; // assigned in enhancedSelectSetup_() + private menuOpened_ = false; + + // Exactly one of these fields must be non-null. + private nativeControl_!: HTMLSelectElement | null; // assigned in initialize() + private selectedText_!: HTMLElement | null; // assigned in initialize() + + private targetElement_!: HTMLElement; // assigned in initialize() + + private hiddenInput_!: HTMLInputElement | null; + private leadingIcon_?: MDCSelectIcon; + private helperText_!: MDCSelectHelperText | null; + private menuElement_!: Element | null; + private ripple!: MDCRipple | null; + private lineRipple_!: MDCLineRipple | null; + private label_!: MDCFloatingLabel | null; + private outline_!: MDCNotchedOutline | null; + private handleChange_!: SpecificEventListener<'change'>; // assigned in initialize() + private handleFocus_!: SpecificEventListener<'focus'>; // assigned in initialize() + private handleBlur_!: SpecificEventListener<'blur'>; // assigned in initialize() + private handleClick_!: SpecificEventListener; // assigned in initialize() + private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialize() + private handleMenuOpened_!: EventListener; // assigned in initialize() + private handleMenuClosed_!: EventListener; // assigned in initialize() + private handleMenuSelected_!: CustomEventListener; // assigned in initialize() + private validationObserver_!: MutationObserver; // assigned in initialize() + + initialize( + labelFactory: LabelFactory = (el) => new MDCFloatingLabel(el), + lineRippleFactory: LineRippleFactory = (el) => new MDCLineRipple(el), + outlineFactory: OutlineFactory = (el) => new MDCNotchedOutline(el), + menuFactory: MenuFactory = (el) => new MDCMenu(el), + iconFactory: IconFactory = (el) => new MDCSelectIcon(el), + helperTextFactory: HelperTextFactory = (el) => new MDCSelectHelperText(el), + ) { + this.nativeControl_ = this.root_.querySelector(strings.NATIVE_CONTROL_SELECTOR); + this.selectedText_ = this.root_.querySelector(strings.SELECTED_TEXT_SELECTOR); + + const targetElement = this.nativeControl_ || this.selectedText_; + if (!targetElement) { + throw new Error( + 'MDCSelect: Missing required element: Exactly one of the following selectors must be present: ' + + `'${strings.NATIVE_CONTROL_SELECTOR}' or '${strings.SELECTED_TEXT_SELECTOR}'`, + ); + } + + this.targetElement_ = targetElement; + if (this.targetElement_.hasAttribute(strings.ARIA_CONTROLS)) { + const helperTextElement = document.getElementById(this.targetElement_.getAttribute(strings.ARIA_CONTROLS)!); + if (helperTextElement) { + this.helperText_ = helperTextFactory(helperTextElement); + } + } + + if (this.selectedText_) { + this.enhancedSelectSetup_(menuFactory); + } + + 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; + + const leadingIcon = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); + if (leadingIcon) { + this.root_.classList.add(cssClasses.WITH_LEADING_ICON); + this.leadingIcon_ = iconFactory(leadingIcon); + + if (this.menuElement_) { + this.menuElement_.classList.add(cssClasses.WITH_LEADING_ICON); + } + } + + if (!this.root_.classList.contains(cssClasses.OUTLINED)) { + this.ripple = this.initRipple_(); + } + + // The required state needs to be sync'd before the mutation observer is added. + this.initialSyncRequiredState_(); + this.addMutationObserverForRequired_(); + } + + /** + * Initializes the select's event listeners and internal state based + * on the environment's state. + */ + initialSyncWithDOM() { + this.handleChange_ = () => this.foundation_.handleChange(/* didChange */ true); + this.handleFocus_ = () => this.foundation_.handleFocus(); + this.handleBlur_ = () => this.foundation_.handleBlur(); + this.handleClick_ = (evt) => { + if (this.selectedText_) { + this.selectedText_.focus(); + } + this.foundation_.handleClick(this.getNormalizedXCoordinate_(evt)); + }; + this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); + this.handleMenuSelected_ = (evtData) => this.selectedIndex = evtData.detail.index; + this.handleMenuOpened_ = () => { + // Menu should open to the last selected element. + if (this.selectedIndex >= 0) { + const selectedItemEl = this.menu_!.items[this.selectedIndex] as HTMLElement; + selectedItemEl.focus(); + } + }; + this.handleMenuClosed_ = () => { + // menuOpened_ is used to track the state of the menu opening or closing since the menu.open function + // will return false if the menu is still closing and this method listens to the closed event which + // occurs after the menu is already closed. + this.menuOpened_ = false; + this.selectedText_!.removeAttribute('aria-expanded'); + if (document.activeElement !== this.selectedText_) { + this.foundation_.handleBlur(); + } + }; + + this.targetElement_.addEventListener('change', this.handleChange_); + this.targetElement_.addEventListener('focus', this.handleFocus_); + this.targetElement_.addEventListener('blur', this.handleBlur_); + + POINTER_EVENTS.forEach((evtType) => { + this.targetElement_.addEventListener(evtType, this.handleClick_ as EventListener); + }); + + if (this.menuElement_) { + this.selectedText_!.addEventListener('keydown', this.handleKeydown_); + this.menu_!.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); + this.menu_!.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); + this.menu_!.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); + + if (this.hiddenInput_ && this.hiddenInput_.value) { + // If the hidden input already has a value, use it to restore the select's value. + // This can happen e.g. if the user goes back or (in some browsers) refreshes the page. + const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); + enhancedAdapterMethods.setValue(this.hiddenInput_.value); + } else if (this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)) { + // If an element is selected, the select should set the initial selected text. + const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); + enhancedAdapterMethods.setValue(enhancedAdapterMethods.getValue()); + } + } + + // Initially sync floating label + this.foundation_.handleChange(/* didChange */ false); + + if (this.root_.classList.contains(cssClasses.DISABLED) + || (this.nativeControl_ && this.nativeControl_.disabled)) { + this.disabled = true; + } + } + + destroy() { + this.targetElement_.removeEventListener('change', this.handleChange_); + this.targetElement_.removeEventListener('focus', this.handleFocus_); + this.targetElement_.removeEventListener('blur', this.handleBlur_); + this.targetElement_.removeEventListener('keydown', this.handleKeydown_); + POINTER_EVENTS.forEach((evtType) => { + this.targetElement_.removeEventListener(evtType, this.handleClick_ as EventListener); + }); + + if (this.menu_) { + this.menu_.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); + this.menu_.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); + this.menu_.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); + this.menu_.destroy(); + } + + if (this.ripple) { + this.ripple.destroy(); + } + if (this.outline_) { + this.outline_.destroy(); + } + if (this.leadingIcon_) { + this.leadingIcon_.destroy(); + } + if (this.helperText_) { + this.helperText_.destroy(); + } + if (this.validationObserver_) { + this.validationObserver_.disconnect(); + } + + super.destroy(); + } + + get value(): string { + return this.foundation_.getValue(); + } + + set value(value: string) { + this.foundation_.setValue(value); + } + + get selectedIndex(): number { + let selectedIndex = -1; + if (this.menuElement_ && this.menu_) { + const selectedEl = this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)!; + selectedIndex = this.menu_.items.indexOf(selectedEl); + } else if (this.nativeControl_) { + selectedIndex = this.nativeControl_.selectedIndex; + } + return selectedIndex; + } + + set selectedIndex(selectedIndex: number) { + this.foundation_.setSelectedIndex(selectedIndex); + } + + get disabled(): boolean { + return this.root_.classList.contains(cssClasses.DISABLED) || + (this.nativeControl_ ? this.nativeControl_.disabled : false); + } + + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + 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 text content of the helper text. + */ + set helperTextContent(content: string) { + this.foundation_.setHelperTextContent(content); + } + + /** + * Sets the current invalid state of the select. + */ + set valid(isValid: boolean) { + this.foundation_.setValid(isValid); + } + + /** + * Checks if the select is in a valid state. + */ + get valid(): boolean { + return this.foundation_.isValid(); + } + + /** + * Sets the control to the required state. + */ + set required(isRequired: boolean) { + if (this.nativeControl_) { + this.nativeControl_.required = isRequired; + } else { + if (isRequired) { + this.selectedText_!.setAttribute('aria-required', isRequired.toString()); + } else { + this.selectedText_!.removeAttribute('aria-required'); + } + } + } + + /** + * Returns whether the select is required. + */ + get required(): boolean { + if (this.nativeControl_) { + return this.nativeControl_.required; + } else { + return this.selectedText_!.getAttribute('aria-required') === 'true'; + } + } + + /** + * Recomputes the outline SVG path for the outline element. + */ + layout() { + this.foundation_.layout(); + } + + getDefaultFoundation(): MDCSelectFoundation { + return new MDCSelectFoundation({ + ...(this.nativeControl_ ? this.getNativeSelectAdapterMethods_() : this.getEnhancedSelectAdapterMethods_()), + ...this.getCommonAdapterMethods_(), + ...this.getOutlineAdapterMethods_(), + ...this.getLabelAdapterMethods_(), + }, + this.getFoundationMap_(), + ); + } + + /** + * Handles setup for the enhanced menu. + */ + private enhancedSelectSetup_(menuFactory: MenuFactory) { + const isDisabled = this.root_.classList.contains(cssClasses.DISABLED); + this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); + this.hiddenInput_ = this.root_.querySelector(strings.HIDDEN_INPUT_SELECTOR); + this.menuElement_ = this.root_.querySelector(strings.MENU_SELECTOR)!; + this.menu_ = menuFactory(this.menuElement_); + this.menu_.hoistMenuToBody(); + this.menu_.setAnchorElement(this.root_); + this.menu_.setAnchorCorner(menuSurfaceConstants.Corner.BOTTOM_START); + this.menu_.wrapFocus = false; + } + + private initRipple_(): MDCRipple { + // tslint:disable:object-literal-sort-keys + const foundation = new MDCRippleFoundation({ + ...MDCRipple.createAdapter(this), + registerInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.targetElement_.addEventListener(evtType, handler); + }, + deregisterInteractionHandler: (evtType: E, handler: SpecificEventListener) => { + this.targetElement_.removeEventListener(evtType, handler); + }, + }); + // tslint:enable:object-literal-sort-keys + + return new MDCRipple(this.root_, foundation); + } + + private getNativeSelectAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + getValue: () => this.nativeControl_!.value, + setValue: (value: string) => { this.nativeControl_!.value = value; }, + openMenu: () => undefined, + closeMenu: () => undefined, + isMenuOpen: () => false, + setSelectedIndex: (index: number) => { this.nativeControl_!.selectedIndex = index; }, + setDisabled: (isDisabled: boolean) => { this.nativeControl_!.disabled = isDisabled; }, + setValid: (isValid: boolean) => { + if (isValid) { + this.root_.classList.remove(cssClasses.INVALID); + } else { + this.root_.classList.add(cssClasses.INVALID); + } + }, + checkValidity: () => this.nativeControl_!.checkValidity(), + }; + // tslint:enable:object-literal-sort-keys + } + + private getEnhancedSelectAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + getValue: () => { + const listItem = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); + if (listItem && listItem.hasAttribute(strings.ENHANCED_VALUE_ATTR)) { + return listItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || ''; + } + return ''; + }, + setValue: (value: string) => { + const element = this.menuElement_!.querySelector(`[${strings.ENHANCED_VALUE_ATTR}="${value}"]`); + this.setEnhancedSelectedIndex_(element ? this.menu_!.items.indexOf(element) : -1); + }, + openMenu: () => { + if (this.menu_ && !this.menu_.open) { + this.menu_.open = true; + this.menuOpened_ = true; + this.selectedText_!.setAttribute('aria-expanded', 'true'); + } + }, + closeMenu: () => { + if (this.menu_ && this.menu_.open) { + this.menu_.open = false; + } + }, + isMenuOpen: () => Boolean(this.menu_) && this.menuOpened_, + setSelectedIndex: (index: number) => this.setEnhancedSelectedIndex_(index), + setDisabled: (isDisabled: boolean) => { + this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); + this.selectedText_!.setAttribute('aria-disabled', isDisabled.toString()); + if (this.hiddenInput_) { + this.hiddenInput_.disabled = isDisabled; + } + }, + checkValidity: () => { + const classList = this.root_.classList; + if (classList.contains(cssClasses.REQUIRED) && !classList.contains(cssClasses.DISABLED)) { + // See notes for required attribute under https://www.w3.org/TR/html52/sec-forms.html#the-select-element + // TL;DR: Invalid if no index is selected, or if the first index is selected and has an empty value. + return this.selectedIndex !== -1 && (this.selectedIndex !== 0 || Boolean(this.value)); + } else { + return true; + } + }, + setValid: (isValid: boolean) => { + this.selectedText_!.setAttribute('aria-invalid', (!isValid).toString()); + if (isValid) { + this.root_.classList.remove(cssClasses.INVALID); + } else { + this.root_.classList.add(cssClasses.INVALID); + } + }, + }; + // tslint:enable:object-literal-sort-keys + } + + private getCommonAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + addClass: (className: string) => this.root_.classList.add(className), + removeClass: (className: string) => this.root_.classList.remove(className), + hasClass: (className: string) => this.root_.classList.contains(className), + setRippleCenter: (normalizedX: number) => this.lineRipple_ && this.lineRipple_.setRippleCenter(normalizedX), + activateBottomLine: () => this.lineRipple_ && this.lineRipple_.activate(), + deactivateBottomLine: () => this.lineRipple_ && this.lineRipple_.deactivate(), + notifyChange: (value: string) => { + const index = this.selectedIndex; + this.emit(strings.CHANGE_EVENT, {value, index}, true /* shouldBubble */); + }, + }; + // tslint:enable:object-literal-sort-keys + } + + private getOutlineAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + hasOutline: () => Boolean(this.outline_), + notchOutline: (labelWidth: number) => this.outline_ && this.outline_.notch(labelWidth), + closeOutline: () => this.outline_ && this.outline_.closeNotch(), + }; + // tslint:enable:object-literal-sort-keys + } + + private getLabelAdapterMethods_() { + return { + floatLabel: (shouldFloat: boolean) => this.label_ && this.label_.float(shouldFloat), + getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, + }; + } + + /** + * Calculates where the line ripple should start based on the x coordinate within the component. + */ + private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number { + const targetClientRect = (evt.target as Element).getBoundingClientRect(); + const xCoordinate = this.isTouchEvent_(evt) ? evt.touches[0].clientX : evt.clientX; + return xCoordinate - targetClientRect.left; + } + + private isTouchEvent_(evt: MouseEvent | TouchEvent): evt is TouchEvent { + return Boolean((evt as TouchEvent).touches); + } + + /** + * Returns a map of all subcomponents to subfoundations. + */ + private getFoundationMap_(): Partial { + return { + helperText: this.helperText_ ? this.helperText_.foundation : undefined, + leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, + }; + } + + private setEnhancedSelectedIndex_(index: number) { + const selectedItem = this.menu_!.items[index]; + this.selectedText_!.textContent = selectedItem ? selectedItem.textContent!.trim() : ''; + const previouslySelected = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); + + if (previouslySelected) { + previouslySelected.classList.remove(cssClasses.SELECTED_ITEM_CLASS); + previouslySelected.removeAttribute(strings.ARIA_SELECTED_ATTR); + } + + if (selectedItem) { + selectedItem.classList.add(cssClasses.SELECTED_ITEM_CLASS); + selectedItem.setAttribute(strings.ARIA_SELECTED_ATTR, 'true'); + } + + // Synchronize hidden input's value with data-value attribute of selected item. + // This code path is also followed when setting value directly, so this covers all cases. + if (this.hiddenInput_) { + this.hiddenInput_.value = selectedItem ? selectedItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || '' : ''; + } + + this.layout(); + } + + private initialSyncRequiredState_() { + const isRequired = + (this.targetElement_ as HTMLSelectElement).required + || this.targetElement_.getAttribute('aria-required') === 'true' + || this.root_.classList.contains(cssClasses.REQUIRED); + if (isRequired) { + if (this.nativeControl_) { + this.nativeControl_.required = true; + } else { + this.selectedText_!.setAttribute('aria-required', 'true'); + } + this.root_.classList.add(cssClasses.REQUIRED); + } + } + + private addMutationObserverForRequired_() { + const observerHandler = (attributesList: string[]) => { + attributesList.some((attributeName) => { + if (VALIDATION_ATTR_WHITELIST.indexOf(attributeName) === -1) { + return false; + } + + if (this.selectedText_) { + if (this.selectedText_.getAttribute('aria-required') === 'true') { + this.root_.classList.add(cssClasses.REQUIRED); + } else { + this.root_.classList.remove(cssClasses.REQUIRED); + } + } else { + if (this.nativeControl_!.required) { + this.root_.classList.add(cssClasses.REQUIRED); + } else { + this.root_.classList.remove(cssClasses.REQUIRED); + } + } + + return true; + }); + }; + + const getAttributesList = (mutationsList: MutationRecord[]): string[] => { + return mutationsList + .map((mutation) => mutation.attributeName) + .filter((attributeName) => attributeName) as string[]; + }; + const observer = new MutationObserver((mutationsList) => observerHandler(getAttributesList(mutationsList))); + observer.observe(this.targetElement_, {attributes: true}); + this.validationObserver_ = observer; + } +} + +export {MDCSelect as default, MDCSelect}; +export * from './helper-text'; +export * from './icon'; +export * from './adapter'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-select/types.ts b/packages/mdc-select/types.ts new file mode 100644 index 00000000000..0d031c687a2 --- /dev/null +++ b/packages/mdc-select/types.ts @@ -0,0 +1,48 @@ +/** + * @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 {MDCMenu} from '@material/menu/index'; +import {MDCNotchedOutline} from '@material/notched-outline/index'; +import {MDCSelectHelperText, MDCSelectHelperTextFoundation} from './helper-text/index'; +import {MDCSelectIcon, MDCSelectIconFoundation} from './icon/index'; + +export interface FoundationMapType { + leadingIcon: MDCSelectIconFoundation; + helperText: MDCSelectHelperTextFoundation; +} + +export type SelectEvent = CustomEvent; + +export interface SelectEventDetail { + value: string; + index: number; +} + +export type LineRippleFactory = (el: Element) => MDCLineRipple; +export type HelperTextFactory = (el: Element) => MDCSelectHelperText; +export type MenuFactory = (el: Element) => MDCMenu; +export type IconFactory = (el: Element) => MDCSelectIcon; +export type LabelFactory = (el: Element) => MDCFloatingLabel; +export type OutlineFactory = (el: Element) => MDCNotchedOutline; diff --git a/packages/mdc-tab-bar/adapter.js b/packages/mdc-tab-bar/adapter.ts similarity index 54% rename from packages/mdc-tab-bar/adapter.js rename to packages/mdc-tab-bar/adapter.ts index a24e8b10a6c..eb648f3d3f3 100644 --- a/packages/mdc-tab-bar/adapter.js +++ b/packages/mdc-tab-bar/adapter.ts @@ -21,128 +21,111 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - -/* eslint-disable no-unused-vars */ -import {MDCTabDimensions} from '@material/tab/adapter'; -/* eslint-enable no-unused-vars */ +import {MDCTabDimensions} from '@material/tab/types'; /** - * Adapter for MDC Tab Bar. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Tab Bar 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 MDCTabBarAdapter { +interface MDCTabBarAdapter { /** * Scrolls to the given position - * @param {number} scrollX The position to scroll to + * @param scrollX The position to scroll to */ - scrollTo(scrollX) {} + scrollTo(scrollX: number): void; /** * Increments the current scroll position by the given amount - * @param {number} scrollXIncrement The amount to increment scroll + * @param scrollXIncrement The amount to increment scroll */ - incrementScroll(scrollXIncrement) {} + incrementScroll(scrollXIncrement: number): void; /** * Returns the current scroll position - * @return {number} */ - getScrollPosition() {} + getScrollPosition(): number; /** * Returns the width of the scroll content - * @return {number} */ - getScrollContentWidth() {} + getScrollContentWidth(): number; /** * Returns the root element's offsetWidth - * @return {number} */ - getOffsetWidth() {} + getOffsetWidth(): number; /** * Returns if the Tab Bar language direction is RTL - * @return {boolean} */ - isRTL() {} + isRTL(): boolean; /** * Sets the tab at the given index to be activated - * @param {number} index The index of the tab to activate + * @param index The index of the tab to activate */ - setActiveTab(index) {} + setActiveTab(index: number): void; /** * Activates the tab at the given index with the given client rect - * @param {number} index The index of the tab to activate - * @param {!ClientRect} clientRect The client rect of the previously active Tab Indicator + * @param index The index of the tab to activate + * @param clientRect The client rect of the previously active Tab Indicator */ - activateTabAtIndex(index, clientRect) {} + activateTabAtIndex(index: number, clientRect: ClientRect): void; /** * Deactivates the tab at the given index - * @param {number} index The index of the tab to deactivate + * @param index The index of the tab to deactivate */ - deactivateTabAtIndex(index) {} + deactivateTabAtIndex(index: number): void; /** * Focuses the tab at the given index - * @param {number} index The index of the tab to focus + * @param index The index of the tab to focus */ - focusTabAtIndex(index) {} + focusTabAtIndex(index: number): void; /** * Returns the client rect of the tab's indicator - * @param {number} index The index of the tab - * @return {!ClientRect} + * @param index The index of the tab */ - getTabIndicatorClientRectAtIndex(index) {} + getTabIndicatorClientRectAtIndex(index: number): ClientRect; /** * Returns the tab dimensions of the tab at the given index - * @param {number} index The index of the tab - * @return {!MDCTabDimensions} + * @param index The index of the tab */ - getTabDimensionsAtIndex(index) {} + getTabDimensionsAtIndex(index: number): MDCTabDimensions; /** * Returns the length of the tab list - * @return {number} */ - getTabListLength() {} + getTabListLength(): number; /** * Returns the index of the previously active tab - * @return {number} */ - getPreviousActiveTabIndex() {} + getPreviousActiveTabIndex(): number; /** * Returns the index of the focused tab - * @return {number} */ - getFocusedTabIndex() {} + getFocusedTabIndex(): number; /** * Returns the index of the given tab - * @param {string} id The ID of the tab whose index to determine - * @return {number} + * @param id The ID of the tab whose index to determine */ - getIndexOfTabById(id) {} + getIndexOfTabById(id: string): number; /** * Emits the MDCTabBar:activated event - * @param {number} index The index of the activated tab + * @param index The index of the activated tab */ - notifyTabActivated(index) {} + notifyTabActivated(index: number): void; } -export default MDCTabBarAdapter; +export {MDCTabBarAdapter as default, MDCTabBarAdapter}; diff --git a/packages/mdc-tab-bar/constants.js b/packages/mdc-tab-bar/constants.ts similarity index 95% rename from packages/mdc-tab-bar/constants.js rename to packages/mdc-tab-bar/constants.ts index c0ec5d780c4..7d63aca67a1 100644 --- a/packages/mdc-tab-bar/constants.js +++ b/packages/mdc-tab-bar/constants.ts @@ -21,31 +21,26 @@ * THE SOFTWARE. */ -/** @enum {string} */ const strings = { - TAB_ACTIVATED_EVENT: 'MDCTabBar:activated', - TAB_SCROLLER_SELECTOR: '.mdc-tab-scroller', - TAB_SELECTOR: '.mdc-tab', ARROW_LEFT_KEY: 'ArrowLeft', ARROW_RIGHT_KEY: 'ArrowRight', END_KEY: 'End', - HOME_KEY: 'Home', ENTER_KEY: 'Enter', + HOME_KEY: 'Home', SPACE_KEY: 'Space', + TAB_ACTIVATED_EVENT: 'MDCTabBar:activated', + TAB_SCROLLER_SELECTOR: '.mdc-tab-scroller', + TAB_SELECTOR: '.mdc-tab', }; -/** @enum {number} */ const numbers = { - EXTRA_SCROLL_AMOUNT: 20, ARROW_LEFT_KEYCODE: 37, ARROW_RIGHT_KEYCODE: 39, END_KEYCODE: 35, - HOME_KEYCODE: 36, ENTER_KEYCODE: 13, + EXTRA_SCROLL_AMOUNT: 20, + HOME_KEYCODE: 36, SPACE_KEYCODE: 32, }; -export { - numbers, - strings, -}; +export {numbers, strings}; diff --git a/packages/mdc-tab-bar/foundation.js b/packages/mdc-tab-bar/foundation.ts similarity index 71% rename from packages/mdc-tab-bar/foundation.js rename to packages/mdc-tab-bar/foundation.ts index 950bf4f0e39..624b87c53b6 100644 --- a/packages/mdc-tab-bar/foundation.js +++ b/packages/mdc-tab-bar/foundation.ts @@ -22,18 +22,11 @@ */ import {MDCFoundation} from '@material/base/foundation'; +import {MDCTabDimensions, TabInteractionEventDetail} from '@material/tab/types'; +import {MDCTabBarAdapter} from './adapter'; +import {numbers, strings} from './constants'; -import {strings, numbers} from './constants'; -import MDCTabBarAdapter from './adapter'; - -/* eslint-disable no-unused-vars */ -import {MDCTabDimensions} from '@material/tab/adapter'; -/* eslint-enable no-unused-vars */ - -/** - * @type {Set} - */ -const ACCEPTABLE_KEYS = new Set(); +const ACCEPTABLE_KEYS: Set = new Set(); // IE11 has no support for new Set with iterable so we need to initialize this by hand ACCEPTABLE_KEYS.add(strings.ARROW_LEFT_KEY); ACCEPTABLE_KEYS.add(strings.ARROW_RIGHT_KEY); @@ -42,10 +35,7 @@ ACCEPTABLE_KEYS.add(strings.HOME_KEY); ACCEPTABLE_KEYS.add(strings.ENTER_KEY); ACCEPTABLE_KEYS.add(strings.SPACE_KEY); -/** - * @type {Map} - */ -const KEYCODE_MAP = new Map(); +const KEYCODE_MAP: Map = new Map(); // IE11 has no support for new Map with iterable so we need to initialize this by hand KEYCODE_MAP.set(numbers.ARROW_LEFT_KEYCODE, strings.ARROW_LEFT_KEY); KEYCODE_MAP.set(numbers.ARROW_RIGHT_KEYCODE, strings.ARROW_RIGHT_KEY); @@ -54,71 +44,54 @@ KEYCODE_MAP.set(numbers.HOME_KEYCODE, strings.HOME_KEY); KEYCODE_MAP.set(numbers.ENTER_KEYCODE, strings.ENTER_KEY); KEYCODE_MAP.set(numbers.SPACE_KEYCODE, strings.SPACE_KEY); -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTabBarFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCTabBarFoundation extends MDCFoundation { static get strings() { return strings; } - /** @return enum {number} */ static get numbers() { return numbers; } - /** - * @see MDCTabBarAdapter for typing information - * @return {!MDCTabBarAdapter} - */ - static get defaultAdapter() { - return /** @type {!MDCTabBarAdapter} */ ({ - scrollTo: () => {}, - incrementScroll: () => {}, - getScrollPosition: () => {}, - getScrollContentWidth: () => {}, - getOffsetWidth: () => {}, - isRTL: () => {}, - setActiveTab: () => {}, - activateTabAtIndex: () => {}, - deactivateTabAtIndex: () => {}, - focusTabAtIndex: () => {}, - getTabIndicatorClientRectAtIndex: () => {}, - getTabDimensionsAtIndex: () => {}, - getPreviousActiveTabIndex: () => {}, - getFocusedTabIndex: () => {}, - getIndexOfTabById: () => {}, - getTabListLength: () => {}, - notifyTabActivated: () => {}, - }); + static get defaultAdapter(): MDCTabBarAdapter { + // tslint:disable:object-literal-sort-keys + return { + scrollTo: () => undefined, + incrementScroll: () => undefined, + getScrollPosition: () => 0, + getScrollContentWidth: () => 0, + getOffsetWidth: () => 0, + isRTL: () => false, + setActiveTab: () => undefined, + activateTabAtIndex: () => undefined, + deactivateTabAtIndex: () => undefined, + focusTabAtIndex: () => undefined, + getTabIndicatorClientRectAtIndex: () => ({top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0}), + getTabDimensionsAtIndex: () => ({rootLeft: 0, rootRight: 0, contentLeft: 0, contentRight: 0}), + getPreviousActiveTabIndex: () => -1, + getFocusedTabIndex: () => -1, + getIndexOfTabById: () => -1, + getTabListLength: () => 0, + notifyTabActivated: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } - /** - * @param {!MDCTabBarAdapter} adapter - * */ - constructor(adapter) { - super(Object.assign(MDCTabBarFoundation.defaultAdapter, adapter)); + private useAutomaticActivation_ = false; - /** @private {boolean} */ - this.useAutomaticActivation_ = false; + constructor(adapter?: Partial) { + super({...MDCTabBarFoundation.defaultAdapter, ...adapter}); } /** * Switches between automatic and manual activation modes. * See https://www.w3.org/TR/wai-aria-practices/#tabpanel for examples. - * @param {boolean} useAutomaticActivation */ - setUseAutomaticActivation(useAutomaticActivation) { + setUseAutomaticActivation(useAutomaticActivation: boolean) { this.useAutomaticActivation_ = useAutomaticActivation; } - /** - * Activates the tab at the given index - * @param {number} index - */ - activateTab(index) { + activateTab(index: number) { const previousActiveIndex = this.adapter_.getPreviousActiveTabIndex(); if (!this.indexIsInRange_(index) || index === previousActiveIndex) { return; @@ -131,11 +104,7 @@ class MDCTabBarFoundation extends MDCFoundation { this.adapter_.notifyTabActivated(index); } - /** - * Handles the keydown event - * @param {!Event} evt - */ - handleKeyDown(evt) { + handleKeyDown(evt: KeyboardEvent) { // Get the key from the event const key = this.getKeyFromEvent_(evt); @@ -171,17 +140,16 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Handles the MDCTab:interacted event - * @param {!CustomEvent} evt */ - handleTabInteraction(evt) { + handleTabInteraction(evt: CustomEvent) { this.adapter_.setActiveTab(this.adapter_.getIndexOfTabById(evt.detail.tabId)); } /** * Scrolls the tab at the given index into view - * @param {number} index The tab index to make visible + * @param index The tab index to make visible */ - scrollIntoView(index) { + scrollIntoView(index: number) { // Early exit if the index is out of range if (!this.indexIsInRange_(index)) { return; @@ -207,12 +175,10 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Private method for determining the index of the destination tab based on what key was pressed - * @param {number} origin The original index from which to determine the destination - * @param {string} key The name of the key - * @return {number} - * @private + * @param origin The original index from which to determine the destination + * @param key The name of the key */ - determineTargetFromKey_(origin, key) { + private determineTargetFromKey_(origin: number, key: string): number { const isRTL = this.isRTL_(); const maxIndex = this.adapter_.getTabListLength() - 1; const shouldGoToEnd = key === strings.END_KEY; @@ -241,14 +207,17 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Calculates the scroll increment that will make the tab at the given index visible - * @param {number} index The index of the tab - * @param {number} nextIndex The index of the next tab - * @param {number} scrollPosition The current scroll position - * @param {number} barWidth The width of the Tab Bar - * @return {number} - * @private + * @param index The index of the tab + * @param nextIndex The index of the next tab + * @param scrollPosition The current scroll position + * @param barWidth The width of the Tab Bar */ - calculateScrollIncrement_(index, nextIndex, scrollPosition, barWidth) { + private calculateScrollIncrement_( + index: number, + nextIndex: number, + scrollPosition: number, + barWidth: number, + ): number { const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex); const relativeContentLeft = nextTabDimensions.contentLeft - scrollPosition - barWidth; const relativeContentRight = nextTabDimensions.contentRight - scrollPosition; @@ -264,15 +233,19 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Calculates the scroll increment that will make the tab at the given index visible in RTL - * @param {number} index The index of the tab - * @param {number} nextIndex The index of the next tab - * @param {number} scrollPosition The current scroll position - * @param {number} barWidth The width of the Tab Bar - * @param {number} scrollContentWidth The width of the scroll content - * @return {number} - * @private + * @param index The index of the tab + * @param nextIndex The index of the next tab + * @param scrollPosition The current scroll position + * @param barWidth The width of the Tab Bar + * @param scrollContentWidth The width of the scroll content */ - calculateScrollIncrementRTL_(index, nextIndex, scrollPosition, barWidth, scrollContentWidth) { + private calculateScrollIncrementRTL_( + index: number, + nextIndex: number, + scrollPosition: number, + barWidth: number, + scrollContentWidth: number, + ): number { const nextTabDimensions = this.adapter_.getTabDimensionsAtIndex(nextIndex); const relativeContentLeft = scrollContentWidth - nextTabDimensions.contentLeft - scrollPosition; const relativeContentRight = scrollContentWidth - nextTabDimensions.contentRight - scrollPosition - barWidth; @@ -288,14 +261,17 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Determines the index of the adjacent tab closest to either edge of the Tab Bar - * @param {number} index The index of the tab - * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab - * @param {number} scrollPosition The current scroll position - * @param {number} barWidth The width of the tab bar - * @return {number} - * @private + * @param index The index of the tab + * @param tabDimensions The dimensions of the tab + * @param scrollPosition The current scroll position + * @param barWidth The width of the tab bar */ - findAdjacentTabIndexClosestToEdge_(index, tabDimensions, scrollPosition, barWidth) { + private findAdjacentTabIndexClosestToEdge_( + index: number, + tabDimensions: MDCTabDimensions, + scrollPosition: number, + barWidth: number, + ): number { /** * Tabs are laid out in the Tab Scroller like this: * @@ -339,15 +315,19 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Determines the index of the adjacent tab closest to either edge of the Tab Bar in RTL - * @param {number} index The index of the tab - * @param {!MDCTabDimensions} tabDimensions The dimensions of the tab - * @param {number} scrollPosition The current scroll position - * @param {number} barWidth The width of the tab bar - * @param {number} scrollContentWidth The width of the scroller content - * @return {number} - * @private + * @param index The index of the tab + * @param tabDimensions The dimensions of the tab + * @param scrollPosition The current scroll position + * @param barWidth The width of the tab bar + * @param scrollContentWidth The width of the scroller content */ - findAdjacentTabIndexClosestToEdgeRTL_(index, tabDimensions, scrollPosition, barWidth, scrollContentWidth) { + private findAdjacentTabIndexClosestToEdgeRTL_( + index: number, + tabDimensions: MDCTabDimensions, + scrollPosition: number, + barWidth: number, + scrollContentWidth: number, + ): number { const rootLeft = scrollContentWidth - tabDimensions.rootLeft - barWidth - scrollPosition; const rootRight = scrollContentWidth - tabDimensions.rootRight - scrollPosition; const rootDelta = rootLeft + rootRight; @@ -367,46 +347,39 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Returns the key associated with a keydown event - * @param {!Event} evt The keydown event - * @return {string} - * @private + * @param evt The keydown event */ - getKeyFromEvent_(evt) { + private getKeyFromEvent_(evt: KeyboardEvent): string { if (ACCEPTABLE_KEYS.has(evt.key)) { return evt.key; } - - return KEYCODE_MAP.get(evt.keyCode); + return KEYCODE_MAP.get(evt.keyCode)!; } - isActivationKey_(key) { + private isActivationKey_(key: string) { return key === strings.SPACE_KEY || key === strings.ENTER_KEY; } /** * Returns whether a given index is inclusively between the ends - * @param {number} index The index to test - * @private + * @param index The index to test */ - indexIsInRange_(index) { + private indexIsInRange_(index: number) { return index >= 0 && index < this.adapter_.getTabListLength(); } /** * Returns the view's RTL property - * @return {boolean} - * @private */ - isRTL_() { + private isRTL_(): boolean { return this.adapter_.isRTL(); } /** - * Scrolls the tab at the given index into view for left-to-right useragents - * @param {number} index The index of the tab to scroll into view - * @private + * Scrolls the tab at the given index into view for left-to-right user agents. + * @param index The index of the tab to scroll into view */ - scrollIntoView_(index) { + private scrollIntoView_(index: number) { const scrollPosition = this.adapter_.getScrollPosition(); const barWidth = this.adapter_.getOffsetWidth(); const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index); @@ -422,10 +395,9 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Scrolls the tab at the given index into view in RTL - * @param {number} index The tab index to make visible - * @private + * @param index The tab index to make visible */ - scrollIntoViewRTL_(index) { + private scrollIntoViewRTL_(index: number) { const scrollPosition = this.adapter_.getScrollPosition(); const barWidth = this.adapter_.getOffsetWidth(); const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index); @@ -442,4 +414,4 @@ class MDCTabBarFoundation extends MDCFoundation { } } -export default MDCTabBarFoundation; +export {MDCTabBarFoundation as default, MDCTabBarFoundation}; diff --git a/packages/mdc-tab-bar/index.js b/packages/mdc-tab-bar/index.js deleted file mode 100644 index 93785225532..00000000000 --- a/packages/mdc-tab-bar/index.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * @license - * Copyright 2018 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 {MDCTab, MDCTabFoundation} from '@material/tab/index'; -import {MDCTabScroller} from '@material/tab-scroller/index'; - -import MDCTabBarAdapter from './adapter'; -import MDCTabBarFoundation from './foundation'; - -let tabIdCounter = 0; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTabBar extends MDCComponent { - /** - * @param {...?} args - */ - constructor(...args) { - super(...args); - - /** @private {!Array} */ - this.tabList_; - - /** @private {?MDCTabScroller} */ - this.tabScroller_; - - /** @private {?function(?Event): undefined} */ - this.handleTabInteraction_; - - /** @private {?function(?Event): undefined} */ - this.handleKeyDown_; - } - - /** - * @param {!Element} root - * @return {!MDCTabBar} - */ - static attachTo(root) { - return new MDCTabBar(root); - } - - set focusOnActivate(focusOnActivate) { - this.tabList_.forEach((tab) => tab.focusOnActivate = focusOnActivate); - } - - set useAutomaticActivation(useAutomaticActivation) { - this.foundation_.setUseAutomaticActivation(useAutomaticActivation); - } - - /** - * @param {(function(!Element): !MDCTab)=} tabFactory A function which creates a new MDCTab - * @param {(function(!Element): !MDCTabScroller)=} tabScrollerFactory A function which creates a new MDCTabScroller - */ - initialize( - tabFactory = (el) => new MDCTab(el), - tabScrollerFactory = (el) => new MDCTabScroller(el)) { - this.tabList_ = this.instantiateTabs_(tabFactory); - this.tabScroller_ = this.instantiateTabScroller_(tabScrollerFactory); - } - - initialSyncWithDOM() { - this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt); - this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); - - this.root_.addEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); - this.root_.addEventListener('keydown', this.handleKeyDown_); - - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].active) { - this.scrollIntoView(i); - break; - } - } - } - - destroy() { - super.destroy(); - this.root_.removeEventListener(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); - this.root_.removeEventListener('keydown', this.handleKeyDown_); - this.tabList_.forEach((tab) => tab.destroy()); - this.tabScroller_.destroy(); - } - - /** - * @return {!MDCTabBarFoundation} - */ - getDefaultFoundation() { - return new MDCTabBarFoundation( - /** @type {!MDCTabBarAdapter} */ ({ - scrollTo: (scrollX) => this.tabScroller_.scrollTo(scrollX), - incrementScroll: (scrollXIncrement) => this.tabScroller_.incrementScroll(scrollXIncrement), - getScrollPosition: () => this.tabScroller_.getScrollPosition(), - getScrollContentWidth: () => this.tabScroller_.getScrollContentWidth(), - getOffsetWidth: () => this.root_.offsetWidth, - isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', - setActiveTab: (index) => this.foundation_.activateTab(index), - activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect), - deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(), - focusTabAtIndex: (index) => this.tabList_[index].focus(), - getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(), - getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(), - getPreviousActiveTabIndex: () => { - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].active) { - return i; - } - } - return -1; - }, - getFocusedTabIndex: () => { - const tabElements = this.getTabElements_(); - const activeElement = document.activeElement; - return tabElements.indexOf(activeElement); - }, - getIndexOfTabById: (id) => { - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].id === id) { - return i; - } - } - return -1; - }, - getTabListLength: () => this.tabList_.length, - notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true), - }) - ); - } - - /** - * Activates the tab at the given index - * @param {number} index The index of the tab - */ - activateTab(index) { - this.foundation_.activateTab(index); - } - - /** - * Scrolls the tab at the given index into view - * @param {number} index THe index of the tab - */ - scrollIntoView(index) { - this.foundation_.scrollIntoView(index); - } - - /** - * Returns all the tab elements in a nice clean array - * @return {!Array} - * @private - */ - getTabElements_() { - return [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); - } - - /** - * Instantiates tab components on all child tab elements - * @param {(function(!Element): !MDCTab)} tabFactory - * @return {!Array} - * @private - */ - instantiateTabs_(tabFactory) { - return this.getTabElements_().map((el) => { - el.id = el.id || `mdc-tab-${++tabIdCounter}`; - return tabFactory(el); - }); - } - - /** - * Instantiates tab scroller component on the child tab scroller element - * @param {(function(!Element): !MDCTabScroller)} tabScrollerFactory - * @return {?MDCTabScroller} - * @private - */ - instantiateTabScroller_(tabScrollerFactory) { - const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR); - if (tabScrollerElement) { - return tabScrollerFactory(tabScrollerElement); - } - return null; - } -} - -export {MDCTabBar, MDCTabBarFoundation}; diff --git a/packages/mdc-tab-bar/index.ts b/packages/mdc-tab-bar/index.ts new file mode 100644 index 00000000000..c3c45b211e5 --- /dev/null +++ b/packages/mdc-tab-bar/index.ts @@ -0,0 +1,175 @@ +/** + * @license + * Copyright 2018 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 {CustomEventListener, SpecificEventListener} from '@material/base/types'; +import {MDCTabScroller} from '@material/tab-scroller/index'; +import {MDCTab, MDCTabFoundation, TabInteractionEvent} from '@material/tab/index'; +import {MDCTabBarFoundation} from './foundation'; +import {TabFactory, TabScrollerFactory} from './types'; + +let tabIdCounter = 0; + +class MDCTabBar extends MDCComponent { + static attachTo(root: Element): MDCTabBar { + return new MDCTabBar(root); + } + + private tabList_!: MDCTab[]; // assigned in initialize() + private tabScroller_!: MDCTabScroller | null; // assigned in initialize() + private handleTabInteraction_!: CustomEventListener; // assigned in initialSyncWithDOM() + private handleKeyDown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + + set focusOnActivate(focusOnActivate: boolean) { + this.tabList_.forEach((tab) => tab.focusOnActivate = focusOnActivate); + } + + set useAutomaticActivation(useAutomaticActivation: boolean) { + this.foundation_.setUseAutomaticActivation(useAutomaticActivation); + } + + initialize( + tabFactory: TabFactory = (el) => new MDCTab(el), + tabScrollerFactory: TabScrollerFactory = (el) => new MDCTabScroller(el), + ) { + this.tabList_ = this.instantiateTabs_(tabFactory); + this.tabScroller_ = this.instantiateTabScroller_(tabScrollerFactory); + } + + initialSyncWithDOM() { + this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt); + this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); + + this.listen(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.listen('keydown', this.handleKeyDown_); + + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].active) { + this.scrollIntoView(i); + break; + } + } + } + + destroy() { + super.destroy(); + this.unlisten(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.unlisten('keydown', this.handleKeyDown_); + this.tabList_.forEach((tab) => tab.destroy()); + + if (this.tabScroller_) { + this.tabScroller_.destroy(); + } + } + + getDefaultFoundation(): MDCTabBarFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCTabBarFoundation({ + scrollTo: (scrollX) => this.tabScroller_!.scrollTo(scrollX), + incrementScroll: (scrollXIncrement) => this.tabScroller_!.incrementScroll(scrollXIncrement), + getScrollPosition: () => this.tabScroller_!.getScrollPosition(), + getScrollContentWidth: () => this.tabScroller_!.getScrollContentWidth(), + getOffsetWidth: () => (this.root_ as HTMLElement).offsetWidth, + isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + setActiveTab: (index) => this.foundation_.activateTab(index), + activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect), + deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(), + focusTabAtIndex: (index) => this.tabList_[index].focus(), + getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(), + getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(), + getPreviousActiveTabIndex: () => { + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].active) { + return i; + } + } + return -1; + }, + getFocusedTabIndex: () => { + const tabElements = this.getTabElements_(); + const activeElement = document.activeElement!; + return tabElements.indexOf(activeElement); + }, + getIndexOfTabById: (id) => { + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].id === id) { + return i; + } + } + return -1; + }, + getTabListLength: () => this.tabList_.length, + notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true), + }); + // tslint:enable:object-literal-sort-keys + } + + /** + * Activates the tab at the given index + * @param index The index of the tab + */ + activateTab(index: number) { + this.foundation_.activateTab(index); + } + + /** + * Scrolls the tab at the given index into view + * @param index THe index of the tab + */ + scrollIntoView(index: number) { + this.foundation_.scrollIntoView(index); + } + + /** + * Returns all the tab elements in a nice clean array + */ + private getTabElements_(): Element[] { + return [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); + } + + /** + * Instantiates tab components on all child tab elements + */ + private instantiateTabs_(tabFactory: TabFactory) { + return this.getTabElements_().map((el) => { + el.id = el.id || `mdc-tab-${++tabIdCounter}`; + return tabFactory(el); + }); + } + + /** + * Instantiates tab scroller component on the child tab scroller element + */ + private instantiateTabScroller_(tabScrollerFactory: TabScrollerFactory): MDCTabScroller | null { + const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR); + if (tabScrollerElement) { + return tabScrollerFactory(tabScrollerElement); + } + return null; + } +} + +export {MDCTabBar as default, MDCTabBar}; +export * from './adapter'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-tab-bar/types.ts b/packages/mdc-tab-bar/types.ts new file mode 100644 index 00000000000..f36458e9728 --- /dev/null +++ b/packages/mdc-tab-bar/types.ts @@ -0,0 +1,28 @@ +/** + * @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 {MDCTabScroller} from '@material/tab-scroller/index'; +import {MDCTab} from '@material/tab/index'; + +export type TabFactory = (el: Element) => MDCTab; +export type TabScrollerFactory = (el: Element) => MDCTabScroller; diff --git a/packages/mdc-tab-indicator/README.md b/packages/mdc-tab-indicator/README.md index f14d8507b73..df71c50c3d1 100644 --- a/packages/mdc-tab-indicator/README.md +++ b/packages/mdc-tab-indicator/README.md @@ -145,7 +145,7 @@ Mixin | Description Method Signature | Description --- | --- -`activate(previousIndicatorClientRect: ClientRect) => void` | Activates the tab indicator. +`activate(previousIndicatorClientRect?: ClientRect) => void` | Activates the tab indicator. `deactivate() => void` | Deactivates the tab indicator. `computeContentClientRect() => ClientRect` | Returns the content element bounding client rect. @@ -167,6 +167,6 @@ Method Signature | Description Method Signature | Description --- | --- `handleTransitionEnd(evt: Event) => void` | Handles the logic for the `"transitionend"` event on the root element. -`activate(previousIndicatorClientRect: ClientRect) => void` | Activates the tab indicator. +`activate(previousIndicatorClientRect?: ClientRect) => void` | Activates the tab indicator. `deactivate() => void` | Deactivates the tab indicator. `computeContentClientRect() => ClientRect` | Returns the content element's bounding client rect. diff --git a/packages/mdc-tab-indicator/adapter.js b/packages/mdc-tab-indicator/adapter.ts similarity index 65% rename from packages/mdc-tab-indicator/adapter.js rename to packages/mdc-tab-indicator/adapter.ts index ba548ac92ac..236a993b67f 100644 --- a/packages/mdc-tab-indicator/adapter.js +++ b/packages/mdc-tab-indicator/adapter.ts @@ -21,43 +21,37 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - /** - * Adapter for MDC Tab Indicator. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Tab Indicator 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 MDCTabIndicatorAdapter { +interface MDCTabIndicatorAdapter { /** * Adds the given className to the root element. - * @param {string} className The className to add + * @param className The className to add */ - addClass(className) {} + addClass(className: string): void; /** * Removes the given className from the root element. - * @param {string} className The className to remove + * @param className The className to remove */ - removeClass(className) {} + removeClass(className: string): void; /** * Returns the client rect of the content element. - * @return {!ClientRect} */ - computeContentClientRect() {} + computeContentClientRect(): ClientRect; /** * Sets a style property of the content element to the passed value - * @param {string} propName The style property name to set - * @param {string} value The style property value + * @param propName The style property name to set + * @param value The style property value */ - setContentStyleProperty(propName, value) {} + setContentStyleProperty(propName: string, value: string): void; } -export default MDCTabIndicatorAdapter; +export {MDCTabIndicatorAdapter as default, MDCTabIndicatorAdapter}; diff --git a/packages/mdc-tab-indicator/constants.js b/packages/mdc-tab-indicator/constants.ts similarity index 96% rename from packages/mdc-tab-indicator/constants.js rename to packages/mdc-tab-indicator/constants.ts index 89a9ee080aa..7437c9887a9 100644 --- a/packages/mdc-tab-indicator/constants.js +++ b/packages/mdc-tab-indicator/constants.ts @@ -21,14 +21,12 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { ACTIVE: 'mdc-tab-indicator--active', FADE: 'mdc-tab-indicator--fade', NO_TRANSITION: 'mdc-tab-indicator--no-transition', }; -/** @enum {string} */ const strings = { CONTENT_SELECTOR: '.mdc-tab-indicator__content', }; diff --git a/packages/mdc-tab-indicator/fading-foundation.js b/packages/mdc-tab-indicator/fading-foundation.ts similarity index 87% rename from packages/mdc-tab-indicator/fading-foundation.js rename to packages/mdc-tab-indicator/fading-foundation.ts index b39124f42ce..2be01e9790b 100644 --- a/packages/mdc-tab-indicator/fading-foundation.js +++ b/packages/mdc-tab-indicator/fading-foundation.ts @@ -21,12 +21,9 @@ * THE SOFTWARE. */ -import MDCTabIndicatorFoundation from './foundation'; +import {MDCTabIndicatorFoundation} from './foundation'; -/** - * @extends {MDCTabIndicatorFoundation} - * @final - */ +/* istanbul ignore next: subclass is not a branch statement */ class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { activate() { this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); @@ -37,4 +34,4 @@ class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { } } -export default MDCFadingTabIndicatorFoundation; +export {MDCFadingTabIndicatorFoundation as default, MDCFadingTabIndicatorFoundation}; diff --git a/packages/mdc-tab-indicator/foundation.js b/packages/mdc-tab-indicator/foundation.ts similarity index 54% rename from packages/mdc-tab-indicator/foundation.js rename to packages/mdc-tab-indicator/foundation.ts index 7332df8622b..bab499e44bc 100644 --- a/packages/mdc-tab-indicator/foundation.js +++ b/packages/mdc-tab-indicator/foundation.ts @@ -22,59 +22,39 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import MDCTabIndicatorAdapter from './adapter'; -import { - cssClasses, - strings, -} from './constants'; +import {MDCTabIndicatorAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; -/** - * @extends {MDCFoundation} - * @abstract - */ -class MDCTabIndicatorFoundation extends MDCFoundation { - /** @return enum {string} */ +abstract class MDCTabIndicatorFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } - /** - * @see MDCTabIndicatorAdapter for typing information - * @return {!MDCTabIndicatorAdapter} - */ - static get defaultAdapter() { - return /** @type {!MDCTabIndicatorAdapter} */ ({ - addClass: () => {}, - removeClass: () => {}, - computeContentClientRect: () => {}, - setContentStyleProperty: () => {}, - }); + static get defaultAdapter(): MDCTabIndicatorAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: () => undefined, + removeClass: () => undefined, + computeContentClientRect: () => ({top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0}), + setContentStyleProperty: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } - /** @param {!MDCTabIndicatorAdapter} adapter */ - constructor(adapter) { - super(Object.assign(MDCTabIndicatorFoundation.defaultAdapter, adapter)); + constructor(adapter?: Partial) { + super({...MDCTabIndicatorFoundation.defaultAdapter, ...adapter}); } - /** @return {!ClientRect} */ - computeContentClientRect() { + computeContentClientRect(): ClientRect { return this.adapter_.computeContentClientRect(); } - /** - * Activates the indicator - * @param {!ClientRect=} previousIndicatorClientRect - * @abstract - */ - activate(previousIndicatorClientRect) {} // eslint-disable-line no-unused-vars - - /** @abstract */ - deactivate() {} + abstract activate(previousIndicatorClientRect?: ClientRect): void; + abstract deactivate(): void; } -export default MDCTabIndicatorFoundation; +export {MDCTabIndicatorFoundation as default, MDCTabIndicatorFoundation}; diff --git a/packages/mdc-tab-indicator/index.js b/packages/mdc-tab-indicator/index.ts similarity index 63% rename from packages/mdc-tab-indicator/index.js rename to packages/mdc-tab-indicator/index.ts index 4eeb5831932..cebd2a33586 100644 --- a/packages/mdc-tab-indicator/index.js +++ b/packages/mdc-tab-indicator/index.ts @@ -23,55 +23,35 @@ import {MDCComponent} from '@material/base/component'; -import MDCTabIndicatorAdapter from './adapter'; -import MDCTabIndicatorFoundation from './foundation'; +import {MDCTabIndicatorAdapter} from './adapter'; +import {MDCFadingTabIndicatorFoundation} from './fading-foundation'; +import {MDCTabIndicatorFoundation} from './foundation'; +import {MDCSlidingTabIndicatorFoundation} from './sliding-foundation'; -import MDCSlidingTabIndicatorFoundation from './sliding-foundation'; -import MDCFadingTabIndicatorFoundation from './fading-foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTabIndicator extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCTabIndicator} - */ - static attachTo(root) { +class MDCTabIndicator extends MDCComponent { + static attachTo(root: Element): MDCTabIndicator { return new MDCTabIndicator(root); } - /** - * @param {...?} args - */ - constructor(...args) { - super(...args); - /** @type {?Element} */ - this.content_; - } + private content_!: HTMLElement; // assigned in initialize() initialize() { - this.content_ = this.root_.querySelector(MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR); + this.content_ = this.root_.querySelector(MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR)!; } - /** - * @return {!ClientRect} - */ - computeContentClientRect() { + computeContentClientRect(): ClientRect { return this.foundation_.computeContentClientRect(); } - /** - * @return {!MDCTabIndicatorFoundation} - */ - getDefaultFoundation() { - const adapter = /** @type {!MDCTabIndicatorAdapter} */ (Object.assign({ + getDefaultFoundation(): MDCTabIndicatorFoundation { + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabIndicatorAdapter = { addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), computeContentClientRect: () => this.content_.getBoundingClientRect(), setContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), - })); + }; + // tslint:enable:object-literal-sort-keys if (this.root_.classList.contains(MDCTabIndicatorFoundation.cssClasses.FADE)) { return new MDCFadingTabIndicatorFoundation(adapter); @@ -81,10 +61,7 @@ class MDCTabIndicator extends MDCComponent { return new MDCSlidingTabIndicatorFoundation(adapter); } - /** - * @param {!ClientRect=} previousIndicatorClientRect - */ - activate(previousIndicatorClientRect) { + activate(previousIndicatorClientRect?: ClientRect) { this.foundation_.activate(previousIndicatorClientRect); } @@ -93,4 +70,8 @@ class MDCTabIndicator extends MDCComponent { } } -export {MDCTabIndicator, MDCTabIndicatorFoundation, MDCSlidingTabIndicatorFoundation, MDCFadingTabIndicatorFoundation}; +export {MDCTabIndicator as default, MDCTabIndicator}; +export * from './adapter'; +export * from './foundation'; +export * from './fading-foundation'; +export * from './sliding-foundation'; diff --git a/packages/mdc-tab-indicator/sliding-foundation.js b/packages/mdc-tab-indicator/sliding-foundation.ts similarity index 90% rename from packages/mdc-tab-indicator/sliding-foundation.js rename to packages/mdc-tab-indicator/sliding-foundation.ts index 927397b8874..943c57bf7ca 100644 --- a/packages/mdc-tab-indicator/sliding-foundation.js +++ b/packages/mdc-tab-indicator/sliding-foundation.ts @@ -21,15 +21,11 @@ * THE SOFTWARE. */ -import MDCTabIndicatorFoundation from './foundation'; +import {MDCTabIndicatorFoundation} from './foundation'; -/** - * @extends {MDCTabIndicatorFoundation} - * @final - */ +/* istanbul ignore next: subclass is not a branch statement */ class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { - /** @param {!ClientRect=} previousIndicatorClientRect */ - activate(previousIndicatorClientRect) { + activate(previousIndicatorClientRect?: ClientRect) { // Early exit if no indicator is present to handle cases where an indicator // may be activated without a prior indicator state if (!previousIndicatorClientRect) { @@ -60,4 +56,4 @@ class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { } } -export default MDCSlidingTabIndicatorFoundation; +export {MDCSlidingTabIndicatorFoundation as default, MDCSlidingTabIndicatorFoundation}; diff --git a/packages/mdc-tab-scroller/adapter.js b/packages/mdc-tab-scroller/adapter.js deleted file mode 100644 index ab1fce28048..00000000000 --- a/packages/mdc-tab-scroller/adapter.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @license - * Copyright 2018 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 no-unused-vars: [2, {"args": "none"}] */ - -/** - * MDCTabScrollerAnimation contains the values required for animating from the - * current scroll position to the new scroll position. The "finalScrollPosition" - * value represents the new scroll position while the "scrollDelta" value is the - * corresponding transformation that is applied to the scroll content. Together, - * they create the animation by first updating the scroll value then applying - * the transformation and animating the transition. Both pieces are necessary - * for the scroll animation to work. The values are used as-is by the tab - * scroller animation method, ensuring that all logic for determining scroll - * position or transformation is abstracted away from the animation method. - * @typedef {{finalScrollPosition: number, scrollDelta: number}} - */ -let MDCTabScrollerAnimation; - -/** - * MDCTabScrollerHorizontalEdges represents the left and right edges of the - * scroll content. These values vary depending on how scrolling in RTL is - * implemented by the browser. One value is always 0 and one value is always - * the max scrollable value as either a positive or negative integer. - * @typedef {{left: number, right: number}} - */ -let MDCTabScrollerHorizontalEdges; - -/** - * Adapter for MDC Tab Scroller. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Tab into your framework. See - * https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md - * for more information. - * - * @record - */ -class MDCTabScrollerAdapter { - /** - * Adds the given className to the root element. - * @param {string} className The className to add - */ - addClass(className) {} - - /** - * Removes the given className from the root element. - * @param {string} className The className to remove - */ - removeClass(className) {} - - /** - * Adds the given className to the scroll area element. - * @param {string} className The className to add - */ - addScrollAreaClass(className) {} - - /** - * Returns whether the event target matches given className. - * @param {EventTarget} evtTarget The event target - * @param {string} selector The selector to check - * @return {boolean} - */ - eventTargetMatchesSelector(evtTarget, selector) {} - - /** - * Sets a style property of the area element to the passed value. - * @param {string} propName The style property name to set - * @param {string} value The style property value - */ - setScrollAreaStyleProperty(propName, value) {} - - /** - * Sets a style property of the content element to the passed value. - * @param {string} propName The style property name to set - * @param {string} value The style property value - */ - setScrollContentStyleProperty(propName, value) {} - - /** - * Returns the scroll content element's computed style value of the given css property `propertyName`. - * We achieve this via `getComputedStyle(...).getPropertyValue(propertyName)`. - * @param {string} propertyName - * @return {string} - */ - getScrollContentStyleValue(propertyName) {} - - /** - * Sets the scrollLeft value of the scroll area element to the passed value. - * @param {number} scrollLeft The new scrollLeft value - */ - setScrollAreaScrollLeft(scrollLeft) {} - - /** - * Returns the scrollLeft value of the scroll area element. - * @return {number} - */ - getScrollAreaScrollLeft() {} - - /** - * Returns the offsetWidth of the scroll content element. - * @return {number} - */ - getScrollContentOffsetWidth() {} - - /** - * Returns the offsetWitdth of the scroll area element. - * @return {number} - */ - getScrollAreaOffsetWidth() {} - - /** - * Returns the bounding client rect of the scroll area element. - * @return {!ClientRect} - */ - computeScrollAreaClientRect() {} - - /** - * Returns the bounding client rect of the scroll content element. - * @return {!ClientRect} - */ - computeScrollContentClientRect() {} - - /** - * Returns the height of the browser's horizontal scrollbars (in px). - * @return {number} - */ - computeHorizontalScrollbarHeight() {} -} - -export {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges, MDCTabScrollerAdapter}; diff --git a/packages/mdc-tab-scroller/adapter.ts b/packages/mdc-tab-scroller/adapter.ts new file mode 100644 index 00000000000..430ef725345 --- /dev/null +++ b/packages/mdc-tab-scroller/adapter.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2018 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. + */ + +/** + * 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 MDCTabScrollerAdapter { + /** + * Adds the given className to the root element. + * @param className The className to add + */ + addClass(className: string): void; + + /** + * Removes the given className from the root element. + * @param className The className to remove + */ + removeClass(className: string): void; + + /** + * Adds the given className to the scroll area element. + * @param className The className to add + */ + addScrollAreaClass(className: string): void; + + /** + * Returns whether the event target matches given className. + * @param evtTarget The event target + * @param selector The selector to check + */ + eventTargetMatchesSelector(evtTarget: EventTarget, selector: string): boolean; + + /** + * Sets a style property of the area element to the passed value. + * @param propName The style property name to set + * @param value The style property value + */ + setScrollAreaStyleProperty(propName: string, value: string): void; + + /** + * Sets a style property of the content element to the passed value. + * @param propName The style property name to set + * @param value The style property value + */ + setScrollContentStyleProperty(propName: string, value: string): void; + + /** + * Returns the scroll content element's computed style value of the given css property `propertyName`. + * We achieve this via `getComputedStyle(...).getPropertyValue(propertyName)`. + */ + getScrollContentStyleValue(propertyName: string): string; + + /** + * Sets the scrollLeft value of the scroll area element to the passed value. + * @param scrollLeft The new scrollLeft value + */ + setScrollAreaScrollLeft(scrollLeft: number): void; + + /** + * Returns the scrollLeft value of the scroll area element. + */ + getScrollAreaScrollLeft(): number; + + /** + * Returns the offsetWidth of the scroll content element. + */ + getScrollContentOffsetWidth(): number; + + /** + * Returns the offsetWitdth of the scroll area element. + */ + getScrollAreaOffsetWidth(): number; + + /** + * Returns the bounding client rect of the scroll area element. + */ + computeScrollAreaClientRect(): ClientRect; + + /** + * Returns the bounding client rect of the scroll content element. + */ + computeScrollContentClientRect(): ClientRect; + + /** + * Returns the height of the browser's horizontal scrollbars (in px). + */ + computeHorizontalScrollbarHeight(): number; +} + +export {MDCTabScrollerAdapter as default, MDCTabScrollerAdapter}; diff --git a/packages/mdc-tab-scroller/constants.js b/packages/mdc-tab-scroller/constants.ts similarity index 97% rename from packages/mdc-tab-scroller/constants.js rename to packages/mdc-tab-scroller/constants.ts index f7f4da6bd83..ff158626ea2 100644 --- a/packages/mdc-tab-scroller/constants.js +++ b/packages/mdc-tab-scroller/constants.ts @@ -21,14 +21,12 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { ANIMATING: 'mdc-tab-scroller--animating', - SCROLL_TEST: 'mdc-tab-scroller__test', SCROLL_AREA_SCROLL: 'mdc-tab-scroller__scroll-area--scroll', + SCROLL_TEST: 'mdc-tab-scroller__test', }; -/** @enum {string} */ const strings = { AREA_SELECTOR: '.mdc-tab-scroller__scroll-area', CONTENT_SELECTOR: '.mdc-tab-scroller__scroll-content', diff --git a/packages/mdc-tab-scroller/foundation.js b/packages/mdc-tab-scroller/foundation.ts similarity index 70% rename from packages/mdc-tab-scroller/foundation.js rename to packages/mdc-tab-scroller/foundation.ts index 7e39a6145f2..1a52fd9720b 100644 --- a/packages/mdc-tab-scroller/foundation.js +++ b/packages/mdc-tab-scroller/foundation.ts @@ -22,69 +22,57 @@ */ import {MDCFoundation} from '@material/base/foundation'; +import {MDCTabScrollerAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -/* eslint-disable no-unused-vars */ -import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges, MDCTabScrollerAdapter} from './adapter'; -import MDCTabScrollerRTL from './rtl-scroller'; -/* eslint-enable no-unused-vars */ -import MDCTabScrollerRTLDefault from './rtl-default-scroller'; -import MDCTabScrollerRTLNegative from './rtl-negative-scroller'; -import MDCTabScrollerRTLReverse from './rtl-reverse-scroller'; +import {MDCTabScrollerRTLDefault} from './rtl-default-scroller'; +import {MDCTabScrollerRTLNegative} from './rtl-negative-scroller'; +import {MDCTabScrollerRTLReverse} from './rtl-reverse-scroller'; +import {MDCTabScrollerRTL} from './rtl-scroller'; +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTabScrollerFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCTabScrollerFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } - /** - * @see MDCTabScrollerAdapter for typing information - * @return {!MDCTabScrollerAdapter} - */ - static get defaultAdapter() { - return /** @type {!MDCTabScrollerAdapter} */ ({ - eventTargetMatchesSelector: () => {}, - addClass: () => {}, - removeClass: () => {}, - addScrollAreaClass: () => {}, - setScrollAreaStyleProperty: () => {}, - setScrollContentStyleProperty: () => {}, - getScrollContentStyleValue: () => {}, - setScrollAreaScrollLeft: () => {}, - getScrollAreaScrollLeft: () => {}, - getScrollContentOffsetWidth: () => {}, - getScrollAreaOffsetWidth: () => {}, - computeScrollAreaClientRect: () => {}, - computeScrollContentClientRect: () => {}, - computeHorizontalScrollbarHeight: () => {}, - }); + static get defaultAdapter(): MDCTabScrollerAdapter { + // tslint:disable:object-literal-sort-keys + return { + eventTargetMatchesSelector: () => false, + addClass: () => undefined, + removeClass: () => undefined, + addScrollAreaClass: () => undefined, + setScrollAreaStyleProperty: () => undefined, + setScrollContentStyleProperty: () => undefined, + getScrollContentStyleValue: () => '', + setScrollAreaScrollLeft: () => undefined, + getScrollAreaScrollLeft: () => 0, + getScrollContentOffsetWidth: () => 0, + getScrollAreaOffsetWidth: () => 0, + computeScrollAreaClientRect: () => ({top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0}), + computeScrollContentClientRect: () => ({top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0}), + computeHorizontalScrollbarHeight: () => 0, + }; + // tslint:enable:object-literal-sort-keys } - /** @param {!MDCTabScrollerAdapter} adapter */ - constructor(adapter) { - super(Object.assign(MDCTabScrollerFoundation.defaultAdapter, adapter)); + /** + * Controls whether we should handle the transitionend and interaction events during the animation. + */ + private isAnimating_ = false; - /** - * This boolean controls whether we should handle the transitionend and interaction events during the animation. - * @private {boolean} - */ - this.isAnimating_ = false; + /** + * The MDCTabScrollerRTL instance varies per browser and allows us to encapsulate the peculiar browser behavior + * of RTL scrolling in it's own class. + */ + private rtlScrollerInstance_?: MDCTabScrollerRTL; - /** - * The MDCTabScrollerRTL instance varies per browser and allows us to encapsulate the peculiar browser behavior - * of RTL scrolling in it's own class. - * @private {?MDCTabScrollerRTL} - */ - this.rtlScrollerInstance_; + constructor(adapter?: Partial) { + super({...MDCTabScrollerFoundation.defaultAdapter, ...adapter}); } init() { @@ -97,9 +85,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Computes the current visual scroll position - * @return {number} */ - getScrollPosition() { + getScrollPosition(): number { if (this.isRTL_()) { return this.computeCurrentScrollPositionRTL_(); } @@ -124,12 +111,12 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Handles the transitionend event - * @param {!Event} evt */ - handleTransitionEnd(evt) { + handleTransitionEnd(evt: Event) { // Early exit if we aren't animating or the event was triggered by a different element. - if (!this.isAnimating_ - || !this.adapter_.eventTargetMatchesSelector(evt.target, MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)) { + const evtTarget = evt.target as Element; + if (!this.isAnimating_ || + !this.adapter_.eventTargetMatchesSelector(evtTarget, MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)) { return; } @@ -139,9 +126,9 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Increment the scroll value by the scrollXIncrement - * @param {number} scrollXIncrement The value by which to increment the scroll position + * @param scrollXIncrement The value by which to increment the scroll position */ - incrementScroll(scrollXIncrement) { + incrementScroll(scrollXIncrement: number) { // Early exit for non-operational increment values if (scrollXIncrement === 0) { return; @@ -156,9 +143,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Scrolls to the given scrollX value - * @param {number} scrollX */ - scrollTo(scrollX) { + scrollTo(scrollX: number) { if (this.isRTL_()) { return this.scrollToRTL_(scrollX); } @@ -168,9 +154,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Returns the appropriate version of the MDCTabScrollerRTL - * @return {!MDCTabScrollerRTL} */ - getRTLScroller() { + getRTLScroller(): MDCTabScrollerRTL { if (!this.rtlScrollerInstance_) { this.rtlScrollerInstance_ = this.rtlScrollerFactory_(); } @@ -180,10 +165,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Returns the translateX value from a CSS matrix transform function string - * @return {number} - * @private */ - calculateCurrentTranslateX_() { + private calculateCurrentTranslateX_(): number { const transformValue = this.adapter_.getScrollContentStyleValue('transform'); // Early exit if no transform is present if (transformValue === 'none') { @@ -194,101 +177,89 @@ class MDCTabScrollerFoundation extends MDCFoundation { // of `matrix(a, b, c, d, tx, ty)`. We only care about tx (translateX) so // we're going to grab all the parenthesized values, strip out tx, and // parse it. - const results = /\((.+)\)/.exec(transformValue)[1]; - const parts = results.split(','); - return parseFloat(parts[4]); + const [, matrixParams] = /\((.+?)\)/.exec(transformValue); + + // @ts-ignore + const [a, b, c, d, tx, ty] = matrixParams.split(','); + + return parseFloat(tx); // tslint:disable-line:ban } /** * Calculates a safe scroll value that is > 0 and < the max scroll value - * @param {number} scrollX The distance to scroll - * @return {number} - * @private + * @param scrollX The distance to scroll */ - clampScrollValue_(scrollX) { + private clampScrollValue_(scrollX: number): number { const edges = this.calculateScrollEdges_(); return Math.min(Math.max(edges.left, scrollX), edges.right); } - /** - * @return {number} - * @private - */ - computeCurrentScrollPositionRTL_() { + private computeCurrentScrollPositionRTL_(): number { const translateX = this.calculateCurrentTranslateX_(); return this.getRTLScroller().getScrollPositionRTL(translateX); } - /** - * @return {!MDCTabScrollerHorizontalEdges} - * @private - */ - calculateScrollEdges_() { + private calculateScrollEdges_(): MDCTabScrollerHorizontalEdges { const contentWidth = this.adapter_.getScrollContentOffsetWidth(); const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); - return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + return { left: 0, right: contentWidth - rootWidth, - }); + }; } /** * Internal scroll method - * @param {number} scrollX The new scroll position - * @private + * @param scrollX The new scroll position */ - scrollTo_(scrollX) { + private scrollTo_(scrollX: number) { const currentScrollX = this.getScrollPosition(); const safeScrollX = this.clampScrollValue_(scrollX); const scrollDelta = safeScrollX - currentScrollX; - this.animate_(/** @type {!MDCTabScrollerAnimation} */ ({ + this.animate_({ finalScrollPosition: safeScrollX, - scrollDelta: scrollDelta, - })); + scrollDelta, + }); } /** * Internal RTL scroll method - * @param {number} scrollX The new scroll position - * @private + * @param scrollX The new scroll position */ - scrollToRTL_(scrollX) { + private scrollToRTL_(scrollX: number) { const animation = this.getRTLScroller().scrollToRTL(scrollX); this.animate_(animation); } /** * Internal increment scroll method - * @param {number} scrollX The new scroll position increment - * @private + * @param scrollX The new scroll position increment */ - incrementScroll_(scrollX) { + private incrementScroll_(scrollX: number) { const currentScrollX = this.getScrollPosition(); const targetScrollX = scrollX + currentScrollX; const safeScrollX = this.clampScrollValue_(targetScrollX); const scrollDelta = safeScrollX - currentScrollX; - this.animate_(/** @type {!MDCTabScrollerAnimation} */ ({ + this.animate_({ finalScrollPosition: safeScrollX, - scrollDelta: scrollDelta, - })); + scrollDelta, + }); } /** - * Internal incremenet scroll RTL method - * @param {number} scrollX The new scroll position RTL increment - * @private + * Internal increment scroll RTL method + * @param scrollX The new scroll position RTL increment */ - incrementScrollRTL_(scrollX) { + private incrementScrollRTL_(scrollX: number) { const animation = this.getRTLScroller().incrementScrollRTL(scrollX); this.animate_(animation); } /** * Animates the tab scrolling - * @param {!MDCTabScrollerAnimation} animation The animation to apply - * @private + * @param animation The animation to apply */ - animate_(animation) { + private animate_(animation: MDCTabScrollerAnimation) { // Early exit if translateX is 0, which means there's no animation to perform if (animation.scrollDelta === 0) { return; @@ -312,9 +283,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Stops scroll animation - * @private */ - stopScrollAnimation_() { + private stopScrollAnimation_() { this.isAnimating_ = false; const currentScrollPosition = this.getAnimatingScrollPosition_(); this.adapter_.removeClass(MDCTabScrollerFoundation.cssClasses.ANIMATING); @@ -324,10 +294,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Gets the current scroll position during animation - * @return {number} - * @private */ - getAnimatingScrollPosition_() { + private getAnimatingScrollPosition_(): number { const currentTranslateX = this.calculateCurrentTranslateX_(); const scrollLeft = this.adapter_.getScrollAreaScrollLeft(); if (this.isRTL_()) { @@ -339,10 +307,8 @@ class MDCTabScrollerFoundation extends MDCFoundation { /** * Determines the RTL Scroller to use - * @return {!MDCTabScrollerRTL} - * @private */ - rtlScrollerFactory_() { + private rtlScrollerFactory_(): MDCTabScrollerRTL { // Browsers have three different implementations of scrollLeft in RTL mode, // dependent on the browser. The behavior is based off the max LTR // scrollleft value and 0. @@ -390,13 +356,9 @@ class MDCTabScrollerFoundation extends MDCFoundation { return new MDCTabScrollerRTLDefault(this.adapter_); } - /** - * @return {boolean} - * @private - */ - isRTL_() { + private isRTL_(): boolean { return this.adapter_.getScrollContentStyleValue('direction') === 'rtl'; } } -export default MDCTabScrollerFoundation; +export {MDCTabScrollerFoundation as default, MDCTabScrollerFoundation}; diff --git a/packages/mdc-tab-scroller/index.js b/packages/mdc-tab-scroller/index.ts similarity index 70% rename from packages/mdc-tab-scroller/index.js rename to packages/mdc-tab-scroller/index.ts index 53371e31d6f..4fc80edf16c 100644 --- a/packages/mdc-tab-scroller/index.js +++ b/packages/mdc-tab-scroller/index.ts @@ -22,43 +22,26 @@ */ import {MDCComponent} from '@material/base/component'; - -import {MDCTabScrollerAdapter} from './adapter'; -import MDCTabScrollerFoundation from './foundation'; +import {SpecificEventListener} from '@material/base/types'; +import {ponyfill} from '@material/dom/index'; +import {MDCTabScrollerFoundation} from './foundation'; import * as util from './util'; -/** - * @extends {MDCComponent} - * @final - */ -class MDCTabScroller extends MDCComponent { - /** - * @param {!Element} root - * @return {!MDCTabScroller} - */ - static attachTo(root) { +type InteractionEventType = 'wheel' | 'touchstart' | 'pointerdown' | 'mousedown' | 'keydown'; + +class MDCTabScroller extends MDCComponent { + static attachTo(root: Element): MDCTabScroller { return new MDCTabScroller(root); } - constructor(...args) { - super(...args); - - /** @private {?Element} */ - this.content_; - - /** @private {?Element} */ - this.area_; - - /** @private {?function(?Event): undefined} */ - this.handleInteraction_; - - /** @private {?function(!Event): undefined} */ - this.handleTransitionEnd_; - } + private content_!: HTMLElement; // assigned in initialize() + private area_!: HTMLElement; // assigned in initialize() + private handleInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() + private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM() initialize() { - this.area_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.AREA_SELECTOR); - this.content_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.CONTENT_SELECTOR); + this.area_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.AREA_SELECTOR)!; + this.content_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)!; } initialSyncWithDOM() { @@ -84,15 +67,10 @@ class MDCTabScroller extends MDCComponent { this.content_.removeEventListener('transitionend', this.handleTransitionEnd_); } - /** - * @return {!MDCTabScrollerFoundation} - */ - getDefaultFoundation() { - const adapter = /** @type {!MDCTabScrollerAdapter} */ ({ - eventTargetMatchesSelector: (evtTarget, selector) => { - const MATCHES = util.getMatchesProperty(HTMLElement.prototype); - return evtTarget[MATCHES](selector); - }, + getDefaultFoundation(): MDCTabScrollerFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCTabScrollerFoundation({ + eventTargetMatchesSelector: (evtTarget, selector) => ponyfill.matches(evtTarget as Element, selector), addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), addScrollAreaClass: (className) => this.area_.classList.add(className), @@ -107,41 +85,41 @@ class MDCTabScroller extends MDCComponent { computeScrollContentClientRect: () => this.content_.getBoundingClientRect(), computeHorizontalScrollbarHeight: () => util.computeHorizontalScrollbarHeight(document), }); - - return new MDCTabScrollerFoundation(adapter); + // tslint:enable:object-literal-sort-keys } /** * Returns the current visual scroll position - * @return {number} */ - getScrollPosition() { + getScrollPosition(): number { return this.foundation_.getScrollPosition(); } /** * Returns the width of the scroll content - * @return {number} */ - getScrollContentWidth() { + getScrollContentWidth(): number { return this.content_.offsetWidth; } /** * Increments the scroll value by the given amount - * @param {number} scrollXIncrement The pixel value by which to increment the scroll value + * @param scrollXIncrement The pixel value by which to increment the scroll value */ - incrementScroll(scrollXIncrement) { + incrementScroll(scrollXIncrement: number) { this.foundation_.incrementScroll(scrollXIncrement); } /** * Scrolls to the given pixel position - * @param {number} scrollX The pixel value to scroll to + * @param scrollX The pixel value to scroll to */ - scrollTo(scrollX) { + scrollTo(scrollX: number) { this.foundation_.scrollTo(scrollX); } } -export {MDCTabScroller, MDCTabScrollerFoundation, util}; +export {MDCTabScroller as default, MDCTabScroller, util}; +export * from './adapter'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-tab-scroller/package.json b/packages/mdc-tab-scroller/package.json index bc4043b9056..0e02117c97d 100644 --- a/packages/mdc-tab-scroller/package.json +++ b/packages/mdc-tab-scroller/package.json @@ -17,6 +17,7 @@ "dependencies": { "@material/animation": "^0.41.0", "@material/base": "^0.41.0", + "@material/dom": "^0.41.0", "@material/tab": "^0.44.0" }, "publishConfig": { diff --git a/packages/mdc-tab-scroller/rtl-default-scroller.js b/packages/mdc-tab-scroller/rtl-default-scroller.ts similarity index 70% rename from packages/mdc-tab-scroller/rtl-default-scroller.js rename to packages/mdc-tab-scroller/rtl-default-scroller.ts index 9baf2db0df9..083a6b5676a 100644 --- a/packages/mdc-tab-scroller/rtl-default-scroller.js +++ b/packages/mdc-tab-scroller/rtl-default-scroller.ts @@ -21,84 +21,53 @@ * THE SOFTWARE. */ -import MDCTabScrollerRTL from './rtl-scroller'; +import {MDCTabScrollerRTL} from './rtl-scroller'; +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -/* eslint-disable no-unused-vars */ -import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; -/* eslint-enable no-unused-vars */ - -/** - * @extends {MDCTabScrollerRTL} - * @final - */ class MDCTabScrollerRTLDefault extends MDCTabScrollerRTL { - /** - * @return {number} - */ - getScrollPositionRTL() { + getScrollPositionRTL(): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const {right} = this.calculateScrollEdges_(); // Scroll values on most browsers are ints instead of floats so we round return Math.round(right - currentScrollLeft); } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - scrollToRTL(scrollX) { + scrollToRTL(scrollX: number): MDCTabScrollerAnimation { const edges = this.calculateScrollEdges_(); const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(edges.right - scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: clampedScrollLeft - currentScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - incrementScrollRTL(scrollX) { + incrementScrollRTL(scrollX: number): MDCTabScrollerAnimation { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft - scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: clampedScrollLeft - currentScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @return {number} - */ - getAnimatingScrollPosition(scrollX) { + getAnimatingScrollPosition(scrollX: number): number { return scrollX; } - /** - * @return {!MDCTabScrollerHorizontalEdges} - * @private - */ - calculateScrollEdges_() { + private calculateScrollEdges_(): MDCTabScrollerHorizontalEdges { const contentWidth = this.adapter_.getScrollContentOffsetWidth(); const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); - return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + return { left: 0, right: contentWidth - rootWidth, - }); + }; } - /** - * @param {number} scrollX - * @return {number} - * @private - */ - clampScrollValue_(scrollX) { + private clampScrollValue_(scrollX: number): number { const edges = this.calculateScrollEdges_(); return Math.min(Math.max(edges.left, scrollX), edges.right); } } -export default MDCTabScrollerRTLDefault; +export {MDCTabScrollerRTLDefault as default, MDCTabScrollerRTLDefault}; diff --git a/packages/mdc-tab-scroller/rtl-negative-scroller.js b/packages/mdc-tab-scroller/rtl-negative-scroller.ts similarity index 66% rename from packages/mdc-tab-scroller/rtl-negative-scroller.js rename to packages/mdc-tab-scroller/rtl-negative-scroller.ts index 6a0d1636629..afb7f5e38ff 100644 --- a/packages/mdc-tab-scroller/rtl-negative-scroller.js +++ b/packages/mdc-tab-scroller/rtl-negative-scroller.ts @@ -21,83 +21,50 @@ * THE SOFTWARE. */ -import MDCTabScrollerRTL from './rtl-scroller'; +import {MDCTabScrollerRTL} from './rtl-scroller'; +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -/* eslint-disable no-unused-vars */ -import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; -/* eslint-enable no-unused-vars */ - -/** - * @extends {MDCTabScrollerRTL} - * @final - */ class MDCTabScrollerRTLNegative extends MDCTabScrollerRTL { - /** - * @param {number} translateX The current translateX position - * @return {number} - */ - getScrollPositionRTL(translateX) { + getScrollPositionRTL(translateX: number): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); return Math.round(translateX - currentScrollLeft); } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - scrollToRTL(scrollX) { + scrollToRTL(scrollX: number): MDCTabScrollerAnimation { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(-scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: clampedScrollLeft - currentScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - incrementScrollRTL(scrollX) { + incrementScrollRTL(scrollX: number): MDCTabScrollerAnimation { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft - scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: clampedScrollLeft - currentScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @param {number} translateX - * @return {number} - */ - getAnimatingScrollPosition(scrollX, translateX) { + getAnimatingScrollPosition(scrollX: number, translateX: number): number { return scrollX - translateX; } - /** - * @return {!MDCTabScrollerHorizontalEdges} - * @private - */ - calculateScrollEdges_() { + private calculateScrollEdges_(): MDCTabScrollerHorizontalEdges { const contentWidth = this.adapter_.getScrollContentOffsetWidth(); const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); - return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + return { left: rootWidth - contentWidth, right: 0, - }); + }; } - /** - * @param {number} scrollX - * @return {number} - * @private - */ - clampScrollValue_(scrollX) { + private clampScrollValue_(scrollX: number): number { const edges = this.calculateScrollEdges_(); return Math.max(Math.min(edges.right, scrollX), edges.left); } } -export default MDCTabScrollerRTLNegative; +export {MDCTabScrollerRTLNegative as default, MDCTabScrollerRTLNegative}; diff --git a/packages/mdc-tab-scroller/rtl-reverse-scroller.js b/packages/mdc-tab-scroller/rtl-reverse-scroller.ts similarity index 68% rename from packages/mdc-tab-scroller/rtl-reverse-scroller.js rename to packages/mdc-tab-scroller/rtl-reverse-scroller.ts index 49dc8707d76..83748615cf6 100644 --- a/packages/mdc-tab-scroller/rtl-reverse-scroller.js +++ b/packages/mdc-tab-scroller/rtl-reverse-scroller.ts @@ -21,83 +21,51 @@ * THE SOFTWARE. */ -import MDCTabScrollerRTL from './rtl-scroller'; +import {MDCTabScrollerRTL} from './rtl-scroller'; +import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -/* eslint-disable no-unused-vars */ -import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './adapter'; -/* eslint-enable no-unused-vars */ - -/** - * @extends {MDCTabScrollerRTL} - * @final - */ class MDCTabScrollerRTLReverse extends MDCTabScrollerRTL { - /** - * @param {number} translateX - * @return {number} - */ - getScrollPositionRTL(translateX) { + getScrollPositionRTL(translateX: number): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); // Scroll values on most browsers are ints instead of floats so we round return Math.round(currentScrollLeft - translateX); } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - scrollToRTL(scrollX) { + scrollToRTL(scrollX: number): MDCTabScrollerAnimation { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: currentScrollLeft - clampedScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - */ - incrementScrollRTL(scrollX) { + incrementScrollRTL(scrollX: number): MDCTabScrollerAnimation { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const clampedScrollLeft = this.clampScrollValue_(currentScrollLeft + scrollX); - return /** @type {!MDCTabScrollerAnimation} */ ({ + return { finalScrollPosition: clampedScrollLeft, scrollDelta: currentScrollLeft - clampedScrollLeft, - }); + }; } - /** - * @param {number} scrollX - * @return {number} - */ - getAnimatingScrollPosition(scrollX, translateX) { + getAnimatingScrollPosition(scrollX: number, translateX: number): number { return scrollX + translateX; } - /** - * @return {!MDCTabScrollerHorizontalEdges} - * @private - */ - calculateScrollEdges_() { + private calculateScrollEdges_(): MDCTabScrollerHorizontalEdges { const contentWidth = this.adapter_.getScrollContentOffsetWidth(); const rootWidth = this.adapter_.getScrollAreaOffsetWidth(); - return /** @type {!MDCTabScrollerHorizontalEdges} */ ({ + return { left: contentWidth - rootWidth, right: 0, - }); + }; } - /** - * @param {number} scrollX - * @return {number} - * @private - */ - clampScrollValue_(scrollX) { + private clampScrollValue_(scrollX: number): number { const edges = this.calculateScrollEdges_(); return Math.min(Math.max(edges.right, scrollX), edges.left); } } -export default MDCTabScrollerRTLReverse; +export {MDCTabScrollerRTLReverse as default, MDCTabScrollerRTLReverse}; diff --git a/packages/mdc-tab-scroller/rtl-scroller.js b/packages/mdc-tab-scroller/rtl-scroller.js deleted file mode 100644 index 692db0f3aff..00000000000 --- a/packages/mdc-tab-scroller/rtl-scroller.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license - * Copyright 2018 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 no-unused-vars: [2, {"args": "none"}] */ - -/* eslint-disable no-unused-vars */ -import {MDCTabScrollerAdapter, MDCTabScrollerAnimation} from './adapter'; -/* eslint-enable no-unused-vars */ - -/** - * @abstract - */ -class MDCTabScrollerRTL { - /** @param {!MDCTabScrollerAdapter} adapter */ - constructor(adapter) { - /** @private */ - this.adapter_ = adapter; - } - - /** - * @param {number} translateX The current translateX position - * @return {number} - * @abstract - */ - getScrollPositionRTL(translateX) {} - - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - * @abstract - */ - scrollToRTL(scrollX) {} - - /** - * @param {number} scrollX - * @return {!MDCTabScrollerAnimation} - * @abstract - */ - incrementScrollRTL(scrollX) {} - - /** - * @param {number} scrollX The current scrollX position - * @param {number} translateX The current translateX position - * @return {number} - * @abstract - */ - getAnimatingScrollPosition(scrollX, translateX) {} -} - -export default MDCTabScrollerRTL; diff --git a/packages/mdc-tab-scroller/rtl-scroller.ts b/packages/mdc-tab-scroller/rtl-scroller.ts new file mode 100644 index 00000000000..6453715de8e --- /dev/null +++ b/packages/mdc-tab-scroller/rtl-scroller.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2018 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 {MDCTabScrollerAdapter} from './adapter'; +import {MDCTabScrollerAnimation} from './types'; + +abstract class MDCTabScrollerRTL { + protected readonly adapter_: MDCTabScrollerAdapter; + + constructor(adapter: MDCTabScrollerAdapter) { + this.adapter_ = adapter; + } + + abstract getScrollPositionRTL(translateX: number): number; + + abstract scrollToRTL(scrollX: number): MDCTabScrollerAnimation; + + abstract incrementScrollRTL(scrollX: number): MDCTabScrollerAnimation; + + /** + * @param scrollX The current scrollX position + * @param translateX The current translateX position + */ + abstract getAnimatingScrollPosition(scrollX: number, translateX: number): number; +} + +export {MDCTabScrollerRTL as default, MDCTabScrollerRTL}; diff --git a/packages/mdc-tab-scroller/types.ts b/packages/mdc-tab-scroller/types.ts new file mode 100644 index 00000000000..c83e801e0fe --- /dev/null +++ b/packages/mdc-tab-scroller/types.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2018 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. + */ + +/** + * MDCTabScrollerAnimation contains the values required for animating from the + * current scroll position to the new scroll position. The "finalScrollPosition" + * value represents the new scroll position while the "scrollDelta" value is the + * corresponding transformation that is applied to the scroll content. Together, + * they create the animation by first updating the scroll value then applying + * the transformation and animating the transition. Both pieces are necessary + * for the scroll animation to work. The values are used as-is by the tab + * scroller animation method, ensuring that all logic for determining scroll + * position or transformation is abstracted away from the animation method. + */ +export interface MDCTabScrollerAnimation { + finalScrollPosition: number; + scrollDelta: number; +} + +/** + * MDCTabScrollerHorizontalEdges represents the left and right edges of the + * scroll content. These values vary depending on how scrolling in RTL is + * implemented by the browser. One value is always 0 and one value is always + * the max scrollable value as either a positive or negative integer. + */ +export interface MDCTabScrollerHorizontalEdges { + left: number; + right: number; +} diff --git a/packages/mdc-tab-scroller/util.js b/packages/mdc-tab-scroller/util.ts similarity index 69% rename from packages/mdc-tab-scroller/util.js rename to packages/mdc-tab-scroller/util.ts index ea43f4ac101..640b74d9a87 100644 --- a/packages/mdc-tab-scroller/util.js +++ b/packages/mdc-tab-scroller/util.ts @@ -25,18 +25,14 @@ import {cssClasses} from './constants'; /** * Stores result from computeHorizontalScrollbarHeight to avoid redundant processing. - * @private {number|undefined} */ -let horizontalScrollbarHeight_; +let horizontalScrollbarHeight_: number | undefined; /** * Computes the height of browser-rendered horizontal scrollbars using a self-created test element. * May return 0 (e.g. on OS X browsers under default configuration). - * @param {!Document} documentObj - * @param {boolean=} shouldCacheResult - * @return {number} */ -function computeHorizontalScrollbarHeight(documentObj, shouldCacheResult = true) { +export function computeHorizontalScrollbarHeight(documentObj: Document, shouldCacheResult = true): number { if (shouldCacheResult && typeof horizontalScrollbarHeight_ !== 'undefined') { return horizontalScrollbarHeight_; } @@ -54,14 +50,20 @@ function computeHorizontalScrollbarHeight(documentObj, shouldCacheResult = true) return horizontalScrollbarHeight; } -/** - * @param {!Object} HTMLElementPrototype - * @return {string} - */ -function getMatchesProperty(HTMLElementPrototype) { - return [ - 'msMatchesSelector', 'matches', - ].filter((p) => p in HTMLElementPrototype).pop(); -} +export type VendorMatchesFunctionName = 'webkitMatchesSelector' | 'msMatchesSelector'; +export type MatchesFunctionName = VendorMatchesFunctionName | 'matches'; -export {computeHorizontalScrollbarHeight, getMatchesProperty}; +export function getMatchesProperty(htmlElementPrototype: {}): MatchesFunctionName { + // Order is important because we return the first existing method we find. + // Do not change the order of the items in the below array. + const matchesMethods: MatchesFunctionName[] = ['matches', 'webkitMatchesSelector', 'msMatchesSelector']; + let method: MatchesFunctionName = 'matches'; + for (const matchesMethod of matchesMethods) { + if (matchesMethod in htmlElementPrototype) { + method = matchesMethod; + break; + } + } + + return method; +} diff --git a/packages/mdc-tab/README.md b/packages/mdc-tab/README.md index f9e655c2ff9..592e1ffa1f4 100644 --- a/packages/mdc-tab/README.md +++ b/packages/mdc-tab/README.md @@ -146,7 +146,7 @@ Property | Value Type | Description Method Signature | Description --- | --- -`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the indicator. `previousIndicatorClientRect` is an optional argument. +`activate(previousIndicatorClientRect?: ClientRect) => void` | Activates the indicator. `previousIndicatorClientRect` is an optional argument. `deactivate() => void` | Deactivates the indicator. `focus() => void` | Focuses the tab. `computeIndicatorClientRect() => ClientRect` | Returns the bounding client rect of the indicator. @@ -168,7 +168,7 @@ Method Signature | Description `removeClass(className: string) => void` | Removes a class from the root element. `hasClass(className: string) => boolean` | Returns true if the root element contains the given class. `setAttr(attr: string, value: string) => void` | Sets the given attribute on the root element to the given value. -`activateIndicator(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab indicator subcomponent. `previousIndicatorClientRect` is an optional argument. +`activateIndicator(previousIndicatorClientRect?: ClientRect) => void` | Activates the tab indicator subcomponent. `previousIndicatorClientRect` is an optional argument. `deactivateIndicator() => void` | Deactivates the tab indicator subcomponent. `getOffsetLeft() => number` | Returns the `offsetLeft` value of the root element. `getOffsetWidth() => number` | Returns the `offsetWidth` value of the root element. @@ -184,13 +184,13 @@ Method Signature | Description `handleClick() => void` | Handles the logic for the `"click"` event. `isActive() => boolean` | Returns whether the tab is active. `setFocusOnActivate(focusOnActivate: boolean) => void` | Sets whether the tab should focus itself when activated. -`activate(previousIndicatorClientRect: ClientRect=) => void` | Activates the tab. `previousIndicatorClientRect` is an optional argument. +`activate(previousIndicatorClientRect?: ClientRect) => void` | Activates the tab. `previousIndicatorClientRect` is an optional argument. `deactivate() => void` | Deactivates the tab. `computeDimensions() => MDCTabDimensions` | Returns the dimensions of the tab. ### `MDCTabFoundation` Event Handlers -When wrapping the Tab component, it is necessary to register the following event handler. For an example of this, see the [MDCTab](index.js) component's `initialSyncWithDOM` method. +When wrapping the Tab component, it is necessary to register the following event handler. For an example of this, see the [MDCTab](index.ts) component's `initialSyncWithDOM` method. Event | Element | Foundation Handler --- | --- | --- diff --git a/packages/mdc-tab/adapter.js b/packages/mdc-tab/adapter.ts similarity index 53% rename from packages/mdc-tab/adapter.js rename to packages/mdc-tab/adapter.ts index 868c53a5daa..036686136e6 100644 --- a/packages/mdc-tab/adapter.js +++ b/packages/mdc-tab/adapter.ts @@ -21,105 +21,77 @@ * THE SOFTWARE. */ -/* eslint no-unused-vars: [2, {"args": "none"}] */ - /** - * MDCTabDimensions provides details about the left and right edges of the Tab - * root element and the Tab content element. These values are used to determine - * the visual position of the Tab with respect it's parent container. - * @typedef {{rootLeft: number, rootRight: number, contentLeft: number, contentRight: number}} - */ -let MDCTabDimensions; - -/** - * @typedef {{ - * detail: { - * tabId: string, - * }, - * bubbles: boolean, - * }} - */ -let MDCTabInteractionEventType; - -/** - * Adapter for MDC Tab. - * - * Defines the shape of the adapter expected by the foundation. Implement this - * adapter to integrate the Tab 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 MDCTabAdapter { +interface MDCTabAdapter { /** * Adds the given className to the root element. - * @param {string} className The className to add + * @param className The className to add */ - addClass(className) {} + addClass(className: string): void; /** * Removes the given className from the root element. - * @param {string} className The className to remove + * @param className The className to remove */ - removeClass(className) {} + removeClass(className: string): void; /** * Returns whether the root element has the given className. - * @param {string} className The className to remove - * @return {boolean} + * @param className The className to remove */ - hasClass(className) {} + hasClass(className: string): boolean; /** * Sets the given attrName of the root element to the given value. - * @param {string} attr The attribute name to set - * @param {string} value The value so give the attribute + * @param attr The attribute name to set + * @param value The value so give the attribute */ - setAttr(attr, value) {} + setAttr(attr: string, value: string): void; /** * Activates the indicator element. - * @param {!ClientRect=} previousIndicatorClientRect The client rect of the previously activated indicator + * @param previousIndicatorClientRect The client rect of the previously activated indicator */ - activateIndicator(previousIndicatorClientRect) {} + activateIndicator(previousIndicatorClientRect?: ClientRect): void; /** Deactivates the indicator. */ - deactivateIndicator() {} + deactivateIndicator(): void; /** * Emits the MDCTab:interacted event for use by parent components */ - notifyInteracted() {} + notifyInteracted(): void; /** * Returns the offsetLeft value of the root element. - * @return {number} */ - getOffsetLeft() {} + getOffsetLeft(): number; /** * Returns the offsetWidth value of the root element. - * @return {number} */ - getOffsetWidth() {} + getOffsetWidth(): number; /** * Returns the offsetLeft of the content element. - * @return {number} */ - getContentOffsetLeft() {} + getContentOffsetLeft(): number; /** * Returns the offsetWidth of the content element. - * @return {number} */ - getContentOffsetWidth() {} + getContentOffsetWidth(): number; /** * Applies focus to the root element */ - focus() {} + focus(): void; } -export {MDCTabDimensions, MDCTabInteractionEventType, MDCTabAdapter}; +export {MDCTabAdapter as default, MDCTabAdapter}; diff --git a/packages/mdc-tab/constants.js b/packages/mdc-tab/constants.ts similarity index 97% rename from packages/mdc-tab/constants.js rename to packages/mdc-tab/constants.ts index b628751f2f8..73a3ef224f7 100644 --- a/packages/mdc-tab/constants.js +++ b/packages/mdc-tab/constants.ts @@ -21,19 +21,17 @@ * THE SOFTWARE. */ -/** @enum {string} */ const cssClasses = { ACTIVE: 'mdc-tab--active', }; -/** @enum {string} */ const strings = { ARIA_SELECTED: 'aria-selected', - RIPPLE_SELECTOR: '.mdc-tab__ripple', CONTENT_SELECTOR: '.mdc-tab__content', - TAB_INDICATOR_SELECTOR: '.mdc-tab-indicator', - TABINDEX: 'tabIndex', INTERACTED_EVENT: 'MDCTab:interacted', + RIPPLE_SELECTOR: '.mdc-tab__ripple', + TABINDEX: 'tabIndex', + TAB_INDICATOR_SELECTOR: '.mdc-tab-indicator', }; export { diff --git a/packages/mdc-tab/foundation.js b/packages/mdc-tab/foundation.ts similarity index 65% rename from packages/mdc-tab/foundation.js rename to packages/mdc-tab/foundation.ts index 718da06be97..cb3f076d3b6 100644 --- a/packages/mdc-tab/foundation.js +++ b/packages/mdc-tab/foundation.ts @@ -22,90 +22,65 @@ */ import {MDCFoundation} from '@material/base/foundation'; +import {MDCTabAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCTabDimensions} from './types'; -/* eslint-disable no-unused-vars */ -import {MDCTabAdapter, MDCTabDimensions} from './adapter'; -/* eslint-enable no-unused-vars */ - -import { - cssClasses, - strings, -} from './constants'; - -/** - * @extends {MDCFoundation} - * @final - */ -class MDCTabFoundation extends MDCFoundation { - /** @return enum {string} */ +class MDCTabFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } - /** @return enum {string} */ static get strings() { return strings; } - /** - * @see MDCTabAdapter for typing information - * @return {!MDCTabAdapter} - */ - static get defaultAdapter() { - return /** @type {!MDCTabAdapter} */ ({ - addClass: () => {}, - removeClass: () => {}, - hasClass: () => {}, - setAttr: () => {}, - activateIndicator: () => {}, - deactivateIndicator: () => {}, - notifyInteracted: () => {}, - getOffsetLeft: () => {}, - getOffsetWidth: () => {}, - getContentOffsetLeft: () => {}, - getContentOffsetWidth: () => {}, - focus: () => {}, - }); + static get defaultAdapter(): MDCTabAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: () => undefined, + removeClass: () => undefined, + hasClass: () => false, + setAttr: () => undefined, + activateIndicator: () => undefined, + deactivateIndicator: () => undefined, + notifyInteracted: () => undefined, + getOffsetLeft: () => 0, + getOffsetWidth: () => 0, + getContentOffsetLeft: () => 0, + getContentOffsetWidth: () => 0, + focus: () => undefined, + }; + // tslint:enable:object-literal-sort-keys } - /** @param {!MDCTabAdapter} adapter */ - constructor(adapter) { - super(Object.assign(MDCTabFoundation.defaultAdapter, adapter)); + private focusOnActivate_ = true; - /** @private {boolean} */ - this.focusOnActivate_ = true; + constructor(adapter?: Partial) { + super({...MDCTabFoundation.defaultAdapter, ...adapter}); } - /** - * Handles the "click" event - */ handleClick() { // It's up to the parent component to keep track of the active Tab and // ensure we don't activate a Tab that's already active. this.adapter_.notifyInteracted(); } - /** - * Returns the Tab's active state - * @return {boolean} - */ - isActive() { + isActive(): boolean { return this.adapter_.hasClass(cssClasses.ACTIVE); } /** * Sets whether the tab should focus itself when activated - * @param {boolean} focusOnActivate */ - setFocusOnActivate(focusOnActivate) { + setFocusOnActivate(focusOnActivate: boolean) { this.focusOnActivate_ = focusOnActivate; } /** * Activates the Tab - * @param {!ClientRect=} previousIndicatorClientRect */ - activate(previousIndicatorClientRect) { + activate(previousIndicatorClientRect?: ClientRect) { this.adapter_.addClass(cssClasses.ACTIVE); this.adapter_.setAttr(strings.ARIA_SELECTED, 'true'); this.adapter_.setAttr(strings.TABINDEX, '0'); @@ -132,21 +107,20 @@ class MDCTabFoundation extends MDCFoundation { /** * Returns the dimensions of the Tab - * @return {!MDCTabDimensions} */ - computeDimensions() { + computeDimensions(): MDCTabDimensions { const rootWidth = this.adapter_.getOffsetWidth(); const rootLeft = this.adapter_.getOffsetLeft(); const contentWidth = this.adapter_.getContentOffsetWidth(); const contentLeft = this.adapter_.getContentOffsetLeft(); return { - rootLeft, - rootRight: rootLeft + rootWidth, contentLeft: rootLeft + contentLeft, contentRight: rootLeft + contentLeft + contentWidth, + rootLeft, + rootRight: rootLeft + rootWidth, }; } } -export default MDCTabFoundation; +export {MDCTabFoundation as default, MDCTabFoundation}; diff --git a/packages/mdc-tab/index.js b/packages/mdc-tab/index.js deleted file mode 100644 index 021dcbb5641..00000000000 --- a/packages/mdc-tab/index.js +++ /dev/null @@ -1,168 +0,0 @@ -/** - * @license - * Copyright 2018 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'; -import {MDCTabIndicator, MDCTabIndicatorFoundation} from '@material/tab-indicator/index'; -import {MDCTabAdapter, MDCTabDimensions, MDCTabInteractionEventType} from './adapter'; -/* eslint-enable no-unused-vars */ - -import MDCTabFoundation from './foundation'; - -/** - * @extends {MDCComponent} - * @final - */ -class MDCTab extends MDCComponent { - /** - * @param {...?} args - */ - constructor(...args) { - super(...args); - - /** @type {string} */ - this.id; - /** @private {?MDCRipple} */ - this.ripple_; - /** @private {?MDCTabIndicator} */ - this.tabIndicator_; - /** @private {?Element} */ - this.content_; - - /** @private {?Function} */ - this.handleClick_; - } - - /** - * @param {!Element} root - * @return {!MDCTab} - */ - static attachTo(root) { - return new MDCTab(root); - } - - initialize( - rippleFactory = (el, foundation) => new MDCRipple(el, foundation), - tabIndicatorFactory = (el) => new MDCTabIndicator(el)) { - this.id = this.root_.id; - const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR); - const rippleAdapter = Object.assign(MDCRipple.createAdapter(/** @type {!RippleCapableSurface} */ (this)), { - addClass: (className) => rippleSurface.classList.add(className), - removeClass: (className) => rippleSurface.classList.remove(className), - updateCssVariable: (varName, value) => rippleSurface.style.setProperty(varName, value), - }); - const rippleFoundation = new MDCRippleFoundation(rippleAdapter); - this.ripple_ = rippleFactory(this.root_, rippleFoundation); - - const tabIndicatorElement = this.root_.querySelector(MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR); - this.tabIndicator_ = tabIndicatorFactory(tabIndicatorElement); - - this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR); - } - - initialSyncWithDOM() { - this.handleClick_ = this.foundation_.handleClick.bind(this.foundation_); - this.listen('click', this.handleClick_); - } - - destroy() { - this.unlisten('click', /** @type {!Function} */ (this.handleClick_)); - this.ripple_.destroy(); - super.destroy(); - } - - /** - * @return {!MDCTabFoundation} - */ - getDefaultFoundation() { - return new MDCTabFoundation( - /** @type {!MDCTabAdapter} */ ({ - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect), - deactivateIndicator: () => this.tabIndicator_.deactivate(), - notifyInteracted: () => this.emit( - MDCTabFoundation.strings.INTERACTED_EVENT, {tabId: this.id}, true /* bubble */), - getOffsetLeft: () => this.root_.offsetLeft, - getOffsetWidth: () => this.root_.offsetWidth, - getContentOffsetLeft: () => this.content_.offsetLeft, - getContentOffsetWidth: () => this.content_.offsetWidth, - focus: () => this.root_.focus(), - })); - } - - /** - * Getter for the active state of the tab - * @return {boolean} - */ - get active() { - return this.foundation_.isActive(); - } - - set focusOnActivate(focusOnActivate) { - this.foundation_.setFocusOnActivate(focusOnActivate); - } - - /** - * Activates the tab - * @param {!ClientRect=} computeIndicatorClientRect - */ - activate(computeIndicatorClientRect) { - this.foundation_.activate(computeIndicatorClientRect); - } - - /** - * Deactivates the tab - */ - deactivate() { - this.foundation_.deactivate(); - } - - /** - * Returns the indicator's client rect - * @return {!ClientRect} - */ - computeIndicatorClientRect() { - return this.tabIndicator_.computeContentClientRect(); - } - - /** - * @return {!MDCTabDimensions} - */ - computeDimensions() { - return this.foundation_.computeDimensions(); - } - - /** - * Focuses the tab - */ - focus() { - this.root_.focus(); - } -} - -export {MDCTab, MDCTabFoundation}; diff --git a/packages/mdc-tab/index.ts b/packages/mdc-tab/index.ts new file mode 100644 index 00000000000..c33ba92f1b5 --- /dev/null +++ b/packages/mdc-tab/index.ts @@ -0,0 +1,144 @@ +/** + * @license + * Copyright 2018 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 {SpecificEventListener} from '@material/base/types'; +import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; +import {MDCTabIndicator} from '@material/tab-indicator/index'; +import {MDCTabFoundation} from './foundation'; +import {MDCTabDimensions, RippleFactory, TabIndicatorFactory, TabInteractionEventDetail} from './types'; + +class MDCTab extends MDCComponent implements RippleCapableSurface { + static attachTo(root: Element): MDCTab { + return new MDCTab(root); + } + + id!: string; // assigned in initialize(); + + // Public visibility for this property is required by RippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + + private ripple_!: MDCRipple; // assigned in initialize(); + private tabIndicator_!: MDCTabIndicator; // assigned in initialize(); + private content_!: HTMLElement; // assigned in initialize(); + private handleClick_!: SpecificEventListener<'click'>; // assigned in initialize(); + + initialize( + rippleFactory: RippleFactory = (el, foundation) => new MDCRipple(el, foundation), + tabIndicatorFactory: TabIndicatorFactory = (el) => new MDCTabIndicator(el), + ) { + this.id = this.root_.id; + const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR)!; + const rippleAdapter = { + ...MDCRipple.createAdapter(this), + addClass: (className: string) => rippleSurface.classList.add(className), + removeClass: (className: string) => rippleSurface.classList.remove(className), + updateCssVariable: (varName: string, value: string) => rippleSurface.style.setProperty(varName, value), + }; + const rippleFoundation = new MDCRippleFoundation(rippleAdapter); + this.ripple_ = rippleFactory(this.root_, rippleFoundation); + + const tabIndicatorElement = this.root_.querySelector(MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR)!; + this.tabIndicator_ = tabIndicatorFactory(tabIndicatorElement); + this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR)!; + } + + initialSyncWithDOM() { + this.handleClick_ = () => this.foundation_.handleClick(); + this.listen('click', this.handleClick_); + } + + destroy() { + this.unlisten('click', this.handleClick_); + this.ripple_.destroy(); + super.destroy(); + } + + getDefaultFoundation(): MDCTabFoundation { + // tslint:disable:object-literal-sort-keys + return new MDCTabFoundation({ + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect), + deactivateIndicator: () => this.tabIndicator_.deactivate(), + notifyInteracted: () => this.emit( + MDCTabFoundation.strings.INTERACTED_EVENT, {tabId: this.id}, true /* bubble */), + getOffsetLeft: () => this.root_.offsetLeft, + getOffsetWidth: () => this.root_.offsetWidth, + getContentOffsetLeft: () => this.content_.offsetLeft, + getContentOffsetWidth: () => this.content_.offsetWidth, + focus: () => this.root_.focus(), + }); + // tslint:enable:object-literal-sort-keys + } + + /** + * Getter for the active state of the tab + */ + get active(): boolean { + return this.foundation_.isActive(); + } + + set focusOnActivate(focusOnActivate: boolean) { + this.foundation_.setFocusOnActivate(focusOnActivate); + } + + /** + * Activates the tab + */ + activate(computeIndicatorClientRect?: ClientRect) { + this.foundation_.activate(computeIndicatorClientRect); + } + + /** + * Deactivates the tab + */ + deactivate() { + this.foundation_.deactivate(); + } + + /** + * Returns the indicator's client rect + */ + computeIndicatorClientRect(): ClientRect { + return this.tabIndicator_.computeContentClientRect(); + } + + computeDimensions(): MDCTabDimensions { + return this.foundation_.computeDimensions(); + } + + /** + * Focuses the tab + */ + focus() { + this.root_.focus(); + } +} + +export {MDCTab as default, MDCTab}; +export * from './adapter'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-tab/types.ts b/packages/mdc-tab/types.ts new file mode 100644 index 00000000000..7058fa4c595 --- /dev/null +++ b/packages/mdc-tab/types.ts @@ -0,0 +1,47 @@ +/** + * @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 {MDCRippleFoundation} from '@material/ripple/foundation'; +import {MDCRipple} from '@material/ripple/index'; +import {MDCTabIndicator} from '@material/tab-indicator/index'; + +/** + * MDCTabDimensions provides details about the left and right edges of the Tab + * root element and the Tab content element. These values are used to determine + * the visual position of the Tab with respect it's parent container. + */ +export interface MDCTabDimensions { + rootLeft: number; + rootRight: number; + contentLeft: number; + contentRight: number; +} + +export type TabInteractionEvent = CustomEvent; + +export interface TabInteractionEventDetail { + tabId: string; +} + +export type RippleFactory = (el: Element, foundation: MDCRippleFoundation) => MDCRipple; +export type TabIndicatorFactory = (el: Element) => MDCTabIndicator; 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 0ee4ec5deb1..9d709fd4e38 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -173,17 +173,17 @@ class JsBundleFactory { notchedOutline: getAbsolutePath('/packages/mdc-notched-outline/index.ts'), radio: getAbsolutePath('/packages/mdc-radio/index.ts'), ripple: getAbsolutePath('/packages/mdc-ripple/index.ts'), - select: getAbsolutePath('/packages/mdc-select/index.js'), + select: getAbsolutePath('/packages/mdc-select/index.ts'), selectionControl: getAbsolutePath('/packages/mdc-selection-control/index.ts'), slider: getAbsolutePath('/packages/mdc-slider/index.ts'), snackbar: getAbsolutePath('/packages/mdc-snackbar/index.ts'), switch: getAbsolutePath('/packages/mdc-switch/index.ts'), - tab: getAbsolutePath('/packages/mdc-tab/index.js'), - tabBar: getAbsolutePath('/packages/mdc-tab-bar/index.js'), - tabIndicator: getAbsolutePath('/packages/mdc-tab-indicator/index.js'), - tabScroller: getAbsolutePath('/packages/mdc-tab-scroller/index.js'), + tab: getAbsolutePath('/packages/mdc-tab/index.ts'), + tabBar: getAbsolutePath('/packages/mdc-tab-bar/index.ts'), + 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-select/mdc-select.test.js b/test/unit/mdc-select/mdc-select.test.js index 9c0b06fceef..a32f7c30caa 100644 --- a/test/unit/mdc-select/mdc-select.test.js +++ b/test/unit/mdc-select/mdc-select.test.js @@ -130,6 +130,10 @@ test('attachTo returns a component instance', () => { assert.isOk(MDCSelect.attachTo(getFixture()) instanceof MDCSelect); }); +test('constructor throws an error when required elements are missing', () => { + assert.throws(() => new MDCSelect(bel`
`), 'Missing required element'); +}); + function setupTest(hasOutline = false, hasLabel = true, hasHelperText = false) { const bottomLine = new FakeBottomLine(); const label = new FakeLabel(); diff --git a/test/unit/mdc-tab-indicator/fading-foundation.test.js b/test/unit/mdc-tab-indicator/fading-foundation.test.js index e915173ceeb..1aca6c8e96a 100644 --- a/test/unit/mdc-tab-indicator/fading-foundation.test.js +++ b/test/unit/mdc-tab-indicator/fading-foundation.test.js @@ -24,7 +24,7 @@ import td from 'testdouble'; import {setupFoundationTest} from '../helpers/setup'; -import MDCFadingTabIndicatorFoundation from '../../../packages/mdc-tab-indicator/fading-foundation'; +import {MDCFadingTabIndicatorFoundation} from '../../../packages/mdc-tab-indicator/fading-foundation'; suite('MDCFadingTabIndicatorFoundation'); @@ -37,7 +37,9 @@ test(`#activate adds the ${MDCFadingTabIndicatorFoundation.cssClasses.ACTIVE} cl }); test(`#deactivate removes the ${MDCFadingTabIndicatorFoundation.cssClasses.ACTIVE} class`, () => { - const {foundation, mockAdapter} = setupTest(); + const foundation = new MDCFadingTabIndicatorFoundation(); + const adapter = foundation.adapter_; + adapter.removeClass = td.func('removeClass'); foundation.deactivate(); - td.verify(mockAdapter.removeClass(MDCFadingTabIndicatorFoundation.cssClasses.ACTIVE)); + td.verify(adapter.removeClass(MDCFadingTabIndicatorFoundation.cssClasses.ACTIVE)); }); diff --git a/test/unit/mdc-tab-indicator/foundation.test.js b/test/unit/mdc-tab-indicator/foundation.test.js index 01598e46f3a..f670b659cef 100644 --- a/test/unit/mdc-tab-indicator/foundation.test.js +++ b/test/unit/mdc-tab-indicator/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabIndicatorFoundation from '../../../packages/mdc-tab-indicator/foundation'; +import {MDCTabIndicatorFoundation} from '../../../packages/mdc-tab-indicator/foundation'; suite('MDCTabIndicatorFoundation'); @@ -48,22 +48,16 @@ test('defaultAdapter returns a complete adapter implementation', () => { const setupTest = () => setupFoundationTest(MDCTabIndicatorFoundation); -test('#computeContentClientRect returns the client rect', () => { +test('#computeContentClientRect returns adapter value', () => { const {foundation, mockAdapter} = setupTest(); foundation.computeContentClientRect(); td.verify(mockAdapter.computeContentClientRect(), {times: 1}); }); -test('#activate is abstract and does nothing', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.activate(); - td.verify(mockAdapter.addClass(td.matchers.isA(String)), {times: 0}); - td.verify(mockAdapter.removeClass(td.matchers.isA(String)), {times: 0}); -}); - -test('#deactivate is abstract and does nothing', () => { - const {foundation, mockAdapter} = setupTest(); - foundation.deactivate(); - td.verify(mockAdapter.addClass(td.matchers.isA(String)), {times: 0}); - td.verify(mockAdapter.removeClass(td.matchers.isA(String)), {times: 0}); +test('#computeContentClientRect returns default client rect', () => { + const foundation = new MDCTabIndicatorFoundation(); + assert.deepEqual( + foundation.computeContentClientRect(), + {top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0} + ); }); diff --git a/test/unit/mdc-tab-indicator/sliding-foundation.test.js b/test/unit/mdc-tab-indicator/sliding-foundation.test.js index ad5089cad57..c9113a8ac74 100644 --- a/test/unit/mdc-tab-indicator/sliding-foundation.test.js +++ b/test/unit/mdc-tab-indicator/sliding-foundation.test.js @@ -24,7 +24,7 @@ import td from 'testdouble'; import {setupFoundationTest} from '../helpers/setup'; -import MDCSlidingTabIndicatorFoundation from '../../../packages/mdc-tab-indicator/sliding-foundation'; +import {MDCSlidingTabIndicatorFoundation} from '../../../packages/mdc-tab-indicator/sliding-foundation'; suite('MDCSlidingTabIndicatorFoundation'); @@ -56,7 +56,9 @@ test('#activate does not modify transform and does not transition if no client r }); test(`#deactivate removes the ${MDCSlidingTabIndicatorFoundation.cssClasses.ACTIVE} class`, () => { - const {foundation, mockAdapter} = setupTest(); + const foundation = new MDCSlidingTabIndicatorFoundation(); + const adapter = foundation.adapter_; + adapter.removeClass = td.func('removeClass'); foundation.deactivate(); - td.verify(mockAdapter.removeClass(MDCSlidingTabIndicatorFoundation.cssClasses.ACTIVE)); + td.verify(adapter.removeClass(MDCSlidingTabIndicatorFoundation.cssClasses.ACTIVE)); }); diff --git a/test/unit/mdc-tab-scroller/rtl-scroller.test.js b/test/unit/mdc-tab-scroller/rtl-scroller.test.js deleted file mode 100644 index 77e30109c30..00000000000 --- a/test/unit/mdc-tab-scroller/rtl-scroller.test.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright 2018 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 {assert} from 'chai'; - -import {setupFoundationTest} from '../helpers/setup'; -import MDCTabScrollerFoundation from '../../../packages/mdc-tab-scroller/foundation'; -import MDCTabScrollerRTL from '../../../packages/mdc-tab-scroller/rtl-scroller'; - -suite('MDCTabScrollerRTL'); - -const setup = () => { - const {mockAdapter} = setupFoundationTest(MDCTabScrollerFoundation); - const scroller = new MDCTabScrollerRTL(mockAdapter); - return {scroller}; -}; - -test('#getScrollPositionRTL() is abstract and does nothing', () => { - const {scroller} = setup(); - assert.isUndefined(scroller.getScrollPositionRTL()); -}); - -test('#scrollToRTL() is abstract and does nothing', () => { - const {scroller} = setup(); - assert.isUndefined(scroller.scrollToRTL()); -}); - -test('#incrementScrollRTL() is abstract and does nothing', () => { - const {scroller} = setup(); - assert.isUndefined(scroller.incrementScrollRTL()); -}); - -test('#getAnimatingScrollPosition() is abstract and does nothing', () => { - const {scroller} = setup(); - assert.isUndefined(scroller.getAnimatingScrollPosition()); -}); diff --git a/test/unit/mdc-tab/mdc-tab.test.js b/test/unit/mdc-tab/mdc-tab.test.js index 9f81fe8d50a..9005a565774 100644 --- a/test/unit/mdc-tab/mdc-tab.test.js +++ b/test/unit/mdc-tab/mdc-tab.test.js @@ -60,7 +60,7 @@ test('click handler is added during initialSyncWithDOM', () => { const {component, root, mockFoundation} = setupTest({createMockFoundation: true}); domEvents.emit(root, 'click'); - td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 1}); + td.verify(mockFoundation.handleClick(), {times: 1}); component.destroy(); }); @@ -70,7 +70,7 @@ test('click handler is removed during destroy', () => { component.destroy(); domEvents.emit(root, 'click'); - td.verify(mockFoundation.handleClick(td.matchers.anything()), {times: 0}); + td.verify(mockFoundation.handleClick(), {times: 0}); }); test('#destroy removes the ripple', () => { 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}; }