Skip to content

Commit de7f505

Browse files
committed
feat(): add slide-toggle component.
1 parent 4f9051f commit de7f505

File tree

11 files changed

+420
-0
lines changed

11 files changed

+420
-0
lines changed

src/components/slide-toggle/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# MdSlideToggle
2+
`MdSlideToggle` is a two-state control, which can be also called `switch`
3+
4+
### Screenshots
5+
![image](https://cloud.githubusercontent.com/assets/4987015/14860895/25cc0dc0-0cab-11e6-9e57-9f6d513444b1.png)
6+
7+
## `<md-slide-toggle>`
8+
### Bound Properties
9+
10+
| Name | Type | Description |
11+
| --- | --- | --- |
12+
| `disabled` | boolean | Disables the `slide-toggle` |
13+
14+
### Examples
15+
A basic slide-toggle would have the following markup.
16+
```html
17+
<md-slide-toggle [(ngModel)]="slideToggleModel">
18+
Default Slide Toggle
19+
</md-slide-toggle>
20+
```
21+
22+
Slide toggle can be also disabled.
23+
```html
24+
<md-slide-toggle disabled>
25+
Disabled Slide Toggle
26+
</md-slide-toggle>
27+
```
28+
29+
## Theming
30+
A slide-toggle is default using the `accent` palette for its styling.
31+
32+
Modifiying the color on a `slide-toggle` can be easily done, by using the following classes.
33+
- `md-primary`
34+
- `md-warn`
35+
36+
Here is an example markup, which uses the primary color.
37+
```html
38+
<md-slide-toggle class="md-primary">
39+
Primary Slide Toggle
40+
</md-slide-toggle>
41+
```
42+
43+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="md-container">
2+
<div class="md-bar"></div>
3+
<div class="md-thumb-container">
4+
<div class="md-thumb"></div>
5+
</div>
6+
</div>
7+
<div class="md-label">
8+
<ng-content></ng-content>
9+
</div>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
@import "../../core/style/variables";
2+
@import "../../core/style/mixins";
3+
@import "../../core/style/elevation";
4+
5+
//TODO(): remove the default theme.
6+
@import "../../core/style/default-theme";
7+
8+
$md-slide-toggle-width: 36px !default;
9+
$md-slide-toggle-height: 24px !default;
10+
$md-slide-toggle-bar-height: 14px !default;
11+
$md-slide-toggle-thumb-size: 20px !default;
12+
$md-slide-toggle-margin: 16px !default;
13+
14+
:host {
15+
display: flex;
16+
height: $md-slide-toggle-height;
17+
18+
margin: $md-slide-toggle-margin 0;
19+
line-height: $md-slide-toggle-height;
20+
21+
white-space: nowrap;
22+
cursor: pointer;
23+
user-select: none;
24+
25+
outline: none;
26+
27+
&[disabled] {
28+
cursor: default;
29+
30+
.md-container {
31+
cursor: default;
32+
}
33+
}
34+
35+
.md-container {
36+
cursor: grab;
37+
width: $md-slide-toggle-width;
38+
height: $md-slide-toggle-height;
39+
40+
position: relative;
41+
user-select: none;
42+
43+
margin-right: 8px;
44+
}
45+
46+
.md-thumb-container {
47+
position: absolute;
48+
top: $md-slide-toggle-height / 2 - $md-slide-toggle-thumb-size / 2;
49+
left: 0;
50+
z-index: 1;
51+
52+
width: $md-slide-toggle-width - $md-slide-toggle-thumb-size;
53+
54+
transform: translate3d(0, 0, 0);
55+
56+
transition: $swift-linear;
57+
transition-property: transform;
58+
59+
.md-thumb {
60+
position: absolute;
61+
margin: 0;
62+
left: 0;
63+
top: 0;
64+
65+
height: $md-slide-toggle-thumb-size;
66+
width: $md-slide-toggle-thumb-size;
67+
border-radius: 50%;
68+
69+
background-color: md-color($md-background, background);
70+
@include md-elevation(1);
71+
}
72+
}
73+
74+
&.md-checked .md-thumb-container {
75+
transform: translate3d(100%, 0, 0);
76+
}
77+
78+
&.md-dragging .md-container {
79+
cursor: grabbing;
80+
}
81+
82+
.md-bar {
83+
position: absolute;
84+
left: 1px;
85+
top: $md-slide-toggle-height / 2 - $md-slide-toggle-bar-height / 2;
86+
87+
width: $md-slide-toggle-width - 2px;
88+
height: $md-slide-toggle-bar-height;
89+
90+
background-color: md-color($md-grey, 500);
91+
92+
border-radius: 8px;
93+
}
94+
95+
.md-bar,
96+
.md-thumb {
97+
transition: $swift-linear;
98+
transition-property: background-color;
99+
transition-delay: 0.05s;
100+
}
101+
102+
@mixin md-switch-checked($palette) {
103+
.md-thumb {
104+
background-color: md-color($palette);
105+
}
106+
107+
.md-bar {
108+
background-color: md-color($palette, 0.5);
109+
}
110+
}
111+
112+
&.md-checked {
113+
@include md-switch-checked($md-accent);
114+
115+
&.md-primary {
116+
@include md-switch-checked($md-primary);
117+
}
118+
119+
&.md-warn {
120+
@include md-switch-checked($md-warn);
121+
}
122+
}
123+
124+
&[disabled] {
125+
.md-thumb {
126+
background-color: md-color($md-grey, 400);
127+
}
128+
.md-bar {
129+
background-color: md-color($md-foreground, divider);
130+
}
131+
}
132+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
it,
3+
describe,
4+
expect,
5+
beforeEach,
6+
inject,
7+
} from '@angular/core/testing';
8+
import {TestComponentBuilder} from '@angular/compiler/testing';
9+
import {By} from '@angular/platform-browser';
10+
import {Component} from '@angular/core';
11+
import {MdSlideToggle} from './slide-toggle';
12+
13+
export function main() {
14+
describe('MdSlideToggle', () => {
15+
let builder: TestComponentBuilder;
16+
17+
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
18+
builder = tcb;
19+
}));
20+
21+
it('should update the model correctly', (done: () => void) => {
22+
return builder.createAsync(TestApp).then((fixture) => {
23+
let testComponent = fixture.debugElement.componentInstance;
24+
let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement;
25+
26+
fixture.detectChanges();
27+
28+
expect(slideToggleEl.classList.contains('md-checked')).toBe(false);
29+
30+
testComponent.slideModel = true;
31+
fixture.detectChanges();
32+
33+
expect(slideToggleEl.classList.contains('md-checked')).toBe(true);
34+
35+
done();
36+
});
37+
});
38+
39+
it('should correctly update aria-disabled', (done: () => void) => {
40+
return builder.createAsync(TestApp).then((fixture) => {
41+
let testComponent = fixture.debugElement.componentInstance;
42+
let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement;
43+
44+
fixture.detectChanges();
45+
46+
expect(slideToggleEl.getAttribute('aria-disabled')).toBe('false');
47+
48+
testComponent.isDisabled = true;
49+
fixture.detectChanges();
50+
51+
expect(slideToggleEl.getAttribute('aria-disabled')).toBe('true');
52+
53+
done();
54+
});
55+
});
56+
57+
it('should correctly update aria-checked', (done: () => void) => {
58+
return builder.createAsync(TestApp).then((fixture) => {
59+
let testComponent = fixture.debugElement.componentInstance;
60+
let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement;
61+
62+
fixture.detectChanges();
63+
64+
expect(slideToggleEl.getAttribute('aria-checked')).toBe('false');
65+
66+
testComponent.slideModel = true;
67+
fixture.detectChanges();
68+
69+
expect(slideToggleEl.getAttribute('aria-checked')).toBe('true');
70+
71+
done();
72+
});
73+
});
74+
75+
it('should set the toggle to checked on click', (done: () => void) => {
76+
return builder.createAsync(TestApp).then((fixture) => {
77+
let slideToggleEl = fixture.debugElement.query(By.css('md-slide-toggle')).nativeElement;
78+
79+
slideToggleEl.click();
80+
81+
expect(slideToggleEl.classList.contains('md-checked')).toBe(true);
82+
83+
done();
84+
});
85+
});
86+
87+
});
88+
}
89+
90+
@Component({
91+
selector: 'test-app',
92+
template: `
93+
<md-slide-toggle [(ngModel)]="slideModel" [disabled]="isDisabled">
94+
<span>Test Slide Toggle</span>
95+
</md-slide-toggle>
96+
`,
97+
directives: [MdSlideToggle]
98+
})
99+
class TestApp {
100+
isDisabled: boolean = false;
101+
slideModel: boolean = false;
102+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
Component,
3+
ElementRef,
4+
OnInit,
5+
Optional,
6+
Renderer
7+
} from '@angular/core';
8+
import {
9+
ControlValueAccessor,
10+
NgControl
11+
} from '@angular/common';
12+
13+
@Component({
14+
selector: 'md-slide-toggle',
15+
inputs: ['disabled'],
16+
host: {
17+
'[attr.aria-disabled]': 'isAriaDisabled()',
18+
'(click)': 'onClick()'
19+
},
20+
templateUrl: './components/slide-toggle/slide-toggle.html',
21+
styleUrls: ['./components/slide-toggle/slide-toggle.css']
22+
})
23+
export class MdSlideToggle implements OnInit, ControlValueAccessor {
24+
25+
private nativeElement: HTMLElement;
26+
private switchContainer: HTMLElement;
27+
private thumbContainer: HTMLElement;
28+
29+
private onChange = (_: any) => {};
30+
private onTouched = () => {};
31+
32+
private _checked: any;
33+
private _disabled: boolean;
34+
35+
constructor(elementRef: ElementRef,
36+
private renderer: Renderer,
37+
@Optional() ngControl: NgControl) {
38+
39+
this.nativeElement = elementRef.nativeElement;
40+
41+
if (ngControl) {
42+
ngControl.valueAccessor = this;
43+
}
44+
}
45+
46+
ngOnInit() {
47+
this.switchContainer = <HTMLElement> this.nativeElement.querySelector('.md-container');
48+
this.thumbContainer = <HTMLElement> this.nativeElement.querySelector('.md-thumb-container');
49+
}
50+
51+
onClick() {
52+
if (!this.disabled) {
53+
this.checked = !this.checked;
54+
this.onTouched();
55+
}
56+
}
57+
58+
writeValue(value: any): void {
59+
this.checked = value;
60+
}
61+
62+
registerOnChange(fn: any): void {
63+
this.onChange = fn;
64+
}
65+
66+
registerOnTouched(fn: any): void {
67+
this.onTouched = fn;
68+
}
69+
70+
isAriaDisabled(): string {
71+
return this.disabled ? 'true' : 'false';
72+
}
73+
74+
get disabled(): string | boolean {
75+
return this._disabled;
76+
}
77+
78+
set disabled(value: string | boolean) {
79+
this._disabled = value === '' || !!value;
80+
81+
this.renderer
82+
.setElementAttribute(this.nativeElement, 'disabled', this._disabled ? 'true' : null);
83+
}
84+
85+
get checked() {
86+
return !!this._checked;
87+
}
88+
89+
set checked(value) {
90+
this._checked = !!value;
91+
92+
this.onTouched();
93+
94+
// Update the ngModel value accessor.
95+
this.onChange(this._checked);
96+
97+
this.renderer
98+
.setElementAttribute(this.nativeElement, 'aria-checked', this._checked);
99+
100+
this.nativeElement.classList.toggle('md-checked', this.checked);
101+
}
102+
103+
}

0 commit comments

Comments
 (0)