diff --git a/src/dev-app/slide-toggle/BUILD.bazel b/src/dev-app/slide-toggle/BUILD.bazel
index dc184702b6bb..7adb91bf3ed7 100644
--- a/src/dev-app/slide-toggle/BUILD.bazel
+++ b/src/dev-app/slide-toggle/BUILD.bazel
@@ -13,6 +13,7 @@ ng_project(
"//:node_modules/@angular/core",
"//:node_modules/@angular/forms",
"//src/material/button",
+ "//src/material/icon",
"//src/material/slide-toggle",
],
)
diff --git a/src/dev-app/slide-toggle/slide-toggle-demo.html b/src/dev-app/slide-toggle/slide-toggle-demo.html
index 59b711ac3e9f..d1c86e9a1395 100644
--- a/src/dev-app/slide-toggle/slide-toggle-demo.html
+++ b/src/dev-app/slide-toggle/slide-toggle-demo.html
@@ -1,16 +1,71 @@
-
Default Slide Toggle
+
Default Slide Toggle
Disabled Slide Toggle
Disable Bound
-
Disabled Interactive Toggle
+
Disabled Interactive Toggle
+
No icon (While Unchecked)
+
No icon (While Checked)
No icon
-
+
+ light_mode
+ dark_mode
+ Custom Icons
+
+
+ light_mode
+ dark_mode
+ Disabled Custom Icons
+
+
+ light_mode
+ dark_mode
+ lock
+ lock_open
+ Disabled Custom Disabled Icons
+
With label before the slide toggle.
-
Default Slide Toggle
-
Disabled Slide Toggle
+
Default Slide Toggle
+
Disabled Slide Toggle
Disable Bound
-
No icon
+
No icon (While Unchecked)
+
No icon (While Checked)
+
No icon
+
+ light_mode
+ dark_mode
+ Custom Icons
+
+
+ light_mode
+ dark_mode
+ Disabled Custom Icons
+
+
+ light_mode
+ dark_mode
+ lock
+ lock_open
+ Disabled Custom Disabled Icons
+
Example where the slide toggle is required inside of a form.
diff --git a/src/dev-app/slide-toggle/slide-toggle-demo.ts b/src/dev-app/slide-toggle/slide-toggle-demo.ts
index 068d26a30e18..f2f6d889eef0 100644
--- a/src/dev-app/slide-toggle/slide-toggle-demo.ts
+++ b/src/dev-app/slide-toggle/slide-toggle-demo.ts
@@ -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 {
diff --git a/src/material/slide-toggle/slide-toggle-config.ts b/src/material/slide-toggle/slide-toggle-config.ts
index e78afce93700..fadd76d882a3 100644
--- a/src/material/slide-toggle/slide-toggle-config.ts
+++ b/src/material/slide-toggle/slide-toggle-config.ts
@@ -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;
@@ -34,6 +34,6 @@ export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS = new InjectionToken
({disableToggleValue: false, hideIcon: false, disabledInteractive: false}),
+ factory: () => ({disableToggleValue: false, hideIcon: 'none', disabledInteractive: false}),
},
);
diff --git a/src/material/slide-toggle/slide-toggle.html b/src/material/slide-toggle/slide-toggle.html
index 6e1c74ce2ad4..8af522db005e 100644
--- a/src/material/slide-toggle/slide-toggle.html
+++ b/src/material/slide-toggle/slide-toggle.html
@@ -19,7 +19,8 @@
[attr.aria-checked]="checked"
[attr.aria-disabled]="disabled && disabledInteractive ? 'true' : null"
(click)="_handleClick()"
- #switch>
+ #switch
+ >
@@ -28,26 +29,21 @@
-
+ [matRippleCentered]="true"
+ >
- @if (!hideIcon) {
-
-
-
-
+
+ @if (hideIcon !== "both") {
+
}
@@ -56,8 +52,45 @@
+ -->
+
+
+
+
+
+
+
+
+ @if (disabled) {
+
+
+
+ } @else {
+
+ }
+
+
+
+
+
+
+
+
+
+ @if (disabled) {
+
+
+
+ } @else {
+
+ }
+
+
diff --git a/src/material/slide-toggle/slide-toggle.scss b/src/material/slide-toggle/slide-toggle.scss
index babf986b0f8d..edf5a57de447 100644
--- a/src/material/slide-toggle/slide-toggle.scss
+++ b/src/material/slide-toggle/slide-toggle.scss
@@ -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;
@@ -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
+ );
}
}
@@ -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
+ );
}
}
@@ -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
+ );
}
}
@@ -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;
@@ -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);
}
diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts
index 7ef13fc56555..86a15edc65b9 100644
--- a/src/material/slide-toggle/slide-toggle.spec.ts
+++ b/src/material/slide-toggle/slide-toggle.spec.ts
@@ -26,6 +26,7 @@ describe('MatSlideToggle without forms', () => {
SlideToggleWithTabindexAttr,
SlideToggleWithoutLabel,
SlideToggleProjectedLabel,
+ SlideToggleProjectedLabelAndCustomIcons,
TextBindingComponent,
SlideToggleWithStaticAriaAttributes,
],
@@ -366,7 +367,7 @@ describe('MatSlideToggle without forms', () => {
expect(rippleElement.classList).toContain('mat-focus-indicator');
}));
- it('should be able to hide the icon', fakeAsync(() => {
+ it('should be able to hide both icons through true, "", and "both"', fakeAsync(() => {
expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeTruthy();
testComponent.hideIcon = true;
@@ -374,6 +375,51 @@ describe('MatSlideToggle without forms', () => {
fixture.detectChanges();
expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
+
+ testComponent.hideIcon = '';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
+
+ testComponent.hideIcon = 'both';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
+ }));
+
+ it('should be able to hide the on icon through "checked"', fakeAsync(() => {
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeTruthy();
+
+ testComponent.hideIcon = 'checked';
+ testComponent.slideChecked = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
+
+ testComponent.slideChecked = false;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeTruthy();
+ }));
+
+ it('should be able to hide the off icon through "unchecked"', fakeAsync(() => {
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeTruthy();
+
+ testComponent.hideIcon = 'unchecked';
+ testComponent.slideChecked = false;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
+
+ testComponent.slideChecked = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeTruthy();
}));
it('should be able to mark a slide toggle as interactive while it is disabled', fakeAsync(() => {
@@ -536,6 +582,86 @@ describe('MatSlideToggle without forms', () => {
expect(host.hasAttribute('aria-label')).toBe(false);
expect(host.hasAttribute('aria-labelledby')).toBe(false);
});
+
+ it('should render default icons if no custom icons are present', () => {
+ const fixture = TestBed.createComponent(SlideToggleBasic);
+ const slideToggleDebug = fixture.debugElement.query(By.directive(MatSlideToggle))!;
+ const slideToggle = slideToggleDebug.componentInstance;
+ const slideToggleElement = slideToggleDebug.nativeElement;
+ fixture.detectChanges();
+
+ const offIconElement = slideToggleElement
+ .querySelector('.mdc-switch__icons')
+ ?.querySelector(`[matUncheckedIcon]`);
+ expect(offIconElement).toBeTruthy();
+
+ slideToggle.checked = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ const onIconElement = slideToggleElement
+ .querySelector('.mdc-switch__icons')
+ ?.querySelector(`[matCheckedIcon]`);
+ expect(onIconElement).toBeTruthy();
+ });
+
+ it('should render custom icons if hideIcon is not present', fakeAsync(() => {
+ const fixture = TestBed.createComponent(SlideToggleProjectedLabelAndCustomIcons);
+ const slideToggleDebug = fixture.debugElement.query(By.directive(MatSlideToggle))!;
+ const slideToggle = slideToggleDebug.componentInstance;
+ const slideToggleElement = slideToggleDebug.nativeElement;
+ slideToggle.hideIcon = 'both';
+ slideToggle.checked = true;
+ slideToggle.disabled = false;
+ fixture.detectChanges();
+
+ const icons = {
+ '[matCheckedIcon]': {
+ checked: true,
+ disabled: false,
+ text: 'light_mode',
+ hide: ['checked', 'both'],
+ },
+ '[matUncheckedIcon]': {
+ checked: false,
+ disabled: false,
+ text: 'dark_mode',
+ hide: ['unchecked', 'both'],
+ },
+ '[matCheckedDisabledIcon]': {
+ checked: true,
+ disabled: true,
+ text: 'lock',
+ hide: ['checked', 'both'],
+ },
+ '[matUncheckedDisabledIcon]': {
+ checked: false,
+ disabled: true,
+ text: 'lock_open',
+ hide: ['unchecked', 'both'],
+ },
+ };
+ for (const hide of ['checked', 'unchecked', 'both', 'none']) {
+ slideToggle.hideIcon = hide;
+ for (const [icon, state] of Object.entries(icons)) {
+ slideToggle.checked = state.checked;
+ slideToggle.disabled = state.disabled;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ const iconElement = slideToggleElement.querySelector(`${icon}`);
+ const otherIcons = Object.keys(icons)
+ .filter(key => key !== icon)
+ .map(key => slideToggleElement.querySelector(`${key}`));
+ state.hide.includes(hide)
+ ? expect(iconElement).toBeFalsy()
+ : expect(iconElement).toBeTruthy();
+ if (iconElement) {
+ expect(iconElement.textContent?.trim()).toBe(state.text);
+ }
+ otherIcons.forEach(otherIcon => expect(otherIcon).toBeFalsy());
+ }
+ }
+ }));
});
describe('MatSlideToggle with forms', () => {
@@ -900,7 +1026,7 @@ class SlideToggleBasic {
toggleTriggered = 0;
dragTriggered = 0;
direction: Direction = 'ltr';
- hideIcon = false;
+ hideIcon: '' | 'both' | 'checked' | 'unchecked' | 'none' | boolean = 'none';
disabledInteractive = false;
onSlideClick: (event?: Event) => void = () => {};
@@ -990,6 +1116,15 @@ class TextBindingComponent {
text: string = 'Some text';
}
+@Component({
+ template: `light_mode
+ dark_mode
+ lock
+ lock_open
+ `,
+ imports: [MatSlideToggleModule, BidiModule],
+})
+class SlideToggleProjectedLabelAndCustomIcons {}
@Component({
template: `
diff --git a/src/material/slide-toggle/slide-toggle.ts b/src/material/slide-toggle/slide-toggle.ts
index 2b0df39012c4..9ea368892132 100644
--- a/src/material/slide-toggle/slide-toggle.ts
+++ b/src/material/slide-toggle/slide-toggle.ts
@@ -46,6 +46,7 @@ import {
MatRipple,
} from '../core';
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
+import {NgTemplateOutlet} from '@angular/common';
/** Change event object emitted by a slide toggle. */
export class MatSlideToggleChange {
@@ -89,7 +90,7 @@ export class MatSlideToggleChange {
multi: true,
},
],
- imports: [MatRipple, _MatInternalFormField],
+ imports: [MatRipple, _MatInternalFormField, NgTemplateOutlet],
})
export class MatSlideToggle
implements OnDestroy, AfterContentInit, OnChanges, ControlValueAccessor, Validator
@@ -184,7 +185,7 @@ export class MatSlideToggle
}
/** Whether to hide the icon inside of the slide toggle. */
- @Input({transform: booleanAttribute}) hideIcon: boolean;
+ @Input({transform: _defaultBoth}) hideIcon: 'both' | 'checked' | 'unchecked' | 'none' = 'none';
/** Whether the slide toggle should remain interactive when it is disabled. */
@Input({transform: booleanAttribute}) disabledInteractive: boolean;
@@ -214,7 +215,8 @@ export class MatSlideToggle
this.tabIndex = tabIndex == null ? 0 : parseInt(tabIndex) || 0;
this.color = defaults.color || 'accent';
this.id = this._uniqueId = inject(_IdGenerator).getId('mat-mdc-slide-toggle-');
- this.hideIcon = defaults.hideIcon ?? false;
+
+ this.hideIcon = defaults.hideIcon ?? 'none';
this.disabledInteractive = defaults.disabledInteractive ?? false;
this._labelId = this._uniqueId + '-label';
}
@@ -317,3 +319,14 @@ export class MatSlideToggle
return this.ariaLabel ? null : this._labelId;
}
}
+function _defaultBoth(
+ value: 'both' | 'checked' | 'unchecked' | '' | boolean | undefined,
+): 'both' | 'checked' | 'unchecked' | 'none' {
+ if (value === '' || value === true) {
+ return 'both';
+ }
+ if (value === undefined || value === false) {
+ return 'none';
+ }
+ return value as 'checked' | 'unchecked' | 'none';
+}
diff --git a/src/universal-app/kitchen-sink/kitchen-sink.html b/src/universal-app/kitchen-sink/kitchen-sink.html
index 7b97a7860eca..61a42997d9a4 100644
--- a/src/universal-app/kitchen-sink/kitchen-sink.html
+++ b/src/universal-app/kitchen-sink/kitchen-sink.html
@@ -113,9 +113,9 @@ Disabled datepicker
Timepicker
Pick a time
-
-
-
+
+
+
Dialog
@@ -292,6 +292,16 @@ Slide-toggle
with a label
+
+ light_mode
+ dark_mode
+ with a custom icon
+
+ lock
+ lock_open
+ with a custom disabled icon
Slider