Skip to content

Commit

Permalink
feat(): add slide-toggle component.
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion committed May 4, 2016
1 parent 4f9051f commit de7f505
Show file tree
Hide file tree
Showing 11 changed files with 420 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/components/slide-toggle/README.md
Original file line number Diff line number Diff line change
@@ -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)

## `<md-slide-toggle>`
### Bound Properties

| Name | Type | Description |
| --- | --- | --- |
| `disabled` | boolean | Disables the `slide-toggle` |

### Examples
A basic slide-toggle would have the following markup.
```html
<md-slide-toggle [(ngModel)]="slideToggleModel">
Default Slide Toggle
</md-slide-toggle>
```

Slide toggle can be also disabled.
```html
<md-slide-toggle disabled>
Disabled Slide Toggle
</md-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
<md-slide-toggle class="md-primary">
Primary Slide Toggle
</md-slide-toggle>
```


9 changes: 9 additions & 0 deletions src/components/slide-toggle/slide-toggle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="md-container">
<div class="md-bar"></div>
<div class="md-thumb-container">
<div class="md-thumb"></div>
</div>
</div>
<div class="md-label">
<ng-content></ng-content>
</div>
132 changes: 132 additions & 0 deletions src/components/slide-toggle/slide-toggle.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
102 changes: 102 additions & 0 deletions src/components/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled">
<span>Test Slide Toggle</span>
</md-slide-toggle>
`,
directives: [MdSlideToggle]
})
class TestApp {
isDisabled: boolean = false;
slideModel: boolean = false;
}
103 changes: 103 additions & 0 deletions src/components/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
@@ -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 = <HTMLElement> this.nativeElement.querySelector('.md-container');
this.thumbContainer = <HTMLElement> 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);
}

}
Loading

0 comments on commit de7f505

Please sign in to comment.