diff --git a/package.json b/package.json index 23bf0987d42..d9117647189 100644 --- a/package.json +++ b/package.json @@ -226,6 +226,7 @@ "mdc-notched-outline", "mdc-radio", "mdc-ripple", + "mdc-select", "mdc-selection-control", "mdc-slider", "mdc-switch", diff --git a/packages/mdc-select/README.md b/packages/mdc-select/README.md index bed761707dc..238fdec6c85 100644 --- a/packages/mdc-select/README.md +++ b/packages/mdc-select/README.md @@ -244,10 +244,17 @@ If you are using a JavaScript framework, such as React or Angular, you can creat | --- | --- | | `addClass(className: string) => void` | Adds a class to the root element. | | `removeClass(className: string) => void` | Removes a class from the root element. | -| `floatLabel(value: boolean) => void` | Floats or defloats label. | +| `hasClass(className: string) => boolean` | Returns true if the root element has the className in its classList. | | `activateBottomLine() => void` | Activates the bottom line component. | | `deactivateBottomLine() => void` | Deactivates the bottom line component. | | `getValue() => string` | Returns the value selected on the `select` element. | +| `isRtl() => boolean` | Returns true if a parent of the root element is in RTL. | +| `hasLabel() => boolean` | Returns true if the `select` has a label associated with it. | +| `floatLabel(value: boolean) => void` | Floats or defloats label. | +| `getLabelWidth() => number` | Returns the offsetWidth of the label element. | +| `hasOutline() => boolean` | Returns true if the `select` has the notched outline element. | +| `notchOutline(labelWidth: number, isRtl, boolean) => void` | Switches the notched outline element to its "notched state." | +| `closeOutline() => void` | Switches the notched outline element to its closed state. | ### `MDCSelectFoundation` diff --git a/packages/mdc-select/adapter.js b/packages/mdc-select/adapter.js new file mode 100644 index 00000000000..faec916dfc4 --- /dev/null +++ b/packages/mdc-select/adapter.js @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint no-unused-vars: [2, {"args": "none"}] */ + +/** + * Adapter for MDC Select. Provides an interface for managing + * - classes + * - dom + * - event handlers + * + * Additionally, provides type information for the adapter to the Closure + * compiler. + * + * 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 { + /** + * Adds class to 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) {} + + /** + * Activates the bottom line, showing a focused state. + */ + activateBottomLine() {} + + /** + * Deactivates the bottom line. + */ + deactivateBottomLine() {} + + /** + * Returns the selected value of the select element. + * @return {string} + */ + getValue() {} + + /** + * Returns true if the direction of the root element is set to RTL. + * @return {boolean} + */ + isRtl() {} + + /** + * Returns true if label element exists, false if it doesn't. + * @return {boolean} + */ + hasLabel() {} + + /** + * Floats label determined based off of the shouldFloat argument. + * @param {boolean} shouldFloat + */ + floatLabel(shouldFloat) {} + + /** + * Returns width of label in pixels, if the label exists. + * @return {number} + */ + getLabelWidth() {} + + /** + * Returns true if outline element exists, false if it doesn't. + * @return {boolean} + */ + hasOutline() {} + + /** + * Updates SVG Path and outline element based on the + * label element width and RTL context, if the outline exists. + * @param {number} labelWidth + * @param {boolean=} isRtl + */ + notchOutline(labelWidth, isRtl) {} + + /** + * Closes notch in outline element, if the outline exists. + */ + closeOutline() {} +} + +export default MDCSelectAdapter; diff --git a/packages/mdc-select/constants.js b/packages/mdc-select/constants.js index 36d42b64a17..9ad8a5add11 100644 --- a/packages/mdc-select/constants.js +++ b/packages/mdc-select/constants.js @@ -1,4 +1,5 @@ /** + * @license * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** @enum {string} */ const cssClasses = { BOX: 'mdc-select--box', DISABLED: 'mdc-select--disabled', @@ -20,6 +23,7 @@ const cssClasses = { OUTLINED: 'mdc-select--outlined', }; +/** @enum {string} */ const strings = { CHANGE_EVENT: 'MDCSelect:change', LINE_RIPPLE_SELECTOR: '.mdc-line-ripple', diff --git a/packages/mdc-select/foundation.js b/packages/mdc-select/foundation.js index 1bb741c64d0..d4742685fb9 100644 --- a/packages/mdc-select/foundation.js +++ b/packages/mdc-select/foundation.js @@ -15,46 +15,65 @@ */ import {MDCFoundation} from '@material/base/index'; +/* eslint-disable no-unused-vars */ +import MDCSelectAdapter from './adapter'; +/* eslint-enable no-unused-vars */ import {cssClasses, strings, numbers} from './constants'; -export default class MDCSelectFoundation extends MDCFoundation { +/** + * @extends {MDCFoundation} + * @final + */ +class MDCSelectFoundation extends MDCFoundation { + /** @return enum {string} */ 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} + */ static get defaultAdapter() { - return { + return /** @type {!MDCSelectAdapter} */ ({ addClass: (/* className: string */) => {}, removeClass: (/* className: string */) => {}, hasClass: (/* className: string */) => false, - floatLabel: (/* value: boolean */) => {}, activateBottomLine: () => {}, deactivateBottomLine: () => {}, getValue: () => {}, isRtl: () => false, - hasLabel: () => {}, + hasLabel: () => false, + floatLabel: (/* value: boolean */) => {}, getLabelWidth: () => {}, - hasOutline: () => {}, - notchOutline: () => {}, + hasOutline: () => false, + notchOutline: (/* labelWidth: number, isRtl: boolean */) => {}, closeOutline: () => {}, - }; + }); } + /** + * @param {!MDCSelectAdapter} adapter + */ constructor(adapter) { super(Object.assign(MDCSelectFoundation.defaultAdapter, adapter)); - - this.focusHandler_ = (evt) => this.handleFocus_(evt); - this.blurHandler_ = (evt) => this.handleBlur_(evt); } + /** + * Updates the styles of the select to show the disasbled state. + * @param {boolean} disabled + */ updateDisabledStyle(disabled) { const {DISABLED} = MDCSelectFoundation.cssClasses; if (disabled) { @@ -64,18 +83,27 @@ export default class MDCSelectFoundation extends MDCFoundation { } } + /** + * Handles value changes, via change event or programmatic updates. + */ handleChange() { const optionHasValue = this.adapter_.getValue().length > 0; this.adapter_.floatLabel(optionHasValue); this.notchOutline(optionHasValue); } + /** + * Handles focus events from root element. + */ handleFocus() { this.adapter_.floatLabel(true); this.notchOutline(true); this.adapter_.activateBottomLine(); } + /** + * Handles blur events from root element. + */ handleBlur() { this.handleChange(); this.adapter_.deactivateBottomLine(); @@ -100,3 +128,5 @@ export default class MDCSelectFoundation extends MDCFoundation { } } } + +export default MDCSelectFoundation; diff --git a/packages/mdc-select/index.js b/packages/mdc-select/index.js index 9799ca0d706..e491bd3ea86 100644 --- a/packages/mdc-select/index.js +++ b/packages/mdc-select/index.js @@ -21,37 +21,84 @@ import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; import {MDCNotchedOutline} from '@material/notched-outline/index'; import MDCSelectFoundation from './foundation'; +import MDCSelectAdapter from './adapter'; import {cssClasses, strings} from './constants'; -export {MDCSelectFoundation}; +/** + * @extends MDCComponent + */ +class MDCSelect extends MDCComponent { + /** + * @param {...?} args + */ + constructor(...args) { + super(...args); + /** @private {?Element} */ + this.nativeControl_; + /** @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_; + } -export class MDCSelect extends MDCComponent { + /** + * @param {!Element} root + * @return {!MDCSelect} + */ static attachTo(root) { return new MDCSelect(root); } + /** + * @return {string} The value of the select. + */ get value() { return this.nativeControl_.value; } + /** + * @param {string} value The value to set on the select. + */ set value(value) { this.nativeControl_.value = value; this.foundation_.handleChange(); } + /** + * @return {number} The selected index of the select. + */ get selectedIndex() { return this.nativeControl_.selectedIndex; } + /** + * @param {number} selectedIndex The index of the option to be set on the select. + */ set selectedIndex(selectedIndex) { this.nativeControl_.selectedIndex = selectedIndex; this.foundation_.handleChange(); } + /** + * @return {boolean} True if the select is disabled. + */ get disabled() { return this.nativeControl_.disabled; } + /** + * @param {boolean} disabled Sets the select disabled or enabled. + */ set disabled(disabled) { this.nativeControl_.disabled = disabled; this.foundation_.updateDisabledStyle(disabled); @@ -65,6 +112,12 @@ export class MDCSelect extends MDCComponent { this.foundation_.notchOutline(openNotch); } + + /** + * @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. + */ initialize( labelFactory = (el) => new MDCFloatingLabel(el), lineRippleFactory = (el) => new MDCLineRipple(el), @@ -88,6 +141,10 @@ export class MDCSelect extends MDCComponent { } } + /** + * @private + * @return {!MDCRipple} + */ initRipple_() { const adapter = Object.assign(MDCRipple.createAdapter(this), { registerInteractionHandler: (type, handler) => this.nativeControl_.addEventListener(type, handler), @@ -97,6 +154,10 @@ export class MDCSelect extends MDCComponent { 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(); this.handleFocus_ = () => this.foundation_.handleFocus(); @@ -129,37 +190,44 @@ export class MDCSelect extends MDCComponent { super.destroy(); } + /** + * @return {!MDCSelectFoundation} + */ getDefaultFoundation() { - return new MDCSelectFoundation((Object.assign({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - activateBottomLine: () => { - if (this.lineRipple_) { - this.lineRipple_.activate(); - } - }, - deactivateBottomLine: () => { - if (this.lineRipple_) { - this.lineRipple_.deactivate(); - } + return new MDCSelectFoundation( + /** @type {!MDCSelectAdapter} */ (Object.assign({ + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + getValue: () => this.nativeControl_.value, + isRtl: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + activateBottomLine: () => { + if (this.lineRipple_) { + this.lineRipple_.activate(); + } + }, + deactivateBottomLine: () => { + if (this.lineRipple_) { + this.lineRipple_.deactivate(); + } + }, }, - isRtl: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', - getValue: () => this.nativeControl_.value, - }, - this.getOutlineAdapterMethods_(), - this.getLabelAdapterMethods_())) + this.getOutlineAdapterMethods_(), + this.getLabelAdapterMethods_()) + ) ); } /** * @return {!{ - * notchOutline: function(number, boolean): undefined, * hasOutline: function(): boolean, + * notchOutline: function(number, boolean): undefined, + * closeOutline: function(): undefined, * }} */ getOutlineAdapterMethods_() { return { + hasOutline: () => !!this.outline_, notchOutline: (labelWidth, isRtl) => { if (this.outline_) { this.outline_.notch(labelWidth, isRtl); @@ -170,25 +238,24 @@ export class MDCSelect extends MDCComponent { this.outline_.closeNotch(); } }, - hasOutline: () => !!this.outline_, }; } /** * @return {!{ - * floatLabel: function(boolean): undefined, * hasLabel: function(): boolean, + * floatLabel: function(boolean): undefined, * getLabelWidth: function(): number, * }} */ getLabelAdapterMethods_() { return { + hasLabel: () => !!this.label_, floatLabel: (shouldFloat) => { if (this.label_) { this.label_.float(shouldFloat); } }, - hasLabel: () => !!this.label_, getLabelWidth: () => { if (this.label_) { return this.label_.getWidth(); @@ -197,3 +264,5 @@ export class MDCSelect extends MDCComponent { }; } } + +export {MDCSelect, MDCSelectFoundation};