diff --git a/app/styleguide.html b/app/styleguide.html index 2ea12594..fecc924b 100644 --- a/app/styleguide.html +++ b/app/styleguide.html @@ -101,6 +101,11 @@ +
+

Switch

+ +
+

Slider

diff --git a/app/styleguide/_colors.scss b/app/styleguide/_colors.scss index 2a45a496..e9dc4be9 100644 --- a/app/styleguide/_colors.scss +++ b/app/styleguide/_colors.scss @@ -83,6 +83,17 @@ $checkbox-color: nth($palette-primary, 6); $checkbox-off-color: rgba(0, 0, 0, 0.54); $checkbox-disabled-color: rgba(0, 0, 0, 0.26); +/* ========== Switches ========== */ + +$switch-color: nth($palette-primary, 6); +$switch-thumb-color: $switch-color; +$switch-track-color: rgba($switch-color, 0.5); + +$switch-off-thumb-color: nth($palette-grey, 1); +$switch-off-track-color: rgba(0, 0, 0, 0.26); +$switch-disabled-thumb-color: nth($palette-grey, 5); +$switch-disabled-track-color: rgba(0, 0, 0, 0.12); + /* ========== Text fields ========== */ $input-text-background-color: transparent; diff --git a/app/styleguide/switch/_switch.scss b/app/styleguide/switch/_switch.scss new file mode 100644 index 00000000..2957bb91 --- /dev/null +++ b/app/styleguide/switch/_switch.scss @@ -0,0 +1,181 @@ +@import "../colors"; +@import "../animation/animation"; +@import "../shadow/shadow"; +@import "../ripple/ripple"; + +$switch-label-height: 24px; +$switch-track-height: 14px; +$switch-track-length: 36px; +$switch-thumb-size: 20px; +$switch-track-top: ($switch-label-height - $switch-track-height) / 2; +$switch-thumb-top: ($switch-label-height - $switch-thumb-size) / 2; +$switch-ripple-size: $switch-label-height * 2; + +.wsk-switch { + position: relative; + + z-index: 1; + + vertical-align: middle; + + display: inline-block; + + box-sizing: border-box; + width: 100%; + height: $switch-label-height; + margin: 12px 0; + padding: 0; + + overflow: visible; +} + +.wsk-switch__input { + line-height: $switch-label-height; + + .wsk-switch.is-upgraded & { + // Hide input element, while still making it respond to focus. + position: absolute; + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + -ms-appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; + } +} + +.wsk-switch__track { + background: $switch-off-track-color; + position: absolute; + left: 0; + top: $switch-track-top; + height: $switch-track-height; + width: $switch-track-length; + border-radius: $switch-track-height; + + cursor: pointer; + + .wsk-switch.is-checked & { + background: $switch-track-color; + } + + .wsk-switch.is-disabled & { + background: $switch-disabled-track-color; + cursor: auto; + } +} + +.wsk-switch__thumb { + background: $switch-off-thumb-color; + position: absolute; + left: 0; + top: $switch-thumb-top; + height: $switch-thumb-size; + width: $switch-thumb-size; + border-radius: 50%; + + cursor: pointer; + + @include shadow-z1(); + + @include material-animation-default(0.28s); + transition-property: left; + + .wsk-switch.is-checked & { + background: $switch-thumb-color; + left: $switch-track-length - $switch-thumb-size; + + @include shadow-z2(); + } + + .wsk-switch.is-disabled & { + background: $switch-disabled-thumb-color; + cursor: auto; + } +} + +.wsk-switch__focus-helper { + position: absolute; + top: 50%; + left: 50%; + + transform: translate(-$switch-button-size / 2, -$switch-button-size / 2); + + display: inline-block; + + box-sizing: border-box; + width: $switch-button-size; + height: $switch-button-size; + border-radius: 50%; + + background-color: transparent; + + .wsk-switch.is-focused & { + box-shadow: 0 0 0px (($switch-ripple-size - $switch-button-size) / 2) + rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.1); + } + + .wsk-switch.is-focused.is-checked & { + box-shadow: 0 0 0px (($switch-ripple-size - $switch-button-size) / 2) + rgba($switch-color, 0.26); + background-color: rgba($switch-color, 0.26); + } +} + +.wsk-switch__label { + position: relative; + cursor: pointer; + font-size: 16px; + line-height: $switch-label-height; + margin: 0; + left: 24px; + + .wsk-switch.is-disabled & { + color: $switch-disabled-thumb-color; + cursor: auto; + } +} + +.wsk-switch__ripple-container { + position: absolute; + z-index: 2; + top: -($switch-ripple-size - $switch-label-height) / 2; + left: $switch-thumb-size / 2 - $switch-ripple-size / 2; + + box-sizing: border-box; + width: $switch-ripple-size; + height: $switch-ripple-size; + border-radius: 50%; + + cursor: pointer; + + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); + + transition-duration: 0.40s; + transition-timing-function: step-end; + transition-property: left; + + & .wsk-ripple { + background: $switch-color; + } + + .wsk-switch.is-disabled & { + cursor: auto; + } + + .wsk-switch.is-disabled & .wsk-ripple { + background: transparent; + } + + .wsk-switch.is-checked & { + cursor: auto; + left: $switch-track-length - $switch-ripple-size / 2 - + $switch-thumb-size / 2; + } +} diff --git a/app/styleguide/switch/demo.html b/app/styleguide/switch/demo.html new file mode 100644 index 00000000..fe535c2d --- /dev/null +++ b/app/styleguide/switch/demo.html @@ -0,0 +1,46 @@ + + + + + + + + Switch + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + diff --git a/app/styleguide/switch/demo.scss b/app/styleguide/switch/demo.scss new file mode 100644 index 00000000..5073f66a --- /dev/null +++ b/app/styleguide/switch/demo.scss @@ -0,0 +1,2 @@ +@import "../styleguide_demo_bp"; +@import "_switch"; diff --git a/app/styleguide/switch/switch.js b/app/styleguide/switch/switch.js new file mode 100644 index 00000000..fb49caf0 --- /dev/null +++ b/app/styleguide/switch/switch.js @@ -0,0 +1,220 @@ +/** + * Class constructor for Checkbox WSK component. + * Implements WSK component design pattern defined at: + * https://github.com/jasonmayes/wsk-component-design-pattern + * @param {HTMLElement} element The element that will be upgraded. + */ +function MaterialSwitch(element) { + 'use strict'; + + this.element_ = element; + + // Initialize instance. + this.init(); +} + +/** + * Store constants in one place so they can be updated easily. + * @enum {string | number} + * @private + */ +MaterialSwitch.prototype.Constant_ = { + TINY_TIMEOUT: 0.001 +}; + +/** + * Store strings for class names defined by this component that are used in + * JavaScript. This allows us to simply change it in one place should we + * decide to modify at a later date. + * @enum {string} + * @private + */ +MaterialSwitch.prototype.CssClasses_ = { + /** + * Class names should use camelCase and be prefixed with the word "material" + * to minimize conflict with 3rd party systems. + */ + + // TODO: Upgrade classnames in HTML / CSS / JS to use material prefix to + // reduce conflict and convert to camelCase for consistency. + WSK_SWITCH_INPUT: 'wsk-switch__input', + + WSK_SWITCH_TRACK: 'wsk-switch__track', + + WSK_SWITCH_THUMB: 'wsk-switch__thumb', + + WSK_SWITCH_FOCUS_HELPER: 'wsk-switch__focus-helper', + + WSK_JS_RIPPLE_EFFECT: 'wsk-js-ripple-effect', + + WSK_JS_RIPPLE_EFFECT_IGNORE_EVENTS: 'wsk-js-ripple-effect--ignore-events', + + WSK_SWITCH_RIPPLE_CONTAINER: 'wsk-switch__ripple-container', + + WSK_RIPPLE_CENTER: 'wsk-ripple--center', + + WSK_RIPPLE: 'wsk-ripple', + + IS_FOCUSED: 'is-focused', + + IS_DISABLED: 'is-disabled', + + IS_CHECKED: 'is-checked' +}; + + +/** + * Handle change of state. + * @param {Event} event The event that fired. + * @private + */ +MaterialSwitch.prototype.onChange_ = function(event) { + 'use strict'; + + this.updateClasses_(this.btnElement_, this.element_); +}; + + +/** + * Handle focus of element. + * @param {Event} event The event that fired. + * @private + */ +MaterialSwitch.prototype.onFocus_ = function(event) { + 'use strict'; + + this.element_.classList.add(this.CssClasses_.IS_FOCUSED); +}; + + +/** + * Handle lost focus of element. + * @param {Event} event The event that fired. + * @private + */ +MaterialSwitch.prototype.onBlur_ = function(event) { + 'use strict'; + + this.element_.classList.remove(this.CssClasses_.IS_FOCUSED); +}; + + +/** + * Handle mouseup. + * @param {Event} event The event that fired. + * @private + */ +MaterialSwitch.prototype.onMouseUp_ = function(event) { + 'use strict'; + + this.blur_(); +}; + + +/** + * Handle class updates. + * @param {HTMLElement} button The button whose classes we should update. + * @param {HTMLElement} label The label whose classes we should update. + * @private + */ +MaterialSwitch.prototype.updateClasses_ = function(button, label) { + 'use strict'; + + if (button.disabled) { + label.classList.add(this.CssClasses_.IS_DISABLED); + } else { + label.classList.remove(this.CssClasses_.IS_DISABLED); + } + + if (button.checked) { + label.classList.add(this.CssClasses_.IS_CHECKED); + } else { + label.classList.remove(this.CssClasses_.IS_CHECKED); + } +}; + + +/** + * Add blur. + * @private + */ +MaterialSwitch.prototype.blur_ = function(event) { + 'use strict'; + + // TODO: figure out why there's a focus event being fired after our blur, + // so that we can avoid this hack. + window.setTimeout(function() { + this.btnElement_.blur(); + }.bind(this), this.Constant_.TINY_TIMEOUT); +}; + + +/** + * Initialize element. + */ +MaterialSwitch.prototype.init = function() { + 'use strict'; + + if (this.element_) { + this.btnElement_ = this.element_.querySelector('.' + + this.CssClasses_.WSK_SWITCH_INPUT); + + var track = document.createElement('div'); + track.classList.add(this.CssClasses_.WSK_SWITCH_TRACK); + + var thumb = document.createElement('div'); + thumb.classList.add(this.CssClasses_.WSK_SWITCH_THUMB); + + var focusHelper = document.createElement('span'); + focusHelper.classList.add(this.CssClasses_.WSK_SWITCH_FOCUS_HELPER); + + thumb.appendChild(focusHelper); + + this.element_.appendChild(track); + this.element_.appendChild(thumb); + + var rippleContainer; + if (this.element_.classList.contains( + this.CssClasses_.WSK_JS_RIPPLE_EFFECT)) { + this.element_.classList.add( + this.CssClasses_.WSK_JS_RIPPLE_EFFECT_IGNORE_EVENTS); + rippleContainer = document.createElement('span'); + rippleContainer.classList.add( + this.CssClasses_.WSK_SWITCH_RIPPLE_CONTAINER); + rippleContainer.classList.add(this.CssClasses_.WSK_JS_RIPPLE_EFFECT); + rippleContainer.classList.add(this.CssClasses_.WSK_RIPPLE_CENTER); + + var ripple = document.createElement('span'); + ripple.classList.add(this.CssClasses_.WSK_RIPPLE); + + rippleContainer.appendChild(ripple); + this.element_.appendChild(rippleContainer); + } + + this.btnElement_.addEventListener('change', this.onChange_.bind(this)); + + this.btnElement_.addEventListener('focus', this.onFocus_.bind(this)); + + this.btnElement_.addEventListener('blur', this.onBlur_.bind(this)); + + this.element_.addEventListener('mouseup', this.onMouseUp_.bind(this)); + + rippleContainer.addEventListener('mouseup', this.onMouseUp_.bind(this)); + + this.updateClasses_(this.btnElement_, this.element_); + this.element_.classList.add('is-upgraded'); + } +}; + + +window.addEventListener('load', function() { + 'use strict'; + + // On document ready, the component registers itself. It can assume + // componentHandler is available in the global scope. + componentHandler.register({ + constructor: MaterialSwitch, + classAsString: 'MaterialSwitch', + cssClass: 'wsk-js-switch' + }); +});