Skip to content

Commit 40222e9

Browse files
committed
feat(material/slide-toggle): enabling custom icons through directives
Introduces 4 directives: `matCheckedIcon`, `matUncheckedIcon`, `matCheckedDisabledIcon`, and `matUncheckedDisabledIcon` These, used in conjuction with `<mat-icon>`s or svgs will enable custom icons to be injected into the thumb of the slide-toggle as per the m3 spec. Reworks hideIcon directive to enable hiding icons based off 4 strings: "both", "checked", "unchecked", and "none". Casts true and false to "both" and "none" respectively. Casts undefined to "none" and "" to "both". This is to not break any current functionality. Implements #28977
1 parent db7ab92 commit 40222e9

File tree

9 files changed

+324
-49
lines changed

9 files changed

+324
-49
lines changed

src/dev-app/slide-toggle/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ng_project(
1313
"//:node_modules/@angular/core",
1414
"//:node_modules/@angular/forms",
1515
"//src/material/button",
16+
"//src/material/icon",
1617
"//src/material/slide-toggle",
1718
],
1819
)

src/dev-app/slide-toggle/slide-toggle-demo.html

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,71 @@
11
<div class="demo-slide-toggle">
2-
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle">Default Slide Toggle</mat-slide-toggle>
2+
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle"
3+
>Default Slide Toggle</mat-slide-toggle
4+
>
35
<mat-slide-toggle [(ngModel)]="firstToggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
46
<mat-slide-toggle [disabled]="firstToggle">Disable Bound</mat-slide-toggle>
5-
<mat-slide-toggle disabled disabledInteractive [(ngModel)]="firstToggle">Disabled Interactive Toggle</mat-slide-toggle>
7+
<mat-slide-toggle disabled disabledInteractive [(ngModel)]="firstToggle"
8+
>Disabled Interactive Toggle</mat-slide-toggle
9+
>
10+
<mat-slide-toggle hideIcon="unchecked" [(ngModel)]="firstToggle"
11+
>No icon (While Unchecked)</mat-slide-toggle
12+
>
13+
<mat-slide-toggle hideIcon="checked" [(ngModel)]="firstToggle"
14+
>No icon (While Checked)</mat-slide-toggle
15+
>
616
<mat-slide-toggle hideIcon [(ngModel)]="firstToggle">No icon</mat-slide-toggle>
7-
17+
<mat-slide-toggle [(ngModel)]="firstToggle">
18+
<mat-icon matCheckedIcon>light_mode</mat-icon>
19+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
20+
Custom Icons
21+
</mat-slide-toggle>
22+
<mat-slide-toggle disabled [(ngModel)]="firstToggle">
23+
<mat-icon matCheckedIcon>light_mode</mat-icon>
24+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
25+
Disabled Custom Icons
26+
</mat-slide-toggle>
27+
<mat-slide-toggle [disabled]="firstToggle">
28+
<mat-icon matCheckedIcon>light_mode</mat-icon>
29+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
30+
<mat-icon matCheckedDisabledIcon>lock</mat-icon>
31+
<mat-icon matUncheckedDisabledIcon>lock_open</mat-icon>
32+
Disabled Custom Disabled Icons
33+
</mat-slide-toggle>
834
<p>With label before the slide toggle.</p>
935

