Skip to content

Commit 83ef5af

Browse files
committed
feat(material-experimental/mdc-slide-toggle): switch to non-deprecated styles
Switches the MDC-based slide toggle to the non-deprecated MDC styles. Notable changes: * New markup which uses a `button` instead of an `input` for the button. * New icons inside the slide toggle's thumb. Technically we could opt out of them, but I think that they look better and they help with accessibility for color-blind users. * New theming system that uses a flat list of variables. There is a fallback for IE11, but I opted not to include it for now, because of the upcoming deprecation and the fact that the component is in experimental. * The component has some slightly different colors and it supports more states (e.g. hover). * Due to the switch from `input` to `button`, the `required` input is basically a noop now.
1 parent 54218d6 commit 83ef5af

File tree

15 files changed

+313
-344
lines changed

15 files changed

+313
-344
lines changed

scripts/check-mdc-tests-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ export const config = {
175175
'should re-add margin if label is added asynchronously',
176176
'should properly update margin if label content is projected',
177177

178+
// The MDC implementation uses a `button` instead of an `input` which can't be required.
179+
'should forward the required attribute',
180+
'should prevent the form from submit when being required',
181+
178182
// TODO: the focus origin functionality has to be implemeted for the MDC slide toggle.
179183
'should not change focus origin if origin not specified'
180184
],

src/material-experimental/mdc-helpers/_focus-indicators.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
// which will clip a square focus indicator so we have to turn it into a circle.
6161
.mat-mdc-checkbox-ripple.mat-mdc-focus-indicator::before,
6262
.mat-radio-ripple.mat-mdc-focus-indicator::before,
63-
.mat-mdc-slider .mat-mdc-focus-indicator::before {
63+
.mat-mdc-slider .mat-mdc-focus-indicator::before,
64+
.mat-mdc-slide-toggle .mat-mdc-focus-indicator::before {
6465
border-radius: 50%;
6566
}
6667

src/material-experimental/mdc-slide-toggle/_slide-toggle-theme.scss

Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,121 @@
1-
@use '@material/theme/theme-color' as mdc-theme-color;
2-
@use '@material/switch/deprecated' as mdc-switch with ($deprecated-suffix: '');
3-
@use '@material/form-field' as mdc-form-field;
41
@use 'sass:map';
2+
@use 'sass:color';
3+
@use '@material/switch/switch-theme' as mdc-switch-theme;
4+
@use '@material/theme/color-palette' as mdc-color-palette;
5+
@use '@material/form-field' as mdc-form-field;
56
@use '../mdc-helpers/mdc-helpers';
67
@use '../../material/core/typography/typography';
78
@use '../../material/core/theming/theming';
9+
@use '../../material/core/theming/palette';
10+
11+
// Generates all color mapping for the properties that only change based on the theme.
12+
@function _get-theme-base-map($is-dark) {
13+
$on-surface: if($is-dark, mdc-color-palette.$grey-100, mdc-color-palette.$grey-800);
14+
$hairline: if($is-dark, mdc-color-palette.$grey-500, mdc-color-palette.$grey-300);
15+
$on-surface-variant: if($is-dark, mdc-color-palette.$grey-200, mdc-color-palette.$grey-700);
16+
$on-surface-state-content: if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900);
17+
$disabled-handle-color: mdc-color-palette.$grey-800;
18+
$selected-icon-color: mdc-color-palette.$grey-100;
19+
$icon-color: if($is-dark, mdc-color-palette.$grey-800, mdc-color-palette.$grey-100);
20+
21+
@return (
22+
disabled-selected-handle-color: $disabled-handle-color,
23+
disabled-unselected-handle-color: $disabled-handle-color,
24+
25+
disabled-selected-track-color: $on-surface,
26+
disabled-unselected-track-color: $on-surface,
27+
unselected-focus-state-layer-color: $on-surface,
28+
unselected-pressed-state-layer-color: $on-surface,
29+
unselected-hover-state-layer-color: $on-surface,
30+
31+
unselected-focus-track-color: $hairline,
32+
unselected-hover-track-color: $hairline,
33+
unselected-pressed-track-color: $hairline,
34+
unselected-track-color: $hairline,
35+
36+
unselected-focus-handle-color: $on-surface-state-content,
37+
unselected-hover-handle-color: $on-surface-state-content,
38+
unselected-pressed-handle-color: $on-surface-state-content,
39+
40+
handle-surface-color: surface,
41+
unselected-handle-color: $on-surface-variant,
42+
43+
selected-icon-color: $selected-icon-color,
44+
disabled-selected-icon-color: $icon-color,
45+
disabled-unselected-icon-color: $icon-color,
46+
unselected-icon-color: $icon-color,
47+
);
48+
}
849

