Skip to content

feat(material/slide-toggle): enabling custom icons through directives #31758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dev-app/slide-toggle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ng_project(
"//:node_modules/@angular/core",
"//:node_modules/@angular/forms",
"//src/material/button",
"//src/material/icon",
"//src/material/slide-toggle",
],
)
Expand Down
67 changes: 61 additions & 6 deletions src/dev-app/slide-toggle/slide-toggle-demo.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,71 @@
<div class="demo-slide-toggle">
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle">Default Slide Toggle</mat-slide-toggle>
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle"
>Default Slide Toggle</mat-slide-toggle
>
<mat-slide-toggle [(ngModel)]="firstToggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
<mat-slide-toggle [disabled]="firstToggle">Disable Bound</mat-slide-toggle>
<mat-slide-toggle disabled disabledInteractive [(ngModel)]="firstToggle">Disabled Interactive Toggle</mat-slide-toggle>
<mat-slide-toggle disabled disabledInteractive [(ngModel)]="firstToggle"
>Disabled Interactive Toggle</mat-slide-toggle
>
<mat-slide-toggle hideIcon="unchecked" [(ngModel)]="firstToggle"
>No icon (While Unchecked)</mat-slide-toggle
>
<mat-slide-toggle hideIcon="checked" [(ngModel)]="firstToggle"
>No icon (While Checked)</mat-slide-toggle
>
<mat-slide-toggle hideIcon [(ngModel)]="firstToggle">No icon</mat-slide-toggle>

<mat-slide-toggle [(ngModel)]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
Custom Icons
</mat-slide-toggle>
<mat-slide-toggle disabled [(ngModel)]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
Disabled Custom Icons
</mat-slide-toggle>
<mat-slide-toggle [disabled]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
<mat-icon matCheckedDisabledIcon>lock</mat-icon>
<mat-icon matUncheckedDisabledIcon>lock_open</mat-icon>
Disabled Custom Disabled Icons
</mat-slide-toggle>
<p>With label before the slide toggle.</p>

<mat-slide-toggle labelPosition="before" color="primary" [(ngModel)]="firstToggle">Default Slide Toggle</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" color="primary" [(ngModel)]="firstToggle"
>Default Slide Toggle</mat-slide-toggle
>
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle" disabled
>Disabled Slide Toggle</mat-slide-toggle
>
<mat-slide-toggle labelPosition="before" [disabled]="firstToggle">Disable Bound</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" hideIcon [(ngModel)]="firstToggle">No icon</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" hideIcon="unchecked" [(ngModel)]="firstToggle"
>No icon (While Unchecked)</mat-slide-toggle
>
<mat-slide-toggle labelPosition="before" hideIcon="checked" [(ngModel)]="firstToggle"
>No icon (While Checked)</mat-slide-toggle
>
<mat-slide-toggle labelPosition="before" hideIcon [(ngModel)]="firstToggle"
>No icon</mat-slide-toggle
>
<mat-slide-toggle labelPosition="before" [(ngModel)]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
Custom Icons
</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" disabled [(ngModel)]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
Disabled Custom Icons
</mat-slide-toggle>
<mat-slide-toggle labelPosition="before" [disabled]="firstToggle">
<mat-icon matCheckedIcon>light_mode</mat-icon>
<mat-icon matUncheckedIcon>dark_mode</mat-icon>
<mat-icon matCheckedDisabledIcon>lock</mat-icon>
<mat-icon matUncheckedDisabledIcon>lock_open</mat-icon>
Disabled Custom Disabled Icons
</mat-slide-toggle>

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

Expand Down
3 changes: 2 additions & 1 deletion src/dev-app/slide-toggle/slide-toggle-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatIconModule} from '@angular/material/icon';