10-
<mat-slide-toggle labelPosition="before" color="primary" [(ngModel)]="firstToggle">Default Slide Toggle</mat-slide-toggle>
11-
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
36+
<mat-slide-toggle labelPosition="before" color="primary" [(ngModel)]="firstToggle"
37+
>Default Slide Toggle</mat-slide-toggle
38+
>
39+
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle" disabled
40+
>Disabled Slide Toggle</mat-slide-toggle
41+
>
1242
<mat-slide-toggle labelPosition="before" [disabled]="firstToggle">Disable Bound</mat-slide-toggle>
13-
<mat-slide-toggle labelPosition="before" hideIcon [(ngModel)]="firstToggle">No icon</mat-slide-toggle>
43+
<mat-slide-toggle labelPosition="before" hideIcon="unchecked" [(ngModel)]="firstToggle"
44+
>No icon (While Unchecked)</mat-slide-toggle
45+
>
46+
<mat-slide-toggle labelPosition="before" hideIcon="checked" [(ngModel)]="firstToggle"
47+
>No icon (While Checked)</mat-slide-toggle
48+
>
49+
<mat-slide-toggle labelPosition="before" hideIcon [(ngModel)]="firstToggle"
50+
>No icon</mat-slide-toggle
51+
>
52+
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle">
53+
<mat-icon matCheckedIcon>light_mode</mat-icon>
54+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
55+
Custom Icons
56+
</mat-slide-toggle>
57+
<mat-slide-toggle labelPosition="before" disabled [(ngModel)]="firstToggle">
58+
<mat-icon matCheckedIcon>light_mode</mat-icon>
59+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
60+
Disabled Custom Icons
61+
</mat-slide-toggle>
62+
<mat-slide-toggle labelPosition="before" [disabled]="firstToggle">
63+
<mat-icon matCheckedIcon>light_mode</mat-icon>
64+
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
65+
<mat-icon matCheckedDisabledIcon>lock</mat-icon>
66+
<mat-icon matUncheckedDisabledIcon>lock_open</mat-icon>
67+
Disabled Custom Disabled Icons
68+
</mat-slide-toggle>
1469

1570
<p>Example where the slide toggle is required inside of a form.</p>
1671

src/dev-app/slide-toggle/slide-toggle-demo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import {ChangeDetectionStrategy, Component} from '@angular/core';
1010
import {FormsModule} from '@angular/forms';
1111
import {MatButtonModule} from '@angular/material/button';
1212
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
13+
import {MatIconModule} from '@angular/material/icon';
1314