50+
// Generates the mapping for the properties that change based on the slide toggle color.
51+
@function _get-theme-color-map($color-palette) {
52+
$state-content: color.scale($color-palette, $blackness: 50%);
53+
$inverse: color.scale($color-palette, $lightness: 75%);
54+
55+
@return (
56+
selected-focus-state-layer-color: $color-palette,
57+
selected-handle-color: $color-palette,
58+
selected-hover-state-layer-color: $color-palette,
59+
selected-pressed-state-layer-color: $color-palette,
60+
61+
selected-focus-handle-color: $state-content,
62+
selected-hover-handle-color: $state-content,
63+
selected-pressed-handle-color: $state-content,
64+
65+
selected-focus-track-color: $inverse,
66+
selected-hover-track-color: $inverse,
67+
selected-pressed-track-color: $inverse,
68+
selected-track-color: $inverse,
69+
);
70+
}
971

1072
@mixin color($config-or-theme) {
1173
$config: theming.get-color-config($config-or-theme);
1274
$primary: theming.get-color-from-palette(map.get($config, primary));
1375
$accent: theming.get-color-from-palette(map.get($config, accent));
1476
$warn: theming.get-color-from-palette(map.get($config, warn));
15-
16-
// Save original values of MDC global variables. We need to save these so we can restore the
17-
// variables to their original values and prevent unintended side effects from using this mixin.
18-
$orig-baseline-theme-color: mdc-switch.$baseline-theme-color;
19-
$orig-toggled-off-thumb-color: mdc-switch.$toggled-off-thumb-color;
20-
$orig-toggled-off-track-color: mdc-switch.$toggled-off-track-color;
21-
$orig-disabled-thumb-color: mdc-switch.$disabled-thumb-color;
22-
$orig-disabled-track-color: mdc-switch.$disabled-track-color;
77+
$is-dark: map.get($config, is-dark);
2378

2479
@include mdc-helpers.mat-using-mdc-theme($config) {
25-
mdc-switch.$baseline-theme-color: primary;
26-
mdc-switch.$toggled-off-thumb-color: mdc-theme-color.prop-value(surface);
27-
mdc-switch.$toggled-off-track-color: mdc-theme-color.prop-value(on-surface);
28-
mdc-switch.$disabled-thumb-color: mdc-theme-color.prop-value(surface);
29-
mdc-switch.$disabled-track-color: mdc-theme-color.prop-value(on-surface);
30-
3180
// MDC's switch doesn't support a `color` property. We add support
3281
// for it by adding a CSS class for accent and warn style.
3382
.mat-mdc-slide-toggle {
3483
@include mdc-form-field.core-styles($query: mdc-helpers.$mat-theme-styles-query);
35-
36-
.mdc-switch__thumb-underlay::after, .mat-ripple-element {
37-
background: mdc-switch.$toggled-off-ripple-color;
38-
}
84+
@include mdc-switch-theme.theme(_get-theme-base-map($is-dark));
3985

4086
&.mat-primary {
41-
@include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
87+
@include mdc-switch-theme.theme(_get-theme-color-map($primary));
4288
}
4389

4490
&.mat-accent {
45-
mdc-switch.$baseline-theme-color: secondary;
46-
@include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
91+
@include mdc-switch-theme.theme(_get-theme-color-map($accent));
4792
}
4893

4994
&.mat-warn {
50-
mdc-switch.$baseline-theme-color: error;
51-
@include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
52-
}
53-
}
54-
55-
// The ripple color matches the palette only when it's checked.
56-
.mat-mdc-slide-toggle-checked {
57-
.mdc-switch__thumb-underlay::after, .mat-ripple-element {
58-
background: $primary;
59-
}
60-
61-
&.mat-accent {
62-
.mdc-switch__thumb-underlay::after, .mat-ripple-element {
63-
background: $accent;
64-
}
65-
}
66-
67-
&.mat-warn {
68-
.mdc-switch__thumb-underlay::after, .mat-ripple-element {
69-
background: $warn;
70-
}
95+
@include mdc-switch-theme.theme(_get-theme-color-map($warn));
7196
}
7297
}
7398
}
74-
75-
// Restore original values of MDC global variables.
76-
mdc-switch.$baseline-theme-color: $orig-baseline-theme-color;
77-
mdc-switch.$toggled-off-thumb-color: $orig-toggled-off-thumb-color;
78-
mdc-switch.$toggled-off-track-color: $orig-toggled-off-track-color;
79-
mdc-switch.$disabled-thumb-color: $orig-disabled-thumb-color;
80-
mdc-switch.$disabled-track-color: $orig-disabled-track-color;
8199
}
82100