@Component({
selector: 'slide-toggle-demo',
templateUrl: 'slide-toggle-demo.html',
styleUrl: 'slide-toggle-demo.css',
imports: [FormsModule, MatButtonModule, MatSlideToggleModule],
imports: [FormsModule, MatButtonModule, MatSlideToggleModule, MatIconModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SlideToggleDemo {
Expand Down
4 changes: 2 additions & 2 deletions src/material/slide-toggle/slide-toggle-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface MatSlideToggleDefaultOptions {
color?: ThemePalette;

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

/** Whether disabled slide toggles should remain interactive. */
disabledInteractive?: boolean;
Expand All @@ -34,6 +34,6 @@ export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatSlideToggl
'mat-slide-toggle-default-options',
{
providedIn: 'root',
factory: () => ({disableToggleValue: false, hideIcon: false, disabledInteractive: false}),
factory: () => ({disableToggleValue: false, hideIcon: 'none', disabledInteractive: false}),
},
);
71 changes: 52 additions & 19 deletions src/material/slide-toggle/slide-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
[attr.aria-checked]="checked"
[attr.aria-disabled]="disabled && disabledInteractive ? 'true' : null"
(click)="_handleClick()"
#switch>
#switch
>
<div class="mat-mdc-slide-toggle-touch-target"></div>
<span class="mdc-switch__track"></span>
<span class="mdc-switch__handle-track">
Expand All @@ -28,26 +29,21 @@
<span class="mdc-elevation-overlay"></span>
</span>
<span class="mdc-switch__ripple">
<span class="mat-mdc-slide-toggle-ripple mat-focus-indicator" mat-ripple
<span
class="mat-mdc-slide-toggle-ripple mat-focus-indicator"
mat-ripple
[matRippleTrigger]="switch"
[matRippleDisabled]="disableRipple || disabled"
[matRippleCentered]="true"></span>
[matRippleCentered]="true"
></span>
</span>
@if (!hideIcon) {
<span class="mdc-switch__icons">
<svg
class="mdc-switch__icon mdc-switch__icon--on"
viewBox="0 0 24 24"
aria-hidden="true">
<path d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z" />
</svg>
<svg
class="mdc-switch__icon mdc-switch__icon--off"
viewBox="0 0 24 24"
aria-hidden="true">
<path d="M20 13H4v-2h16v2z" />
</svg>
</span>

@if (hideIcon !== "both") {
<ng-container
[ngTemplateOutlet]="checked ?
(hideIcon === 'checked' ? null : onIconsTemplate) :
(hideIcon === 'unchecked' ? null : offIconsTemplate)"
></ng-container>
}
</span>
</span>
Expand All @@ -56,8 +52,45 @@
<!--
Clicking on the label will trigger another click event from the button.
Stop propagation here so other listeners further up in the DOM don't execute twice.
-->
-->
<label class="mdc-label" [for]="buttonId" [attr.id]="_labelId" (click)="$event.stopPropagation()">
<ng-content></ng-content>
</label>
</div>

<ng-template #onDefaultTemplate>
<ng-content select="[matCheckedIcon]">
<svg matCheckedIcon viewBox="0 0 24 24" aria-hidden="true">
<path d="M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z" />
</svg>
</ng-content>
</ng-template>
<ng-template #onIconsTemplate>
<span class="mdc-switch__icons">
@if (disabled) {
<ng-content select="[matCheckedDisabledIcon]">
<ng-container *ngTemplateOutlet="onDefaultTemplate"></ng-container>
</ng-content>
} @else {
<ng-container *ngTemplateOutlet="onDefaultTemplate"></ng-container>
}
</span>
</ng-template>
<ng-template #offDefaultTemplate>
<ng-content select="[matUncheckedIcon]">
<svg matUncheckedIcon viewBox="0 0 24 24" aria-hidden="true">
<path d="M4,13h16v-2H4v2z" />
</svg>
</ng-content>
</ng-template>
<ng-template #offIconsTemplate>
<span class="mdc-switch__icons">
@if (disabled) {
<ng-content select="[matUncheckedDisabledIcon]">
<ng-container *ngTemplateOutlet="offDefaultTemplate"></ng-container>
</ng-content>
} @else {
<ng-container *ngTemplateOutlet="offDefaultTemplate"></ng-container>
}
</span>
</ng-template>
53 changes: 40 additions & 13 deletions src/material/slide-toggle/slide-toggle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ $_interactive-disabled-selector: '.mat-mdc-slide-toggle-disabled-interactive.mdc

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

[matCheckedIcon],
[matUncheckedIcon],
[matCheckedDisabledIcon],
[matUncheckedDisabledIcon] {
display: none;
}

.mdc-switch {
align-items: center;
background: none;
Expand Down Expand Up @@ -66,9 +73,13 @@ $fallbacks: m3-slide-toggle.get-tokens();

.mdc-switch--disabled & {
border-width: token-utils.slot(
slide-toggle-disabled-unselected-track-outline-width, $fallbacks);
slide-toggle-disabled-unselected-track-outline-width,
$fallbacks
);
border-color: token-utils.slot(
slide-toggle-disabled-unselected-track-outline-color, $fallbacks);
slide-toggle-disabled-unselected-track-outline-color,
$fallbacks
);
}
}

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

&:has(.mdc-switch__icons) {
margin: token-utils.slot(
slide-toggle-unselected-with-icon-handle-horizontal-margin, $fallbacks);
slide-toggle-unselected-with-icon-handle-horizontal-margin,
$fallbacks
);
}
}

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

&:has(.mdc-switch__icons) {
margin: token-utils.slot(
slide-toggle-selected-with-icon-handle-horizontal-margin, $fallbacks);
slide-toggle-selected-with-icon-handle-horizontal-margin,
$fallbacks
);
}
}