1415
@Component({
1516
selector: 'slide-toggle-demo',
1617
templateUrl: 'slide-toggle-demo.html',
1718
styleUrl: 'slide-toggle-demo.css',
18-
imports: [FormsModule, MatButtonModule, MatSlideToggleModule],
19+
imports: [FormsModule, MatButtonModule, MatSlideToggleModule, MatIconModule],
1920
changeDetection: ChangeDetectionStrategy.OnPush,
2021
})
2122
export class SlideToggleDemo {

src/material/slide-toggle/slide-toggle-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface MatSlideToggleDefaultOptions {
2323
color?: ThemePalette;
2424

2525
/** Whether to hide the icon inside the slide toggle. */
26-
hideIcon?: boolean;
26+
hideIcon?: 'both' | 'checked' | 'unchecked' | 'none';
2727

2828
/** Whether disabled slide toggles should remain interactive. */
2929
disabledInteractive?: boolean;
@@ -34,6 +34,6 @@ export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatSlideToggl
3434
'mat-slide-toggle-default-options',
3535
{
3636
providedIn: 'root',
37-
factory: () => ({disableToggleValue: false, hideIcon: false, disabledInteractive: false}),
37+
factory: () => ({disableToggleValue: false, hideIcon: 'none', disabledInteractive: false}),
3838
},
3939
);

src/material/slide-toggle/slide-toggle.html

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
[attr.aria-checked]="checked"
2020
[attr.aria-disabled]="disabled && disabledInteractive ? 'true' : null"
2121
(click)="_handleClick()"
22-
#switch>
22+
#switch
23+
>
2324
<div class="mat-mdc-slide-toggle-touch-target"></div>
2425
<span class="mdc-switch__track"></span>
2526
<span class="mdc-switch__handle-track">
@@ -28,26 +29,21 @@
2829
<span class="mdc-elevation-overlay"></span>
2930
</span>
3031
<span class="mdc-switch__ripple">
31-
<span class="mat-mdc-slide-toggle-ripple mat-focus-indicator" mat-ripple
32+
<span
33+
class="mat-mdc-slide-toggle-ripple mat-focus-indicator"
34+
mat-ripple
3235
[matRippleTrigger]="switch"
3336
[matRippleDisabled]="disableRipple || disabled"
34-
[matRippleCentered]="true"></span>
37+
[matRippleCentered]="true"
38+
></span>
3539
</span>
36-
@if (!hideIcon) {
37-
<span class="mdc-switch__icons">
38-
<svg
39-
class="mdc-switch__icon mdc-switch__icon--on"
40-
viewBox="0 0 24 24"
41-
aria-hidden="true">
42-
<path d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z" />
43-
</svg>
44-
<svg
45-
class="mdc-switch__icon mdc-switch__icon--off"
46-
viewBox="0 0 24 24"
47-
aria-hidden="true">
48-
<path d="M20 13H4v-2h16v2z" />
49-
</svg>
50-
</span>
40+
41+
@if (hideIcon !== "both") {
42+
<ng-container
43+
[ngTemplateOutlet]="checked ?
44+
(hideIcon === 'checked' ? null : onIconsTemplate) :
45+
(hideIcon === 'unchecked' ? null : offIconsTemplate)"
46+
></ng-container>
5147
}
5248
</span>
5349
</span>
@@ -56,8 +52,45 @@
5652
<!--
5753
Clicking on the label will trigger another click event from the button.
5854
Stop propagation here so other listeners further up in the DOM don't execute twice.
59-
-->
55+
-->
6056
<label class="mdc-label" [for]="buttonId" [attr.id]="_labelId" (click)="$event.stopPropagation()">
6157
<ng-content></ng-content>
6258
</label>
6359
</div>
60+
61+
<ng-template #onDefaultTemplate>
62+
<ng-content select="[matCheckedIcon]">
63+
<svg matCheckedIcon viewBox="0 0 24 24" aria-hidden="true">
64+
<path d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z" />
65+
</svg>
66+
</ng-content>
67+
</ng-template>
68+
<ng-template #onIconsTemplate>
69+
<span class="mdc-switch__icons">
70+
@if (disabled) {
71+
<ng-content select="[matCheckedDisabledIcon]">
72+
<ng-container *ngTemplateOutlet="onDefaultTemplate"></ng-container>
73+
</ng-content>
74+
} @else {
75+
<ng-container *ngTemplateOutlet="onDefaultTemplate"></ng-container>
76+
}
77+
</span>
78+
</ng-template>
79+
<ng-template #offDefaultTemplate>
80+
<ng-content select="[matUncheckedIcon]">
81+
<svg matUncheckedIcon viewBox="0 0 24 24" aria-hidden="true">
82+
<path d="M4,13h16v-2H4v2z" />
83+
</svg>
84+
</ng-content>
85+
</ng-template>
86+
<ng-template #offIconsTemplate>
87+
<span class="mdc-switch__icons">
88+
@if (disabled) {
89+
<ng-content select="[matUncheckedDisabledIcon]">
90+
<ng-container *ngTemplateOutlet="offDefaultTemplate"></ng-container>
91+
</ng-content>
92+
} @else {
93+
<ng-container *ngTemplateOutlet="offDefaultTemplate"></ng-container>
94+
}
95+
</span>
96+
</ng-template>

src/material/slide-toggle/slide-toggle.scss

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ $_interactive-disabled-selector: '.mat-mdc-slide-toggle-disabled-interactive.mdc
88

99
$fallbacks: m3-slide-toggle.get-tokens();
1010

11+
[matCheckedIcon],
12+
[matUncheckedIcon],
13+
[matCheckedDisabledIcon],
14+
[matUncheckedDisabledIcon] {
15+
display: none;
16+
}
17+
1118
.mdc-switch {
1219
align-items: center;
1320
background: none;
@@ -66,9 +73,13 @@ $fallbacks: m3-slide-toggle.get-tokens();
6673

6774
.mdc-switch--disabled & {
6875
border-width: token-utils.slot(
69-
slide-toggle-disabled-unselected-track-outline-width, $fallbacks);
76+
slide-toggle-disabled-unselected-track-outline-width,
77+
$fallbacks
78+
);
7079
border-color: token-utils.slot(
71-
slide-toggle-disabled-unselected-track-outline-color, $fallbacks);
80+
slide-toggle-disabled-unselected-track-outline-color,
81+
$fallbacks
82+
);
7283
}
7384
}
7485

@@ -223,7 +234,9 @@ $fallbacks: m3-slide-toggle.get-tokens();
223234

