Skip to content

Commit 6fa546e

Browse files
committed
update(slide-toggle): add focus indicator
Fixes #471.
1 parent de2a9f2 commit 6fa546e

File tree

5 files changed

+104
-29
lines changed

5 files changed

+104
-29
lines changed

src/components/slide-toggle/slide-toggle.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
<div class="md-slide-toggle-container">
33
<div class="md-slide-toggle-bar"></div>
44
<div class="md-slide-toggle-thumb-container">
5-
<div class="md-slide-toggle-thumb"></div>
5+
<div class="md-slide-toggle-thumb">
6+
<div class="md-ink-ripple"></div>
7+
</div>
68
</div>
79

810
<input #input class="md-slide-toggle-checkbox" type="checkbox"
@@ -13,7 +15,8 @@
1315
[attr.name]="name"
1416
[attr.aria-label]="ariaLabel"
1517
[attr.aria-labelledby]="ariaLabelledby"
16-
(blur)="onTouched()"
18+
(blur)="onInputBlur()"
19+
(focus)="onInputFocus()"
1720
(change)="onChangeEvent()">
1821
</div>
1922
<span class="md-slide-toggle-content">

src/components/slide-toggle/slide-toggle.scss

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,26 @@ $md-slide-toggle-thumb-size: 20px !default;
1212
$md-slide-toggle-margin: 16px !default;
1313

1414
@mixin md-switch-checked($palette) {
15-
.md-slide-toggle-thumb {
16-
background-color: md-color($palette);
15+
&.md-checked {
16+
.md-slide-toggle-thumb {
17+
background-color: md-color($palette);
18+
}
19+
20+
.md-slide-toggle-bar {
21+
background-color: md-color($palette, 0.5);
22+
}
1723
}
24+
}
25+
26+
@mixin md-switch-ripple($palette) {
27+
// Temporary ripple effect for the thumb of the slide-toggle.
28+
// Bind to the parent selector and specify the current palette.
29+
@include md-temporary-ink-ripple(slide-toggle, true, $palette);
1830

19-
.md-slide-toggle-bar {
20-
background-color: md-color($palette, 0.5);
31+
&.md-slide-toggle-focused {
32+
&:not(.md-checked) .md-ink-ripple {
33+
background-color: rgba(black, 0.12);
34+
}
2135
}
2236
}
2337

@@ -34,21 +48,24 @@ $md-slide-toggle-margin: 16px !default;
3448
outline: none;
3549

3650
&.md-checked {
37-
@include md-switch-checked($md-accent);
38-
39-
&.md-primary {
40-
@include md-switch-checked($md-primary);
41-
}
42-
43-
&.md-warn {
44-
@include md-switch-checked($md-warn);
45-
}
46-
4751
.md-slide-toggle-thumb-container {
4852
transform: translate3d(100%, 0, 0);
4953
}
5054
}
5155

56+
@include md-switch-checked($md-accent);
57+
@include md-switch-ripple($md-accent);
58+
59+
&.md-primary {
60+
@include md-switch-checked($md-primary);
61+
@include md-switch-ripple($md-primary);
62+
}
63+
64+
&.md-warn {
65+
@include md-switch-checked($md-warn);
66+
@include md-switch-ripple($md-warn);
67+
}
68+
5269
&.md-disabled {
5370

5471
.md-slide-toggle-label, .md-slide-toggle-container {

src/components/slide-toggle/slide-toggle.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,31 @@ export function main() {
236236
expect(slideToggleElement.classList).toContain('md-checked');
237237
});
238238

239+
it('should correctly set the slide-toggle to checked on focus', () => {
240+
expect(slideToggleElement.classList).not.toContain('md-slide-toggle-focused');
241+
242+
dispatchFocusChangeEvent('focus', inputElement);
243+
fixture.detectChanges();
244+
245+
expect(slideToggleElement.classList).toContain('md-slide-toggle-focused');
246+
});
247+
239248
});
240249

241250
});
242251
}
243252

