From de7f505e03d49c9902eac08c00c0626b3298d1e6 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 27 Apr 2016 19:25:31 +0200 Subject: [PATCH] feat(): add slide-toggle component. --- src/components/slide-toggle/README.md | 43 ++++++ src/components/slide-toggle/slide-toggle.html | 9 ++ src/components/slide-toggle/slide-toggle.scss | 132 ++++++++++++++++++ .../slide-toggle/slide-toggle.spec.ts | 102 ++++++++++++++ src/components/slide-toggle/slide-toggle.ts | 103 ++++++++++++++ src/core/style/_variables.scss | 4 + src/demo-app/demo-app.html | 1 + src/demo-app/demo-app.ts | 2 + .../slide-toggle/slide-toggle-demo.html | 14 ++ .../slide-toggle/slide-toggle-demo.scss | 0 .../slide-toggle/slide-toggle-demo.ts | 10 ++ 11 files changed, 420 insertions(+) create mode 100644 src/components/slide-toggle/README.md create mode 100644 src/components/slide-toggle/slide-toggle.html create mode 100644 src/components/slide-toggle/slide-toggle.scss create mode 100644 src/components/slide-toggle/slide-toggle.spec.ts create mode 100644 src/components/slide-toggle/slide-toggle.ts create mode 100644 src/demo-app/slide-toggle/slide-toggle-demo.html create mode 100644 src/demo-app/slide-toggle/slide-toggle-demo.scss create mode 100644 src/demo-app/slide-toggle/slide-toggle-demo.ts diff --git a/src/components/slide-toggle/README.md b/src/components/slide-toggle/README.md new file mode 100644 index 000000000000..4ebf389aa2cc --- /dev/null +++ b/src/components/slide-toggle/README.md @@ -0,0 +1,43 @@ +# MdSlideToggle +`MdSlideToggle` is a two-state control, which can be also called `switch` + +### Screenshots +![image](https://cloud.githubusercontent.com/assets/4987015/14860895/25cc0dc0-0cab-11e6-9e57-9f6d513444b1.png) + +## `` +### Bound Properties + +| Name | Type | Description | +| --- | --- | --- | +| `disabled` | boolean | Disables the `slide-toggle` | + +### Examples +A basic slide-toggle would have the following markup. +```html + + Default Slide Toggle + +``` + +Slide toggle can be also disabled. +```html + + Disabled Slide Toggle + +``` + +## Theming +A slide-toggle is default using the `accent` palette for its styling. + +Modifiying the color on a `slide-toggle` can be easily done, by using the following classes. +- `md-primary` +- `md-warn` + +Here is an example markup, which uses the primary color. +```html + + Primary Slide Toggle + +``` + + diff --git a/src/components/slide-toggle/slide-toggle.html b/src/components/slide-toggle/slide-toggle.html new file mode 100644 index 000000000000..059bf4f8b1ba --- /dev/null +++ b/src/components/slide-toggle/slide-toggle.html @@ -0,0 +1,9 @@ +
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/src/components/slide-toggle/slide-toggle.scss b/src/components/slide-toggle/slide-toggle.scss new file mode 100644 index 000000000000..09d24880bfc2 --- /dev/null +++ b/src/components/slide-toggle/slide-toggle.scss @@ -0,0 +1,132 @@ +@import "../../core/style/variables"; +@import "../../core/style/mixins"; +@import "../../core/style/elevation"; + +//TODO(): remove the default theme. +@import "../../core/style/default-theme"; + +$md-slide-toggle-width: 36px !default; +$md-slide-toggle-height: 24px !default; +$md-slide-toggle-bar-height: 14px !default; +$md-slide-toggle-thumb-size: 20px !default; +$md-slide-toggle-margin: 16px !default; + +:host { + display: flex; + height: $md-slide-toggle-height; + + margin: $md-slide-toggle-margin 0; + line-height: $md-slide-toggle-height; + + white-space: nowrap; + cursor: pointer; + user-select: none; + + outline: none; + + &[disabled] { + cursor: default; + + .md-container { + cursor: default; + } + } + + .md-container { + cursor: grab; + width: $md-slide-toggle-width; + height: $md-slide-toggle-height; + + position: relative; + user-select: none; + + margin-right: 8px; + } + + .md-thumb-container { + position: absolute; + top: $md-slide-toggle-height / 2 - $md-slide-toggle-thumb-size / 2; + left: 0; + z-index: 1; + + width: $md-slide-toggle-width - $md-slide-toggle-thumb-size; + + transform: translate3d(0, 0, 0); + + transition: $swift-linear; + transition-property: transform; + + .md-thumb { + position: absolute; + margin: 0; + left: 0; + top: 0; + + height: $md-slide-toggle-thumb-size; + width: $md-slide-toggle-thumb-size; + border-radius: 50%; + + background-color: md-color($md-background, background); + @include md-elevation(1); + } + } + + &.md-checked .md-thumb-container { + transform: translate3d(100%, 0, 0); + } + + &.md-dragging .md-container { + cursor: grabbing; + } + + .md-bar { + position: absolute; + left: 1px; + top: $md-slide-toggle-height / 2 - $md-slide-toggle-bar-height / 2; + + width: $md-slide-toggle-width - 2px; + height: $md-slide-toggle-bar-height; + + background-color: md-color($md-grey, 500); + + border-radius: 8px; + } + + .md-bar, + .md-thumb { + transition: $swift-linear; + transition-property: background-color; + transition-delay: 0.05s; + } + + @mixin md-switch-checked($palette) { + .md-thumb { + background-color: md-color($palette); + } + + .md-bar { + background-color: md-color($palette, 0.5); + } + } + + &.md-checked { + @include md-switch-checked($md-accent); + + &.md-primary { + @include md-switch-checked($md-primary); + } + + &.md-warn { + @include md-switch-checked($md-warn); + } + } + + &[disabled] { + .md-thumb { + background-color: md-color($md-grey, 400); + } + .md-bar { + background-color: md-color($md-foreground, divider); + } + } +} diff --git a/src/components/slide-toggle/slide-toggle.spec.ts b/src/components/slide-toggle/slide-toggle.spec.ts new file mode 100644 index 000000000000..4ceb7260833e --- /dev/null +++ b/src/components/slide-toggle/slide-toggle.spec.ts @@ -0,0 +1,102 @@ +import { + it, + describe, + expect, + beforeEach, + inject, +} from '@angular/core/testing'; +import {TestComponentBuilder} from '@angular/compiler/testing'; +import {By} from '@angular/platform-browser'; +import {Component} from '@angular/core'; +import {MdSlideToggle} from './slide-toggle'; + +export function main() { + describe('MdSlideToggle', () => { + let builder: TestComponentBuilder; + + beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + builder = tcb; + })); + + it('should update the model correctly', (done: () => void) => { + return builder.createAsync(TestApp).then((fixture) => { + let testComponent = fixture.debugElement.componentInstance; + let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement; + + fixture.detectChanges(); + + expect(slideToggleEl.classList.contains('md-checked')).toBe(false); + + testComponent.slideModel = true; + fixture.detectChanges(); + + expect(slideToggleEl.classList.contains('md-checked')).toBe(true); + + done(); + }); + }); + + it('should correctly update aria-disabled', (done: () => void) => { + return builder.createAsync(TestApp).then((fixture) => { + let testComponent = fixture.debugElement.componentInstance; + let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement; + + fixture.detectChanges(); + + expect(slideToggleEl.getAttribute('aria-disabled')).toBe('false'); + + testComponent.isDisabled = true; + fixture.detectChanges(); + + expect(slideToggleEl.getAttribute('aria-disabled')).toBe('true'); + + done(); + }); + }); + + it('should correctly update aria-checked', (done: () => void) => { + return builder.createAsync(TestApp).then((fixture) => { + let testComponent = fixture.debugElement.componentInstance; + let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement; + + fixture.detectChanges(); + + expect(slideToggleEl.getAttribute('aria-checked')).toBe('false'); + + testComponent.slideModel = true; + fixture.detectChanges(); + + expect(slideToggleEl.getAttribute('aria-checked')).toBe('true'); + + done(); + }); + }); + + it('should set the toggle to checked on click', (done: () => void) => { + return builder.createAsync(TestApp).then((fixture) => { + let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement; + + slideToggleEl.click(); + + expect(slideToggleEl.classList.contains('md-checked')).toBe(true); + + done(); + }); + }); + + }); +} + +@Component({ + selector: 'test-app', + template: ` + + Test Slide Toggle + + `, + directives: [MdSlideToggle] +}) +class TestApp { + isDisabled: boolean = false; + slideModel: boolean = false; +} diff --git a/src/components/slide-toggle/slide-toggle.ts b/src/components/slide-toggle/slide-toggle.ts new file mode 100644 index 000000000000..7101492aafa8 --- /dev/null +++ b/src/components/slide-toggle/slide-toggle.ts @@ -0,0 +1,103 @@ +import { + Component, + ElementRef, + OnInit, + Optional, + Renderer +} from '@angular/core'; +import { + ControlValueAccessor, + NgControl +} from '@angular/common'; + +@Component({ + selector: 'md-slide-toggle', + inputs: ['disabled'], + host: { + '[attr.aria-disabled]': 'isAriaDisabled()', + '(click)': 'onClick()' + }, + templateUrl: './components/slide-toggle/slide-toggle.html', + styleUrls: ['./components/slide-toggle/slide-toggle.css'] +}) +export class MdSlideToggle implements OnInit, ControlValueAccessor { + + private nativeElement: HTMLElement; + private switchContainer: HTMLElement; + private thumbContainer: HTMLElement; + + private onChange = (_: any) => {}; + private onTouched = () => {}; + + private _checked: any; + private _disabled: boolean; + + constructor(elementRef: ElementRef, + private renderer: Renderer, + @Optional() ngControl: NgControl) { + + this.nativeElement = elementRef.nativeElement; + + if (ngControl) { + ngControl.valueAccessor = this; + } + } + + ngOnInit() { + this.switchContainer = this.nativeElement.querySelector('.md-container'); + this.thumbContainer = this.nativeElement.querySelector('.md-thumb-container'); + } + + onClick() { + if (!this.disabled) { + this.checked = !this.checked; + this.onTouched(); + } + } + + writeValue(value: any): void { + this.checked = value; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + isAriaDisabled(): string { + return this.disabled ? 'true' : 'false'; + } + + get disabled(): string | boolean { + return this._disabled; + } + + set disabled(value: string | boolean) { + this._disabled = value === '' || !!value; + + this.renderer + .setElementAttribute(this.nativeElement, 'disabled', this._disabled ? 'true' : null); + } + + get checked() { + return !!this._checked; + } + + set checked(value) { + this._checked = !!value; + + this.onTouched(); + + // Update the ngModel value accessor. + this.onChange(this._checked); + + this.renderer + .setElementAttribute(this.nativeElement, 'aria-checked', this._checked); + + this.nativeElement.classList.toggle('md-checked', this.checked); + } + +} diff --git a/src/core/style/_variables.scss b/src/core/style/_variables.scss index 00d65b5fc226..417a6cd0a0cf 100644 --- a/src/core/style/_variables.scss +++ b/src/core/style/_variables.scss @@ -41,3 +41,7 @@ $swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !defa $swift-ease-in-out-duration: 0.5s !default; $swift-ease-in-out-timing-function: $ease-in-out-curve-function !default; $swift-ease-in-out: all $swift-ease-in-out-duration $swift-ease-in-out-timing-function !default; + +$swift-linear-duration: 0.08s !default; +$swift-linear-timing-function: linear !default; +$swift-linear: all $swift-linear-duration $swift-linear-timing-function !default; \ No newline at end of file diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index a4b60622bc16..911e41ce2455 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -16,6 +16,7 @@ Progress Bar Radio Sidenav + Slide Toggle Toolbar diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts index 4e63e2206f48..3c1b1c92c841 100644 --- a/src/demo-app/demo-app.ts +++ b/src/demo-app/demo-app.ts @@ -22,6 +22,7 @@ import {InputDemo} from './input/input-demo'; import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo'; import {GesturesDemo} from './gestures/gestures-demo'; import {GridListDemo} from './grid-list/grid-list-demo'; +import {SlideToggleDemo} from './slide-toggle/slide-toggle-demo'; @Component({ selector: 'home', @@ -54,6 +55,7 @@ export class Home {} new Route({path: '/card', component: CardDemo}), new Route({path: '/radio', component: RadioDemo}), new Route({path: '/sidenav', component: SidenavDemo}), + new Route({path: '/slide-toggle', component: SlideToggleDemo}), new Route({path: '/progress-circle', component: ProgressCircleDemo}), new Route({path: '/progress-bar', component: ProgressBarDemo}), new Route({path: '/portal', component: PortalDemo}), diff --git a/src/demo-app/slide-toggle/slide-toggle-demo.html b/src/demo-app/slide-toggle/slide-toggle-demo.html new file mode 100644 index 000000000000..b0e002974987 --- /dev/null +++ b/src/demo-app/slide-toggle/slide-toggle-demo.html @@ -0,0 +1,14 @@ +
+ + + Default Slide Toggle + + + + Disabled Slide Toggle + + + + Disable Bound + +
\ No newline at end of file diff --git a/src/demo-app/slide-toggle/slide-toggle-demo.scss b/src/demo-app/slide-toggle/slide-toggle-demo.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/demo-app/slide-toggle/slide-toggle-demo.ts b/src/demo-app/slide-toggle/slide-toggle-demo.ts new file mode 100644 index 000000000000..68bf0398c1d1 --- /dev/null +++ b/src/demo-app/slide-toggle/slide-toggle-demo.ts @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; +import {MdSlideToggle} from '../../components/slide-toggle/slide-toggle'; + +@Component({ + selector: 'switch-demo', + templateUrl: 'demo-app/slide-toggle/slide-toggle-demo.html', + styleUrls: ['demo-app/slide-toggle/slide-toggle-demo.css'], + directives: [MdSlideToggle] +}) +export class SlideToggleDemo {}