224235
&:has(.mdc-switch__icons) {
225236
margin: token-utils.slot(
226-
slide-toggle-unselected-with-icon-handle-horizontal-margin, $fallbacks);
237+
slide-toggle-unselected-with-icon-handle-horizontal-margin,
238+
$fallbacks
239+
);
227240
}
228241
}
229242

@@ -234,7 +247,9 @@ $fallbacks: m3-slide-toggle.get-tokens();
234247

235248
&:has(.mdc-switch__icons) {
236249
margin: token-utils.slot(
237-
slide-toggle-selected-with-icon-handle-horizontal-margin, $fallbacks);
250+
slide-toggle-selected-with-icon-handle-horizontal-margin,
251+
$fallbacks
252+
);
238253
}
239254
}
240255

@@ -275,7 +290,8 @@ $fallbacks: m3-slide-toggle.get-tokens();
275290
left: 0;
276291
position: absolute;
277292
top: 0;
278-
transition: background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1),
293+
transition:
294+
background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1),
279295
border-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1);
280296
z-index: -1;
281297

@@ -435,39 +451,50 @@ $fallbacks: m3-slide-toggle.get-tokens();
435451
}
436452
}
437453

438-
.mdc-switch__icon {
439-
bottom: 0;
440-
left: 0;
441-
margin: auto;
454+
.mdc-switch__icons > [matUncheckedIcon],
455+
.mdc-switch__icons > [matCheckedIcon],
456+
.mdc-switch__icons > [matUncheckedDisabledIcon],
457+
.mdc-switch__icons > [matCheckedDisabledIcon] {
458+
margin: 0;
442459
position: absolute;
443-
right: 0;
444-
top: 0;
460+
top: 50%;
461+
right: 50%;
462+
transform: translate(50%, -50%);
445463
opacity: 0;
464+
font-size: 16px;
465+
display: block;
466+
446467
transition: opacity 30ms 0ms cubic-bezier(0.4, 0, 1, 1);
447468

448469
.mdc-switch--unselected & {
449470
width: token-utils.slot(slide-toggle-unselected-icon-size, $fallbacks);
450471
height: token-utils.slot(slide-toggle-unselected-icon-size, $fallbacks);
451472
fill: token-utils.slot(slide-toggle-unselected-icon-color, $fallbacks);
473+
color: token-utils.slot(slide-toggle-unselected-icon-color, $fallbacks);
452474
}
453475

454476
.mdc-switch--unselected.mdc-switch--disabled & {
455477
fill: token-utils.slot(slide-toggle-disabled-unselected-icon-color, $fallbacks);
478+
color: token-utils.slot(slide-toggle-disabled-unselected-icon-color, $fallbacks);
456479
}
457480

458481
.mdc-switch--selected & {
459482
width: token-utils.slot(slide-toggle-selected-icon-size, $fallbacks);
460483
height: token-utils.slot(slide-toggle-selected-icon-size, $fallbacks);
461484
fill: token-utils.slot(slide-toggle-selected-icon-color, $fallbacks);
485+
color: token-utils.slot(slide-toggle-selected-icon-color, $fallbacks);
462486
}
463487

464488
.mdc-switch--selected.mdc-switch--disabled & {
465489
fill: token-utils.slot(slide-toggle-disabled-selected-icon-color, $fallbacks);
490+
color: token-utils.slot(slide-toggle-disabled-selected-icon-color, $fallbacks);
466491
}
467492
}
468493

469-
.mdc-switch--selected .mdc-switch__icon--on,
470-
.mdc-switch--unselected .mdc-switch__icon--off {
494+
.mdc-switch--selected .mdc-switch__icons [matCheckedIcon],
495+
.mdc-switch--unselected .mdc-switch__icons [matUncheckedIcon],
496+
.mdc-switch--disabled.mdc-switch--selected .mdc-switch__icons [matCheckedDisabledIcon],
497+
.mdc-switch--disabled.mdc-switch--unselected .mdc-switch__icons [matUncheckedDisabledIcon] {
471498
opacity: 1;
472499
transition: opacity 45ms 30ms cubic-bezier(0, 0, 0.2, 1);
473500
}

0 commit comments

Comments
 (0)