Skip to content

Commit dc45b79

Browse files
devversionjelbourn
authored andcommitted
update(slide-toggle): add focus indicator (#475)
Fixes #471.
1 parent c469081 commit dc45b79

File tree

5 files changed

+109
-29
lines changed

5 files changed

+109
-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: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,28 @@ $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+
// When the slide-toggle is not checked and it shows its focus indicator, it should use a 12% opacity
34+
// of black in light themes and 12% of white in dark themes.
35+
background-color: md-color($md-foreground, divider);
36+
}
2137
}
2238
}
2339

@@ -34,21 +50,24 @@ $md-slide-toggle-margin: 16px !default;
3450
outline: none;
3551

3652
&.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-
4753
.md-slide-toggle-thumb-container {
4854
transform: translate3d(100%, 0, 0);
4955
}
5056
}
5157

58+
@include md-switch-checked($md-accent);
59+
@include md-switch-ripple($md-accent);
60+
61+
&.md-primary {
62+
@include md-switch-checked($md-primary);
63+
@include md-switch-ripple($md-primary);
64+
}
65+
66+
&.md-warn {
67+
@include md-switch-checked($md-warn);
68+
@include md-switch-ripple($md-warn);
69+
}
70+
5271
&.md-disabled {
5372

5473
.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
@@ -235,10 +235,30 @@ describe('MdSlideToggle', () => {
235235
expect(slideToggleElement.classList).toContain('md-checked');
236236
});
237237

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

240249
});
241250

251+
/**
252+
* Dispatches a focus change event from an element.
253+
* @param eventName Name of the event, either 'focus' or 'blur'.
254+
* @param element The element from which the event will be dispatched.
255+
*/
256+
function dispatchFocusChangeEvent(eventName: string, element: HTMLElement): void {
257+
let event = document.createEvent('Event');
258+
event.initEvent(eventName, true, true);
259+
element.dispatchEvent(event);
260+
}
261+
242262
@Component({
243263
selector: 'slide-toggle-test-app',
244264
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: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,40 @@
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+
* When $bindToParent is set to true, it will check for the focused class on the same selector as you included
44+
* that mixin.
45+
* It is also possible to specify the color palette of the temporary ripple. By default it uses the
46+
* accent palette for its background.
47+
*/
48+
@mixin md-temporary-ink-ripple($component, $bindToParent: false, $palette: $md-accent) {
4149
// TODO(mtlin): Replace when ink ripple component is implemented.
4250
// A placeholder ink ripple, shown when keyboard focused.
4351
.md-ink-ripple {
44-
background-color: md-color($md-accent);
4552
border-radius: 50%;
53+
opacity: 0;
4654
height: 48px;
4755
left: 50%;
48-
opacity: 0;
4956
overflow: hidden;
5057
pointer-events: none;
5158
position: absolute;
5259
top: 50%;
5360
transform: translate(-50%,-50%);
5461
transition: opacity ease 0.28s, background-color ease 0.28s;
5562
width: 48px;
63+
}
5664

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

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-
}
71+
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
72+
// this state.
73+
#{if($bindToParent, '&', '')}.md-#{$component}-disabled .md-ink-ripple {
74+
background-color: #000;
6775
}
6876
}

0 commit comments

Comments
 (0)