253+
/**
254+
* Dispatches a focus change event from an element.
255+
* @param eventName Name of the event, either 'focus' or 'blur'.
256+
* @param element The element from which the event will be dispatched.
257+
*/
258+
function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void {
259+
let event = document.createEvent('Event');
260+
event.initEvent(eventName, true, true);
261+
element.dispatchEvent(event);
262+
}
263+
244264
@Component({
245265
selector: 'slide-toggle-test-app',
246266
template: `

src/components/slide-toggle/slide-toggle.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ let nextId = 0;
3030
host: {
3131
'[class.md-checked]': 'checked',
3232
'[class.md-disabled]': 'disabled',
33-
'(click)': 'onTouched()'
33+
// This md-slide-toggle prefix will change, once the temporary ripple is removed.
34+
'[class.md-slide-toggle-focused]': '_hasFocus',
35+
'(click)': 'onTouched()',
36+
'(mousedown)': 'setMousedown()'
3437
},
3538
templateUrl: 'slide-toggle.html',
3639
styleUrls: ['slide-toggle.css'],
@@ -46,6 +49,8 @@ export class MdSlideToggle implements ControlValueAccessor {
4649
private _uniqueId = `md-slide-toggle-${++nextId}`;
4750
private _checked: boolean = false;
4851
private _color: string;
52+
private _hasFocus: boolean = false;
53+
private _isMousedown: boolean = false;
4954

5055
@Input() @BooleanFieldValue() disabled: boolean = false;
5156
@Input() name: string = null;
@@ -76,6 +81,31 @@ export class MdSlideToggle implements ControlValueAccessor {
7681
}
7782
}
7883

84+
/** @internal */
85+
setMousedown() {
86+
// We only *show* the focus style when focus has come to the button via the keyboard.
87+
// The Material Design spec is silent on this topic, and without doing this, the
88+
// button continues to look :active after clicking.
89+
// @see http://marcysutton.com/button-focus-hell/
90+
this._isMousedown = true;
91+
setTimeout(() => this._isMousedown = false, 100);
92+
}
93+
94+
/** @internal */
95+
onInputFocus() {
96+
// Only show the focus / ripple indicator when the focus was not triggered by a mouse
97+
// interaction on the component.
98+
if (!this._isMousedown) {
99+
this._hasFocus = true;
100+
}
101+
}
102+
103+
/** @internal */
104+
onInputBlur() {
105+
this._hasFocus = false;
106+
this.onTouched();
107+
}
108+
79109
/**
80110
* Implemented as part of ControlValueAccessor.
81111
* @internal

src/core/style/_mixins.scss

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,37 @@
3737
display: table;
3838
}
3939
}
40-
@mixin md-temporary-ink-ripple($component) {
40+
41+
/**
42+
* A mixin, which generates temporary ink ripple on a given component.
43+
* It can be also bound to the current parent selector.
44+
*/
45+
@mixin md-temporary-ink-ripple($component, $bindToParent: false, $palette: $md-accent) {
4146
// TODO(mtlin): Replace when ink ripple component is implemented.
4247
// A placeholder ink ripple, shown when keyboard focused.
4348
.md-ink-ripple {
44-
background-color: md-color($md-accent);
4549
border-radius: 50%;
50+
opacity: 0;
4651
height: 48px;
4752
left: 50%;
48-
opacity: 0;
4953
overflow: hidden;
5054
pointer-events: none;
5155
position: absolute;
5256
top: 50%;
5357
transform: translate(-50%,-50%);
5458
transition: opacity ease 0.28s, background-color ease 0.28s;
5559
width: 48px;
60+
}
5661

57-
// Fade in when radio focused.
58-
.md-#{$component}-focused & {
59-
opacity: 0.1;
60-
}
62+
// Fade in when radio focused.
63+
#{if($bindToParent, '&', '')}.md-#{$component}-focused .md-ink-ripple {
64+
opacity: 1;
65+
background-color: md-color($palette, 0.26);
66+
}
6167

62-
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
63-
// this state.
64-
.md-#{$component}-disabled & {
65-
background: #000;
66-
}
68+
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
69+
// this state.
70+
#{if($bindToParent, '&', '')}.md-#{$component}-disabled .md-ink-ripple {
71+
background-color: #000;
6772
}
6873
}

0 commit comments

Comments
 (0)