diff --git a/demos/switch.html b/demos/switch.html index 9f8d8978dfd..360affd0842 100644 --- a/demos/switch.html +++ b/demos/switch.html @@ -18,6 +18,7 @@ Switch - Material Components Catalog + @@ -63,9 +64,11 @@
- -
-
+
+
+
+ +
@@ -74,21 +77,25 @@

Enabled

- -
-
+
+
+
+ +
-
- -
-
+
+
+
+
+ +
@@ -97,14 +104,16 @@

Enabled

Disabled

-
- -
-
+
+
+
+
+ +
@@ -134,10 +143,16 @@

Disabled

diff --git a/demos/switch.scss b/demos/switch.scss index 22df5f0b2b0..5ca4316327c 100644 --- a/demos/switch.scss +++ b/demos/switch.scss @@ -28,9 +28,7 @@ .demo-switch--custom { $color: $material-color-red-500; - @include mdc-switch-track-color($color); - @include mdc-switch-knob-color($color); - @include mdc-switch-focus-indicator-color($color); + @include mdc-switch-toggled-on-color($color); } .rtl-toggle { diff --git a/demos/theme/index.html b/demos/theme/index.html index 0ef009d2603..d5081f2f3c2 100644 --- a/demos/theme/index.html +++ b/demos/theme/index.html @@ -677,19 +677,23 @@

-
- -
-
+
+
+
+
+ +
-
- -
-
+
+
+
+
+ +
@@ -933,6 +937,14 @@

mdc.slider.MDCSlider.attachTo(slider); }); + /* + * Switch + */ + + [].forEach.call(document.querySelectorAll('.mdc-switch'), function(switchEl) { + mdc.switchComponent.MDCSwitch.attachTo(switchEl); + }); + /* * Tab Bar */ diff --git a/package.json b/package.json index f6a64c93120..a5db57c6fda 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,7 @@ "mdc-ripple", "mdc-selection-control", "mdc-slider", + "mdc-switch", "mdc-tab", "mdc-textfield", "mdc-top-app-bar" diff --git a/packages/material-components-web/README.md b/packages/material-components-web/README.md index f5201a24560..42a59d16c2d 100644 --- a/packages/material-components-web/README.md +++ b/packages/material-components-web/README.md @@ -27,6 +27,8 @@ import { checkbox } from 'material-components-web'; const checkbox = new checkbox.MDCCheckbox(document.querySelector('.mdc-checkbox')); ``` +> NOTE: Since switch is a reserved word in JS, switch is instead named `switchControl`. + > NOTE: Built CSS files as well as UMD JS bundles will be available as part of the package > post-alpha. diff --git a/packages/material-components-web/index.js b/packages/material-components-web/index.js index f0d7354af01..8f19259ccc5 100644 --- a/packages/material-components-web/index.js +++ b/packages/material-components-web/index.js @@ -36,6 +36,7 @@ import * as select from '@material/select/index'; import * as selectionControl from '@material/selection-control/index'; import * as slider from '@material/slider/index'; import * as snackbar from '@material/snackbar/index'; +import * as switchControl from '@material/switch/index'; import * as tabs from '@material/tabs/index'; import * as textField from '@material/textfield/index'; import * as toolbar from '@material/toolbar/index'; @@ -66,6 +67,7 @@ autoInit.register('MDCTextField', textField.MDCTextField); autoInit.register('MDCMenu', menu.MDCMenu); autoInit.register('MDCSelect', select.MDCSelect); autoInit.register('MDCSlider', slider.MDCSlider); +autoInit.register('MDCSwitch', switchControl.MDCSwitch); autoInit.register('MDCToolbar', toolbar.MDCToolbar); autoInit.register('MDCTopAppBar', topAppBar.MDCTopAppBar); @@ -92,6 +94,7 @@ export { select, selectionControl, slider, + switchControl, snackbar, tabs, textField, diff --git a/packages/mdc-switch/README.md b/packages/mdc-switch/README.md index d27d47b5137..be6a006bc67 100644 --- a/packages/mdc-switch/README.md +++ b/packages/mdc-switch/README.md @@ -15,7 +15,7 @@ path: /catalog/input-controls/switches/

--> -Switches toggle the state of a single settings option on or off, and are mobile preferred. +Switches toggle the state of a single setting on or off. They are the preferred way to adjust settings on mobile. ## Design & API Documentation @@ -40,24 +40,47 @@ npm install @material/switch ```html
- -
-
+
+
+
+ +
``` + +### Styles + +```scss +@import "@material/switch/mdc-switch"; +``` + +### JavaScript Instantiation + +The Switch requires JavaScript to function, so it is necessary to instantiate MDCSwitch with the HTML. + +```js +import {MDCSwitch} from '@material/switch'; + +const switchControl = new MDCSwitch(document.querySelector('.mdc-switch')); +``` + +> See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. + ## Variant ### Disabled Switch -Users can add the `disabled` attribute directly to the `` element or a parent `
` element to disable a switch. +Users can add the class 'mdc-switch--disabled' to the 'mdc-switch' element to disable the switch. ```html -
- -
-
+
+
+
+
+ +
@@ -70,17 +93,63 @@ Users can add the `disabled` attribute directly to the `` element or a pa CSS Class | Description --- | --- `mdc-switch` | Mandatory, for the parent element. -`mdc-switch__native-control` | Mandatory, for the input checkbox. -`mdc-switch__background` | Mandatory, for the background element. -`mdc-switch__knob` | Mandatory, for the knob element. +`mdc-switch__track` | Mandatory, for the track element. +`mdc-switch__thumb-underlay` | Mandatory, for the ripple effect. +`mdc-switch__thumb` | Mandatory, for the thumb element. +`mdc-switch__native-control` | Mandatory, for the hidden input checkbox. ### Sass Mixins -The following mixins apply only to _enabled_ switches in the _on_ (checked) state. -It is not currently possible to customize the color of a _disabled_ or _off_ (unchecked) switch. +MDC Switch uses [MDC Theme](../mdc-theme)'s `secondary` color by default for the checked (toggled on) state. +Use the following mixins to customize _enabled_ switches. It is not currently possible to customize the color of a + _disabled_ switch. Disabled switches use the same colors as enabled switches, but with a different opacity value. Mixin | Description --- | --- -`mdc-switch-track-color($color)` | Sets the track color. -`mdc-switch-knob-color($color)` | Sets the knob color. -`mdc-switch-focus-indicator-color($color)` | Sets the focus indicator color. +`mdc-switch-toggled-on-color($color)` | Sets the base color of the track, thumb, and ripple when the switch is toggled on. +`mdc-switch-toggled-off-color($color)` | Sets the base color of the track, thumb, and ripple when the switch is toggled off. +`mdc-switch-toggled-on-track-color($color)` | Sets color of the track when the switch is toggled on. +`mdc-switch-toggled-off-track-color($color)` | Sets color of the track when the switch is toggled off. +`mdc-switch-toggled-on-thumb-color($color)` | Sets color of the thumb when the switch is toggled on. +`mdc-switch-toggled-off-thumb-color($color)` | Sets color of the thumb when the switch is toggled off. +`mdc-switch-toggled-on-ripple-color($color)` | Sets the color of the ripple surrounding the thumb when the switch is toggled on. +`mdc-switch-toggled-off-ripple-color($color)` | Sets the color of the ripple surrounding the thumb when the switch is toggled off. + +## `MDCSwitch` Properties and Methods + +Property | Value Type | Description +--- | --- | --- +`checked` | Boolean | Setter/getter for the switch's checked state +`disabled` | Boolean | Setter/getter for the switch's disabled state + +## Usage within Web Frameworks + +If you are using a JavaScript framework, such as React or Angular, you can create a Switch for your framework. Depending on your needs, you can use the _Simple Approach: Wrapping MDC Web Vanilla Components_, or the _Advanced Approach: Using Foundations and Adapters_. Please follow the instructions [here](../../docs/integrating-into-frameworks.md). + +### `MDCSwitchAdapter` + +| Method Signature | Description | +| --- | --- | +| `addClass(className: string) => void` | Adds a class to the root element. | +| `removeClass(className: string) => void` | Removes a class from the root element. | +| `setNativeControlChecked(checked: boolean)` | Sets the checked state of the native control. | +| `isNativeControlChecked() => boolean` | Returns the checked state of the native control. | +| `setNativeControlDisabled(disabled: boolean)` | Sets the disabled state of the native control. | +| `isNativeControlDisabled() => boolean` | Returns the disabled state of the native control. | + +### `MDCSwitchFoundation` + +| Method Signature | Description | +| --- | --- | +| `isChecked() => boolean` | Returns whether the native control is checked. | +| `setChecked(checked: boolean) => void` | Sets the checked value of the native control and updates styling to reflect the checked state. | +| `isDisabled() => boolean` | Returns whether the native control is disabled. | +| `setDisabled(disabled: boolean) => void` | Sets the disabled value of the native control and updates styling to reflect the disabled state. | +| `handleChange() => void` | Handles a change event from the native control. | + +### `MDCSwitchFoundation` Event Handlers +If wrapping the switch component it is necessary to add an event handler for native control change events that calls the `handleChange` foundation method. For an example of this, see the [MDCSwitch](index.js) component `initialSyncWithDOM` method. + +| Event | Element Selector | Foundation Handler | +| --- | --- | --- | +| `change` | `.mdc-switch__native-control` | `handleChange()` | \ No newline at end of file diff --git a/packages/mdc-switch/_functions.scss b/packages/mdc-switch/_functions.scss index 2ff997103a5..06e64497389 100644 --- a/packages/mdc-switch/_functions.scss +++ b/packages/mdc-switch/_functions.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// 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. diff --git a/packages/mdc-switch/_mixins.scss b/packages/mdc-switch/_mixins.scss index 93ac3eedea9..117faeb70b4 100644 --- a/packages/mdc-switch/_mixins.scss +++ b/packages/mdc-switch/_mixins.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// 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. @@ -15,72 +15,58 @@ // @import "@material/theme/mixins"; +@import "@material/ripple/mixins"; +@import "@material/rtl/mixins"; @import "./variables"; -// -// Public -// +@mixin mdc-switch-toggled-on-color($color) { + @include mdc-switch-toggled-on-track-color($color); + @include mdc-switch-toggled-on-thumb-color($color); + @include mdc-switch-toggled-on-ripple-color($color); +} -@mixin mdc-switch-track-color($color) { - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background::before { +@mixin mdc-switch-toggled-off-color($color) { + @include mdc-switch-toggled-off-track-color($color); + @include mdc-switch-toggled-off-thumb-color($color); + @include mdc-switch-toggled-off-ripple-color($color); +} + +@mixin mdc-switch-toggled-on-track-color($color) { + &.mdc-switch--checked .mdc-switch__track { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-knob-color($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background .mdc-switch__knob { +@mixin mdc-switch-toggled-on-thumb-color($color) { + &.mdc-switch--checked .mdc-switch__thumb { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-focus-indicator-color($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:checked ~ .mdc-switch__background .mdc-switch__knob::before { - @include mdc-theme-prop(background-color, $color); +@mixin mdc-switch-toggled-on-ripple-color($color) { + &.mdc-switch--checked .mdc-switch__thumb-underlay { + @include mdc-states($color); } } -// -// Private -// - -@mixin mdc-switch-unchecked-track-color_($color) { - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background::before { +@mixin mdc-switch-toggled-off-track-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__track { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-unchecked-knob-color_($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background .mdc-switch__knob { +@mixin mdc-switch-toggled-off-thumb-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__thumb { @include mdc-theme-prop(background-color, $color); @include mdc-theme-prop(border-color, $color); } } -@mixin mdc-switch-unchecked-focus-indicator-color_($color) { - // stylelint-disable-next-line selector-max-specificity - .mdc-switch__native-control:enabled:not(:checked) ~ .mdc-switch__background .mdc-switch__knob::before { - @include mdc-theme-prop(background-color, $color); +@mixin mdc-switch-toggled-off-ripple-color($color) { + &:not(.mdc-switch--checked) .mdc-switch__thumb-underlay { + @include mdc-states($color); } } - -@mixin mdc-switch-native-control_ { - position: absolute; - top: -14px; - left: -14px; - width: $mdc-switch-focus-ring-diameter; - height: $mdc-switch-focus-ring-diameter; -} - -@mixin mdc-switch-tap-target_ { - position: absolute; - top: -24px; - left: -24px; - width: $mdc-switch-focus-ring-diameter; - height: $mdc-switch-focus-ring-diameter; -} diff --git a/packages/mdc-switch/_variables.scss b/packages/mdc-switch/_variables.scss index c6d52982ec7..bf506f9be2f 100644 --- a/packages/mdc-switch/_variables.scss +++ b/packages/mdc-switch/_variables.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// 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. @@ -14,17 +14,33 @@ // limitations under the License. // -$mdc-switch-track-width: 34px; +@import "@material/theme/variables"; + +$mdc-switch-track-width: 32px; $mdc-switch-track-height: 14px; -$mdc-switch-knob-diameter: 20px; -$mdc-switch-focus-ring-diameter: 48px; -$mdc-switch-knob-active-margin: $mdc-switch-track-width - $mdc-switch-knob-diameter; +$mdc-switch-thumb-diameter: 20px; +$mdc-switch-tap-target-size: 48px; -$mdc-switch-unchecked-track-color: #000; -$mdc-switch-unchecked-knob-color: #fafafa; -$mdc-switch-unchecked-focus-ring-color: #9e9e9e; -$mdc-switch-disabled-knob-color: #bdbdbd; +// Amount the edge of the thumb should be offset from the edge of the track. +$mdc-switch-thumb-offset: 4px; -$mdc-switch-baseline-theme-color: secondary; +// Position for the tap target that contains the thumb to align the thumb correctly offset from the track. +$mdc-switch-tap-target-initial-position: + -$mdc-switch-tap-target-size / 2 + $mdc-switch-thumb-diameter / 2 - + $mdc-switch-thumb-offset; + +// Value to cover the whole switch area (including the ripple) with the native control. +$mdc-switch-native-control-width: + $mdc-switch-track-width + + ($mdc-switch-tap-target-size - $mdc-switch-thumb-diameter) + + $mdc-switch-thumb-offset * 2; -$mdc-switch-knob-vertical-offset_: -3px; +$mdc-switch-thumb-active-margin: $mdc-switch-track-width - $mdc-switch-thumb-diameter + $mdc-switch-thumb-offset * 2; + +$mdc-switch-toggled-off-thumb-color: mdc-theme-prop-value(surface); +$mdc-switch-toggled-off-track-color: mdc-theme-prop-value(on-surface); +$mdc-switch-toggled-off-ripple-color: #9e9e9e; +$mdc-switch-disabled-thumb-color: mdc-theme-prop-value(surface); +$mdc-switch-disabled-track-color: mdc-theme-prop-value(on-surface); + +$mdc-switch-baseline-theme-color: secondary; diff --git a/packages/mdc-switch/adapter.js b/packages/mdc-switch/adapter.js new file mode 100644 index 00000000000..69056aa689d --- /dev/null +++ b/packages/mdc-switch/adapter.js @@ -0,0 +1,55 @@ +/** + * @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 Switch. Provides an interface for managing + * - classes + * - dom + * + * 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 MDCSwitchAdapter { + /** @param {string} className */ + addClass(className) {} + + /** @param {string} className */ + removeClass(className) {} + + /** @param {boolean} checked */ + setNativeControlChecked(checked) {} + + /** @return {boolean} checked */ + isNativeControlChecked() {} + + /** @param {boolean} disabled */ + setNativeControlDisabled(disabled) {} + + /** @return {boolean} disabled */ + isNativeControlDisabled() {} +} + +export default MDCSwitchAdapter; diff --git a/packages/mdc-switch/constants.js b/packages/mdc-switch/constants.js new file mode 100644 index 00000000000..13805ce8bdc --- /dev/null +++ b/packages/mdc-switch/constants.js @@ -0,0 +1,31 @@ +/** + * @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. + */ + +/** @enum {string} */ +const cssClasses = { + CHECKED: 'mdc-switch--checked', + DISABLED: 'mdc-switch--disabled', +}; + +/** @enum {string} */ +const strings = { + NATIVE_CONTROL_SELECTOR: '.mdc-switch__native-control', + RIPPLE_SURFACE_SELECTOR: '.mdc-switch__thumb-underlay', +}; + + +export {cssClasses, strings}; diff --git a/packages/mdc-switch/foundation.js b/packages/mdc-switch/foundation.js new file mode 100644 index 00000000000..27a70d9534e --- /dev/null +++ b/packages/mdc-switch/foundation.js @@ -0,0 +1,106 @@ +/** + * @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. + */ + +import MDCFoundation from '@material/base/foundation'; +import MDCSwitchAdapter from './adapter'; +/* eslint-enable no-unused-vars */ +import {cssClasses, strings} from './constants'; + +/** + * @extends {MDCFoundation} + */ +class MDCSwitchFoundation extends MDCFoundation { + /** @return enum {string} */ + static get strings() { + return strings; + } + + /** @return enum {string} */ + static get cssClasses() { + return cssClasses; + } + + /** @return {!MDCSwitchAdapter} */ + static get defaultAdapter() { + return /** @type {!MDCSwitchAdapter} */ ({ + addClass: (/* className: string */) => {}, + removeClass: (/* className: string */) => {}, + setNativeControlChecked: (/* checked: boolean */) => {}, + isNativeControlChecked: () => /* boolean */ {}, + setNativeControlDisabled: (/* disabled: boolean */) => {}, + isNativeControlDisabled: () => /* boolean */ {}, + }); + } + + constructor(adapter) { + super(Object.assign(MDCSwitchFoundation.defaultAdapter, adapter)); + } + + /** @override */ + init() { + // Do an initial state update based on the state of the native control. + this.handleChange(); + } + + /** @return {boolean} */ + isChecked() { + return this.adapter_.isNativeControlChecked(); + } + + /** @param {boolean} checked */ + setChecked(checked) { + this.adapter_.setNativeControlChecked(checked); + this.updateCheckedStyling_(checked); + } + + /** @return {boolean} */ + isDisabled() { + return this.adapter_.isNativeControlDisabled(); + } + + /** @param {boolean} disabled */ + setDisabled(disabled) { + this.adapter_.setNativeControlDisabled(disabled); + if (disabled) { + this.adapter_.addClass(cssClasses.DISABLED); + } else { + this.adapter_.removeClass(cssClasses.DISABLED); + } + } + + /** + * Handles the change event for the switch native control. + */ + handleChange() { + this.updateCheckedStyling_(this.isChecked()); + } + + /** + * Updates the styling of the switch based on its checked state. + * @param {boolean} checked + * @private + */ + updateCheckedStyling_(checked) { + if (checked) { + this.adapter_.addClass(cssClasses.CHECKED); + } else { + this.adapter_.removeClass(cssClasses.CHECKED); + } + } +} + +export default MDCSwitchFoundation; diff --git a/packages/mdc-switch/index.js b/packages/mdc-switch/index.js new file mode 100644 index 00000000000..5ea3b4eae5a --- /dev/null +++ b/packages/mdc-switch/index.js @@ -0,0 +1,129 @@ +/** + * @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. + */ + +import MDCComponent from '@material/base/component'; +/* eslint-disable no-unused-vars */ +import {MDCSelectionControlState, MDCSelectionControl} from '@material/selection-control/index'; +/* eslint-enable no-unused-vars */ +import MDCSwitchFoundation from './foundation'; +import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; +import {getMatchesProperty} from '@material/ripple/util'; + +/** + * @extends MDCComponent + * @implements {MDCSelectionControl} + */ +class MDCSwitch extends MDCComponent { + static attachTo(root) { + return new MDCSwitch(root); + } + + constructor(...args) { + super(...args); + + /** @private {!MDCRipple} */ + this.ripple_ = this.initRipple_(); + + /** @private {!Function} */ + this.changeHandler_; + } + + destroy() { + super.destroy(); + this.ripple_.destroy(); + this.nativeControl_.removeEventListener('change', this.changeHandler_); + } + + initialSyncWithDOM() { + this.changeHandler_ = this.foundation_.handleChange.bind(this.foundation_); + this.nativeControl_.addEventListener('change', this.changeHandler_); + } + + /** + * Returns the state of the native control element, or null if the native control element is not present. + * @return {?MDCSelectionControlState} + * @private + */ + get nativeControl_() { + const {NATIVE_CONTROL_SELECTOR} = MDCSwitchFoundation.strings; + const el = /** @type {?MDCSelectionControlState} */ ( + this.root_.querySelector(NATIVE_CONTROL_SELECTOR)); + return el; + } + + /** + * @return {!MDCRipple} + * @private + */ + initRipple_() { + const {RIPPLE_SURFACE_SELECTOR} = MDCSwitchFoundation.strings; + const rippleSurface = /** @type {!Element} */ (this.root_.querySelector(RIPPLE_SURFACE_SELECTOR)); + + const MATCHES = getMatchesProperty(HTMLElement.prototype); + const adapter = Object.assign(MDCRipple.createAdapter(this), { + isUnbounded: () => true, + isSurfaceActive: () => this.nativeControl_[MATCHES](':active'), + addClass: (className) => rippleSurface.classList.add(className), + removeClass: (className) => rippleSurface.classList.remove(className), + registerInteractionHandler: (type, handler) => this.nativeControl_.addEventListener(type, handler), + deregisterInteractionHandler: (type, handler) => this.nativeControl_.removeEventListener(type, handler), + updateCssVariable: (varName, value) => rippleSurface.style.setProperty(varName, value), + computeBoundingRect: () => rippleSurface.getBoundingClientRect(), + }); + const foundation = new MDCRippleFoundation(adapter); + return new MDCRipple(this.root_, foundation); + } + + /** @return {!MDCSwitchFoundation} */ + getDefaultFoundation() { + return new MDCSwitchFoundation({ + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setNativeControlChecked: (checked) => this.nativeControl_.checked = checked, + isNativeControlChecked: () => this.nativeControl_.checked, + setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled, + isNativeControlDisabled: () => this.nativeControl_.disabled, + }); + } + + /** @return {!MDCRipple} */ + get ripple() { + return this.ripple_; + } + + /** @return {boolean} */ + get checked() { + return this.foundation_.isChecked(); + } + + /** @param {boolean} checked */ + set checked(checked) { + this.foundation_.setChecked(checked); + } + + /** @return {boolean} */ + get disabled() { + return this.foundation_.isDisabled(); + } + + /** @param {boolean} disabled */ + set disabled(disabled) { + this.foundation_.setDisabled(disabled); + } +} + +export {MDCSwitchFoundation, MDCSwitch}; diff --git a/packages/mdc-switch/mdc-switch.scss b/packages/mdc-switch/mdc-switch.scss index 9e85409f282..b58c3d1d010 100644 --- a/packages/mdc-switch/mdc-switch.scss +++ b/packages/mdc-switch/mdc-switch.scss @@ -1,5 +1,5 @@ // -// Copyright 2016 Google Inc. All Rights Reserved. +// 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. @@ -16,147 +16,118 @@ @import "@material/elevation/mixins"; @import "@material/rtl/mixins"; +@import "@material/ripple/common"; +@import "@material/ripple/mixins"; @import "./functions"; @import "./mixins"; @import "./variables"; -// postcss-bem-linter: define switch .mdc-switch { + @include mdc-switch-toggled-on-track-color($mdc-switch-baseline-theme-color); + @include mdc-switch-toggled-on-thumb-color($mdc-switch-baseline-theme-color); + @include mdc-switch-toggled-off-track-color($mdc-switch-toggled-off-track-color); + @include mdc-switch-toggled-off-thumb-color($mdc-switch-toggled-off-thumb-color); + @include mdc-switch-toggled-off-ripple-color($mdc-switch-toggled-off-ripple-color); + display: inline-block; position: relative; - - &__native-control { - @include mdc-switch-native-control_; - - display: inline-block; - margin-top: $mdc-switch-knob-vertical-offset_; - margin-left: 0; - transition: mdc-switch-transition(transform); - opacity: 0; - cursor: pointer; - z-index: 2; - - &:checked { - transform: translateX($mdc-switch-knob-active-margin); - - @include mdc-rtl { - transform: translateX(-($mdc-switch-knob-active-margin)); - } - } - } + outline: none; + user-select: none; } -@at-root { - @include mdc-switch-unchecked-track-color_($mdc-switch-unchecked-track-color); - @include mdc-switch-unchecked-knob-color_($mdc-switch-unchecked-knob-color); - @include mdc-switch-unchecked-focus-indicator-color_($mdc-switch-unchecked-focus-ring-color); - @include mdc-switch-track-color($mdc-switch-baseline-theme-color); - @include mdc-switch-knob-color($mdc-switch-baseline-theme-color); - @include mdc-switch-focus-indicator-color($mdc-switch-baseline-theme-color); +.mdc-switch__native-control { + @include mdc-rtl-reflexive-position(left, 0); + + position: absolute; + top: 0; + width: $mdc-switch-native-control-width; + height: $mdc-switch-tap-target-size; + margin: 0; + opacity: 0; + cursor: pointer; + pointer-events: auto; } -.mdc-switch__background { - display: block; - position: relative; +.mdc-switch__track { + box-sizing: border-box; width: $mdc-switch-track-width; height: $mdc-switch-track-height; - border-radius: 50%; - outline: none; - user-select: none; - - // Track - &::before { - display: block; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - transition: - mdc-switch-transition(opacity), - mdc-switch-transition(background-color), - mdc-switch-transition(border-color); - border: 1px solid; - border-radius: 7px; - opacity: .38; - content: ""; - } + transition: + mdc-switch-transition(opacity), + mdc-switch-transition(background-color), + mdc-switch-transition(border-color); + border: 1px solid; + border-radius: $mdc-switch-track-height / 2; + opacity: .38; } -.mdc-switch__knob { - @include mdc-elevation(2); - @include mdc-rtl-reflexive-position(left, 0); +.mdc-switch__thumb-underlay { + @include mdc-rtl-reflexive-position(left, $mdc-switch-tap-target-initial-position); + @include mdc-ripple-surface(); + @include mdc-ripple-radius-unbounded; + @include mdc-states($mdc-switch-baseline-theme-color); - display: block; + display: flex; position: absolute; - top: $mdc-switch-knob-vertical-offset_; - box-sizing: border-box; - width: $mdc-switch-knob-diameter; - height: $mdc-switch-knob-diameter; + // Ensures the knob is centered on the track. + top: -(($mdc-switch-tap-target-size - $mdc-switch-track-height) / 2); + align-items: center; + justify-content: center; + width: $mdc-switch-tap-target-size; + height: $mdc-switch-tap-target-size; transform: translateX(0); transition: mdc-switch-transition(transform), mdc-switch-transition(background-color), mdc-switch-transition(border-color); - border: $mdc-switch-knob-diameter / 2 solid; - border-radius: 50%; - z-index: 1; - - // Focus indicator - &::before { - @include mdc-switch-tap-target_; - - transform: scale(0); - transition: - mdc-switch-transition(transform), - mdc-switch-transition(background-color); - border-radius: 50%; - opacity: .2; - content: ""; - } } -// Focus indicator -.mdc-switch__native-control:focus ~ .mdc-switch__background .mdc-switch__knob::before { - transform: scale(1); +.mdc-switch__thumb { + @include mdc-elevation(2); + + box-sizing: border-box; + width: $mdc-switch-thumb-diameter; + height: $mdc-switch-thumb-diameter; + border: $mdc-switch-thumb-diameter / 2 solid; + border-radius: 50%; + // Allow events to go through to the native control, necessary for IE and Edge. + pointer-events: none; + z-index: 1; } -.mdc-switch__native-control:checked ~ .mdc-switch__background { - // Track - &::before { - opacity: .5; +.mdc-switch--checked { + .mdc-switch__track { + opacity: .54; } - .mdc-switch__knob { - transform: translateX($mdc-switch-knob-active-margin); + .mdc-switch__thumb-underlay { + transform: translateX($mdc-switch-thumb-active-margin); @include mdc-rtl { - transform: translateX(-($mdc-switch-knob-active-margin)); + transform: translateX(-($mdc-switch-thumb-active-margin)); } + } - // Focus indicator - &::before { - opacity: .15; + // Translate the native control the opposite direction so that the tap target stays the same. + .mdc-switch__native-control { + transform: translateX(-($mdc-switch-thumb-active-margin)); + + @include mdc-rtl { + transform: translateX($mdc-switch-thumb-active-margin); } } } -// postcss-bem-linter: end - -.mdc-switch__native-control:disabled { - cursor: initial; -} +.mdc-switch--disabled { + opacity: .38; + pointer-events: none; -.mdc-switch__native-control:disabled ~ .mdc-switch__background { - // Track - &::before { - background-color: $mdc-switch-unchecked-track-color; - opacity: .12; + .mdc-switch__thumb { + border-width: 1px; // In high contrast mode, only show outline of knob. } - .mdc-switch__knob { - border-width: 1px; // In high contrast mode, only show outline of the knob. - border-color: $mdc-switch-disabled-knob-color; - background-color: $mdc-switch-disabled-knob-color; + .mdc-switch__native-control { + cursor: default; + pointer-events: none; } } diff --git a/packages/mdc-switch/package.json b/packages/mdc-switch/package.json index 40873c54721..966b32a604b 100644 --- a/packages/mdc-switch/package.json +++ b/packages/mdc-switch/package.json @@ -8,17 +8,22 @@ "material design", "switch" ], + "main": "index.js", "repository": { "type": "git", "url": "https://github.com/material-components/material-components-web.git" }, "dependencies": { "@material/animation": "^0.34.0", + "@material/base": "^0.35.0", "@material/elevation": "^0.36.1", + "@material/ripple": "^0.37.1", "@material/rtl": "^0.36.0", + "@material/selection-control": "^0.37.1", "@material/theme": "^0.35.0" }, "publishConfig": { "access": "public" } } + diff --git a/scripts/check-pkg-for-release.js b/scripts/check-pkg-for-release.js index 139a9c6be60..36ff709a13b 100644 --- a/scripts/check-pkg-for-release.js +++ b/scripts/check-pkg-for-release.js @@ -193,6 +193,8 @@ function checkAutoInitAddedInMDCPackage(ast) { let nameCamel = camelCase(pkg.name.replace('@material/', '')); if (nameCamel === 'textfield') { nameCamel = 'textField'; + } else if (nameCamel === 'switch') { + nameCamel = 'switchControl'; } let autoInitedCount = 0; traverse(ast, { @@ -216,6 +218,8 @@ function checkComponentExportedAddedInMDCPackage(ast) { let nameCamel = camelCase(pkg.name.replace('@material/', '')); if (nameCamel === 'textfield') { nameCamel = 'textField'; + } else if (nameCamel === 'switch') { + nameCamel = 'switchControl'; } let isExported = false; traverse(ast, { diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js index 5c698a59590..455cba118b4 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -147,6 +147,7 @@ class JsBundleFactory { selectionControl: getAbsolutePath('/packages/mdc-selection-control/index.js'), slider: getAbsolutePath('/packages/mdc-slider/index.js'), snackbar: getAbsolutePath('/packages/mdc-snackbar/index.js'), + switch: getAbsolutePath('/packages/mdc-switch/index.js'), tabs: getAbsolutePath('/packages/mdc-tabs/index.js'), textfield: getAbsolutePath('/packages/mdc-textfield/index.js'), toolbar: getAbsolutePath('/packages/mdc-toolbar/index.js'), diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index d57ae5718be..cd7e871a792 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -278,6 +278,33 @@ "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/advorak/2018/07/17/22_50_36_536/spec/mdc-icon-button/mixins/ink-color.html.windows_ie_11.png" } }, + "spec/mdc-switch/classes/baseline.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/classes/baseline.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/classes/baseline.html.windows_ie_11.png" + } + }, + "spec/mdc-switch/mixins/thumb-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/mixins/thumb-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/thumb-color.html.windows_ie_11.png" + } + }, + "spec/mdc-switch/mixins/track-color.html": { + "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/21/00_22_42_705/spec/mdc-switch/mixins/track-color.html", + "screenshots": { + "desktop_windows_chrome@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_chrome_67.png", + "desktop_windows_edge@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_edge_17.png", + "desktop_windows_firefox@latest": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_firefox_61.png", + "desktop_windows_ie@11": "https://storage.googleapis.com/mdc-web-screenshot-tests/rlfriedman/2018/07/23/18_31_44_533/spec/mdc-switch/mixins/track-color.html.windows_ie_11.png" + } + }, "spec/mdc-textfield/classes/baseline-textfield.html": { "public_url": "https://storage.googleapis.com/mdc-web-screenshot-tests/kfranqueiro/2018/07/18/20_19_24_759/spec/mdc-textfield/classes/baseline-textfield.html", "screenshots": { diff --git a/test/screenshot/spec/mdc-switch/classes/baseline.html b/test/screenshot/spec/mdc-switch/classes/baseline.html new file mode 100644 index 00000000000..0a338602145 --- /dev/null +++ b/test/screenshot/spec/mdc-switch/classes/baseline.html @@ -0,0 +1,83 @@ + + + + + + Baseline Switch - MDC Web Screenshot Test + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-switch/fixture.scss b/test/screenshot/spec/mdc-switch/fixture.scss new file mode 100644 index 00000000000..68c7405a99c --- /dev/null +++ b/test/screenshot/spec/mdc-switch/fixture.scss @@ -0,0 +1,35 @@ +// +// 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. +// + +@import "../../../../packages/mdc-switch/mixins"; +@import "../../../../packages/mdc-theme/color-palette"; + +$custom-switch-color: $material-color-red-500; + +.test-cell--switch { + width: 85px; + height: 80px; +} + +.custom-switch--thumb-color { + @include mdc-switch-toggled-on-thumb-color($custom-switch-color); + @include mdc-switch-toggled-off-thumb-color($custom-switch-color) +} + +.custom-switch--track-color { + @include mdc-switch-toggled-on-track-color($custom-switch-color); + @include mdc-switch-toggled-off-track-color($custom-switch-color) +} diff --git a/test/screenshot/spec/mdc-switch/mixins/thumb-color.html b/test/screenshot/spec/mdc-switch/mixins/thumb-color.html new file mode 100644 index 00000000000..b1fdeec5863 --- /dev/null +++ b/test/screenshot/spec/mdc-switch/mixins/thumb-color.html @@ -0,0 +1,83 @@ + + + + + + thumb-color Switch Mixin - MDC Web Screenshot Test + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + diff --git a/test/screenshot/spec/mdc-switch/mixins/track-color.html b/test/screenshot/spec/mdc-switch/mixins/track-color.html new file mode 100644 index 00000000000..d8a89635345 --- /dev/null +++ b/test/screenshot/spec/mdc-switch/mixins/track-color.html @@ -0,0 +1,83 @@ + + + + + + track-color Switch Mixin - MDC Web Screenshot Test + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + diff --git a/test/unit/mdc-switch/foundation.test.js b/test/unit/mdc-switch/foundation.test.js new file mode 100644 index 00000000000..c215fcd4b47 --- /dev/null +++ b/test/unit/mdc-switch/foundation.test.js @@ -0,0 +1,148 @@ +/** + * 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. + */ + +import {assert} from 'chai'; +import td from 'testdouble'; + +import MDCSwitchFoundation from '../../../packages/mdc-switch/foundation'; + +suite('MDCSwitchFoundation'); + +test('exports cssClasses', () => { + assert.isOk('cssClasses' in MDCSwitchFoundation); +}); + +test('exports strings', () => { + assert.isOk('strings' in MDCSwitchFoundation); +}); + +test('defaultAdapter returns a complete adapter implementation', () => { + const {defaultAdapter} = MDCSwitchFoundation; + const methods = Object.keys(defaultAdapter).filter((k) => typeof defaultAdapter[k] === 'function'); + + assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function'); + assert.deepEqual(methods, ['addClass', 'removeClass', 'setNativeControlChecked', 'isNativeControlChecked', + 'setNativeControlDisabled', 'isNativeControlDisabled']); + methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m])); +}); + +function setupTest() { + const mockAdapter = td.object(MDCSwitchFoundation.defaultAdapter); + const foundation = new MDCSwitchFoundation(mockAdapter); + return {foundation, mockAdapter}; +} + +test('#init adds mdc-switch--checked to the switch element if the switch is initially checked', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(true); + + foundation.init(); + td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('#init removes mdc-switch--checked from the switch element if the switch is initially unchecked', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(false); + + foundation.init(); + td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('#isChecked returns true when the value of adapter.isChecked() is true', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(true); + assert.isOk(foundation.isChecked()); +}); + +test('#isChecked returns false when the value of adapter.isChecked() is false', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(false); + assert.isNotOk(foundation.isChecked()); +}); + +test('#setChecked updates the checked state', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.setChecked(true); + td.verify(mockAdapter.setNativeControlChecked(true)); + + foundation.setChecked(false); + td.verify(mockAdapter.setNativeControlChecked(false)); +}); + +test('#setChecked adds mdc-switch--checked to the switch element when set to true', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(true); + foundation.setChecked(true); + td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('#setChecked removes mdc-switch--checked from the switch element when set to false', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlChecked()).thenReturn(false); + foundation.setChecked(false); + td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('#isDisabled returns true when adapter.isDisabled() is true', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlDisabled()).thenReturn(true); + assert.isOk(foundation.isDisabled()); +}); + +test('#isDisabled returns false when adapter.isDisabled() is false', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.isNativeControlDisabled()).thenReturn(false); + assert.isNotOk(foundation.isDisabled()); +}); + +test('#setDisabled updates the disabled state', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.setDisabled(true); + td.verify(mockAdapter.setNativeControlDisabled(true)); + + foundation.setDisabled(false); + td.verify(mockAdapter.setNativeControlDisabled(false)); +}); + +test('#setDisabled adds mdc-switch--disabled to the switch element when set to true', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.setDisabled(true); + td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.DISABLED)); +}); + +test('#setDisabled removes mdc-switch--disabled from the switch element when set to false', () => { + const {foundation, mockAdapter} = setupTest(); + foundation.setDisabled(false); + td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.DISABLED)); +}); + +test('#handleChange adds mdc-switch--checked to the switch when it is a checked state', () => { + const {foundation, mockAdapter} = setupTest(); + + td.when(mockAdapter.isNativeControlChecked()).thenReturn(true); + + foundation.handleChange(); + td.verify(mockAdapter.addClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('#handleChange removes mdc-switch--checked from the switch when it is an unchecked state', () => { + const {foundation, mockAdapter} = setupTest(); + + td.when(mockAdapter.isNativeControlChecked()).thenReturn(false); + + foundation.handleChange(); + td.verify(mockAdapter.removeClass(MDCSwitchFoundation.cssClasses.CHECKED)); +}); diff --git a/test/unit/mdc-switch/mdc-switch.test.js b/test/unit/mdc-switch/mdc-switch.test.js new file mode 100644 index 00000000000..fbc83961613 --- /dev/null +++ b/test/unit/mdc-switch/mdc-switch.test.js @@ -0,0 +1,143 @@ +/** + * 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. + */ + +import {assert} from 'chai'; +import bel from 'bel'; +import td from 'testdouble'; + +import {supportsCssVariables} from '../../../packages/mdc-ripple/util'; +import {createMockRaf} from '../helpers/raf'; +import {MDCSwitchFoundation, MDCSwitch} from '../../../packages/mdc-switch'; +import {MDCRipple} from '../../../packages/mdc-ripple'; + +const {NATIVE_CONTROL_SELECTOR, RIPPLE_SURFACE_SELECTOR} = MDCSwitchFoundation.strings; + +function getFixture() { + return bel` +
+
+
+
+ +
+
+
+ `; +} + +function setupTest() { + const root = getFixture(); + const component = new MDCSwitch(root); + const rippleSurface = root.querySelector(RIPPLE_SURFACE_SELECTOR); + return {root, component, rippleSurface}; +} + +suite('MDCSwitch'); + +test('attachTo initializes and returns a MDCSwitch instance', () => { + assert.isOk(MDCSwitch.attachTo(getFixture()) instanceof MDCSwitch); +}); + +if (supportsCssVariables(window)) { + test('#constructor initializes the root element with a ripple', () => { + const raf = createMockRaf(); + const {rippleSurface} = setupTest(); + raf.flush(); + assert.isOk(rippleSurface.classList.contains('mdc-ripple-upgraded')); + raf.restore(); + }); + + test('#destroy removes the ripple', () => { + const raf = createMockRaf(); + const {component, rippleSurface} = setupTest(); + raf.flush(); + component.destroy(); + raf.flush(); + assert.isNotOk(rippleSurface.classList.contains('mdc-ripple-upgraded')); + }); +} + +test('get/set checked updates the checked value of the native switch input element', () => { + const {root, component} = setupTest(); + const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR); + component.checked = true; + assert.isOk(inputEl.checked); + assert.equal(component.checked, inputEl.checked); +}); + +test('get/set checked updates the component styles', () => { + const {root, component} = setupTest(); + component.checked = true; + assert.isOk(root.classList.contains(MDCSwitchFoundation.cssClasses.CHECKED)); + component.checked = false; + assert.isNotOk(root.classList.contains(MDCSwitchFoundation.cssClasses.CHECKED)); +}); + +test('get/set disabled updates the disabled value of the native switch input element', () => { + const {root, component} = setupTest(); + const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR); + component.disabled = true; + assert.isOk(inputEl.disabled); + assert.equal(component.disabled, inputEl.disabled); + component.disabled = false; + assert.isNotOk(inputEl.disabled); + assert.equal(component.disabled, inputEl.disabled); +}); + +test('get/set disabled updates the component styles', () => { + const {root, component} = setupTest(); + component.disabled = true; + assert.isOk(root.classList.contains(MDCSwitchFoundation.cssClasses.DISABLED)); + component.disabled = false; + assert.isNotOk(root.classList.contains(MDCSwitchFoundation.cssClasses.DISABLED)); +}); + +test('get ripple returns a MDCRipple instance', () => { + const {component} = setupTest(); + assert.isOk(component.ripple instanceof MDCRipple); +}); + +function setupMockFoundationTest(root = getFixture()) { + const MockFoundationCtor = td.constructor(MDCSwitchFoundation); + const mockFoundation = new MockFoundationCtor(); + const component = new MDCSwitch(root, mockFoundation); + return {root, component, mockFoundation}; +} + +test('change handler is added to the native control element', () => { + const {root, mockFoundation} = setupMockFoundationTest(); + + const event = document.createEvent('Event'); + event.initEvent('change', true, false); + + const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR); + inputEl.dispatchEvent(event); + + td.verify(mockFoundation.handleChange(event), {times: 1}); +}); + +test('change handler is removed from the native control element on destroy', () => { + const {root, component, mockFoundation} = setupMockFoundationTest(); + component.destroy(); + + const event = document.createEvent('Event'); + event.initEvent('change', true, false); + + const inputEl = root.querySelector(NATIVE_CONTROL_SELECTOR); + inputEl.dispatchEvent(event); + + td.verify(mockFoundation.handleChange(event), {times: 0}); +});