83101
@mixin typography($config-or-theme) {
84102
$config: typography.private-typography-to-2018-config(
85103
theming.get-typography-config($config-or-theme));
86104
@include mdc-helpers.mat-using-mdc-typography($config) {
87-
@include mdc-switch.without-ripple($query: mdc-helpers.$mat-typography-styles-query);
88105
@include mdc-form-field.core-styles($query: mdc-helpers.$mat-typography-styles-query);
89106
}
90107
}
91108

92109
@mixin density($config-or-theme) {
93110
$density-scale: theming.get-density-config($config-or-theme);
94-
.mat-mdc-slide-toggle .mdc-switch {
95-
@include mdc-switch.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
111+
.mat-mdc-slide-toggle {
112+
@include mdc-switch-theme.theme(mdc-switch-theme.density($density-scale));
96113
}
97114
}
98115

99116
@mixin theme($theme-or-color-config) {
100117
$theme: theming.private-legacy-get-theme($theme-or-color-config);
118+
101119
@include theming.private-check-duplicate-theme-styles($theme, 'mat-mdc-slide-toggle') {
102120
$color: theming.get-color-config($theme);
103121
$density: theming.get-density-config($theme);

src/material-experimental/mdc-slide-toggle/slide-toggle.e2e.spec.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {expectToExist} from '@angular/cdk/testing/private/e2e';
33

44

55
describe('MDC-based slide-toggle', () => {
6-
const getInput = () => element(by.css('#normal-slide-toggle input'));
6+
const getButton = () => element(by.css('#normal-slide-toggle button'));
77
const getNormalToggle = () => element(by.css('#normal-slide-toggle'));
88

99
beforeEach(async () => await browser.get('mdc-slide-toggle'));
@@ -13,44 +13,44 @@ describe('MDC-based slide-toggle', () => {
1313
});
1414

1515
it('should change the checked state on click', async () => {
16-
const inputEl = getInput();
16+
const buttonEl = getButton();
1717

18-
expect(await inputEl.getAttribute('checked'))
19-
.toBeFalsy('Expect slide-toggle to be unchecked');
18+
expect(await buttonEl.getAttribute('aria-checked'))
19+
.toBe('false', 'Expect slide-toggle to be unchecked');
2020

2121
await getNormalToggle().click();
2222

23-
expect(await inputEl.getAttribute('checked'))
24-
.toBeTruthy('Expect slide-toggle to be checked');
23+
expect(await buttonEl.getAttribute('aria-checked'))
24+
.toBe('true', 'Expect slide-toggle to be checked');
2525
});
2626

2727
it('should change the checked state on click', async () => {
28-
const inputEl = getInput();
28+
const buttonEl = getButton();
2929

30-
expect(await inputEl.getAttribute('checked'))
31-
.toBeFalsy('Expect slide-toggle to be unchecked');
30+
expect(await buttonEl.getAttribute('aria-checked'))
31+
.toBe('false', 'Expect slide-toggle to be unchecked');
3232

3333
await getNormalToggle().click();
3434

35-
expect(await inputEl.getAttribute('checked'))
36-
.toBeTruthy('Expect slide-toggle to be checked');
35+
expect(await buttonEl.getAttribute('aria-checked'))
36+
.toBe('true', 'Expect slide-toggle to be checked');
3737
});
3838

3939
it('should not change the checked state on click when disabled', async () => {
40-
const inputEl = getInput();
40+
const buttonEl = getButton();
4141

42-
expect(await inputEl.getAttribute('checked'))
43-
.toBeFalsy('Expect slide-toggle to be unchecked');
42+
expect(await buttonEl.getAttribute('aria-checked'))
43+
.toBe('false', 'Expect slide-toggle to be unchecked');
4444

4545
await element(by.css('#disabled-slide-toggle')).click();
4646

47-
expect(await inputEl.getAttribute('checked'))
48-
.toBeFalsy('Expect slide-toggle to be unchecked');
47+
expect(await buttonEl.getAttribute('aria-checked'))
48+
.toBe('false', 'Expect slide-toggle to be unchecked');
4949
});
5050

5151
it('should move the thumb on state change', async () => {
5252
const slideToggleEl = getNormalToggle();
53-
const thumbEl = element(by.css('#normal-slide-toggle .mdc-switch__thumb-underlay'));
53+
const thumbEl = element(by.css('#normal-slide-toggle .mdc-switch__handle'));
5454
const previousPosition = await thumbEl.getLocation();
5555

5656
await slideToggleEl.click();
@@ -61,15 +61,15 @@ describe('MDC-based slide-toggle', () => {
6161
});
6262

6363
it('should toggle the slide-toggle on space key', async () => {
64-
const inputEl = getInput();
64+
const buttonEl = getButton();
6565

66-
expect(await inputEl.getAttribute('checked'))
67-
.toBeFalsy('Expect slide-toggle to be unchecked');
66+
expect(await buttonEl.getAttribute('aria-checked'))
67+
.toBe('false', 'Expect slide-toggle to be unchecked');
6868

69-
await inputEl.sendKeys(Key.SPACE);
69+
await buttonEl.sendKeys(Key.SPACE);
7070

71-
expect(await inputEl.getAttribute('checked'))
72-
.toBeTruthy('Expect slide-toggle to be checked');
71+
expect(await buttonEl.getAttribute('aria-checked'))
72+
.toBe('true', 'Expect slide-toggle to be checked');
7373
});
7474

7575
});
Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,50 @@
11
<div class="mdc-form-field"
22
[class.mdc-form-field--align-end]="labelPosition == 'before'">
3-
<div class="mdc-switch mat-mdc-switch" #switch>
3+
<button
4+
class="mdc-switch"
5+
role="switch"
6+
type="button"
7+
[class.mdc-switch--selected]="checked"
8+
[class.mdc-switch--unselected]="!checked"
9+
[tabIndex]="tabIndex"
10+
[disabled]="disabled"
11+
[attr.id]="buttonId"
12+
[attr.name]="name"
13+
[attr.aria-label]="ariaLabel"
14+
[attr.aria-labelledby]="_getAriaLabelledBy()"
15+
[attr.aria-describedby]="ariaDescribedby"
16+
(click)="_handleClick($event)"
17+
#switch>
418
<div class="mdc-switch__track"></div>
5-
<div class="mdc-switch__thumb-underlay mat-mdc-focus-indicator">
6-
<div class="mat-mdc-slide-toggle-ripple" mat-ripple
7-
[matRippleTrigger]="switch"
8-
[matRippleDisabled]="disableRipple || disabled"
9-
[matRippleCentered]="true"
10-
[matRippleAnimation]="_rippleAnimation"></div>
11-
<div class="mdc-switch__thumb">
12-
<input #input class="mdc-switch__native-control" type="checkbox"
13-
role="switch"
14-
[id]="inputId"
15-
[required]="required"
16-
[tabIndex]="tabIndex"
17-
[checked]="checked"
18-
[disabled]="disabled"
19-
[attr.name]="name"
20-
[attr.aria-checked]="checked.toString()"
21-
[attr.aria-label]="ariaLabel"
22-
[attr.aria-labelledby]="ariaLabelledby"
23-
[attr.aria-describedby]="ariaDescribedby"
24-
(change)="_onChangeEvent($event)"
25-
(click)="_onInputClick($event)">
19+
<div class="mdc-switch__handle-track">
20+
<div class="mdc-switch__handle">
21+
<div class="mdc-switch__shadow">
22+
<div class="mdc-elevation-overlay"></div>
23+
</div>
24+
<div class="mdc-switch__ripple">
25+
<div class="mat-mdc-slide-toggle-ripple mat-mdc-focus-indicator" mat-ripple
26+
[matRippleTrigger]="switch"
27+
[matRippleDisabled]="disableRipple || disabled"
28+
[matRippleCentered]="true"
29+
[matRippleAnimation]="_rippleAnimation"></div>
30+
</div>
31+
<div class="mdc-switch__icons">
32+
<svg class="mdc-switch__icon mdc-switch__icon--on" viewBox="0 0 24 24">
33+
<path d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z" />
34+
</svg>
35+
<svg class="mdc-switch__icon mdc-switch__icon--off" viewBox="0 0 24 24">
36+
<path d="M20 13H4v-2h16v2z" />
37+
</svg>
38+
</div>
2639
</div>
2740
</div>
28-
</div>
41+
</button>
2942

30-
<label [for]="inputId">
43+
<!--
44+
Clicking on the label will trigger another click event from the button.
45+
Stop propagation here so other listeners further up in the DOM don't execute twice.
46+
-->
47+
<label [for]="buttonId" [attr.id]="_labelId" (click)="$event.stopPropagation()">
3148
<ng-content></ng-content>
3249
</label>
3350
</div>

0 commit comments

Comments
 (0)