Expand Down Expand Up @@ -275,7 +290,8 @@ $fallbacks: m3-slide-toggle.get-tokens();
left: 0;
position: absolute;
top: 0;
transition: background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1),
transition:
background-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1),
border-color 75ms 0ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: -1;

Expand Down Expand Up @@ -435,39 +451,50 @@ $fallbacks: m3-slide-toggle.get-tokens();
}
}

.mdc-switch__icon {
bottom: 0;
left: 0;
margin: auto;
.mdc-switch__icons > [matUncheckedIcon],
.mdc-switch__icons > [matCheckedIcon],
.mdc-switch__icons > [matUncheckedDisabledIcon],
.mdc-switch__icons > [matCheckedDisabledIcon] {
margin: 0;
position: absolute;
right: 0;
top: 0;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
opacity: 0;
font-size: 16px;
display: block;

transition: opacity 30ms 0ms cubic-bezier(0.4, 0, 1, 1);

.mdc-switch--unselected & {
width: token-utils.slot(slide-toggle-unselected-icon-size, $fallbacks);
height: token-utils.slot(slide-toggle-unselected-icon-size, $fallbacks);
fill: token-utils.slot(slide-toggle-unselected-icon-color, $fallbacks);
color: token-utils.slot(slide-toggle-unselected-icon-color, $fallbacks);
}

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

.mdc-switch--selected & {
width: token-utils.slot(slide-toggle-selected-icon-size, $fallbacks);
height: token-utils.slot(slide-toggle-selected-icon-size, $fallbacks);
fill: token-utils.slot(slide-toggle-selected-icon-color, $fallbacks);
color: token-utils.slot(slide-toggle-selected-icon-color, $fallbacks);
}

.mdc-switch--selected.mdc-switch--disabled & {
fill: token-utils.slot(slide-toggle-disabled-selected-icon-color, $fallbacks);
color: token-utils.slot(slide-toggle-disabled-selected-icon-color, $fallbacks);
}
}

.mdc-switch--selected .mdc-switch__icon--on,
.mdc-switch--unselected .mdc-switch__icon--off {
.mdc-switch--selected .mdc-switch__icons [matCheckedIcon],
.mdc-switch--unselected .mdc-switch__icons [matUncheckedIcon],
.mdc-switch--disabled.mdc-switch--selected .mdc-switch__icons [matCheckedDisabledIcon],
.mdc-switch--disabled.mdc-switch--unselected .mdc-switch__icons [matUncheckedDisabledIcon] {
opacity: 1;
transition: opacity 45ms 30ms cubic-bezier(0, 0, 0.2, 1);
}
Expand Down
Loading