diff --git a/src/dev-app/slide-toggle/slide-toggle-demo.html b/src/dev-app/slide-toggle/slide-toggle-demo.html
index 7c63ab9c059b..b9e67a0bae61 100644
--- a/src/dev-app/slide-toggle/slide-toggle-demo.html
+++ b/src/dev-app/slide-toggle/slide-toggle-demo.html
@@ -2,6 +2,7 @@
Default Slide Toggle
Disabled Slide Toggle
Disable Bound
+ Disabled Interactive Toggle
No icon
With label before the slide toggle.
diff --git a/src/dev-app/slide-toggle/slide-toggle-demo.ts b/src/dev-app/slide-toggle/slide-toggle-demo.ts
index a09f11e02d78..321f543085ef 100644
--- a/src/dev-app/slide-toggle/slide-toggle-demo.ts
+++ b/src/dev-app/slide-toggle/slide-toggle-demo.ts
@@ -20,8 +20,8 @@ import {MatSlideToggleModule} from '@angular/material/slide-toggle';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SlideToggleDemo {
- firstToggle: boolean = false;
- formToggle: boolean = false;
+ firstToggle = false;
+ formToggle = false;
onFormSubmit() {
alert(`You submitted the form. Value: ${this.formToggle}.`);
diff --git a/src/material/slide-toggle/slide-toggle-config.ts b/src/material/slide-toggle/slide-toggle-config.ts
index 4b1936e834b9..f2150a3d32a4 100644
--- a/src/material/slide-toggle/slide-toggle-config.ts
+++ b/src/material/slide-toggle/slide-toggle-config.ts
@@ -24,6 +24,9 @@ export interface MatSlideToggleDefaultOptions {
/** Whether to hide the icon inside the slide toggle. */
hideIcon?: boolean;
+
+ /** Whether disabled slide toggles should remain interactive. */
+ disabledInteractive?: boolean;
}
/** Injection token to be used to override the default options for `mat-slide-toggle`. */
@@ -31,6 +34,6 @@ export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS = new InjectionToken ({disableToggleValue: false, hideIcon: false}),
+ factory: () => ({disableToggleValue: false, hideIcon: false, disabledInteractive: false}),
},
);
diff --git a/src/material/slide-toggle/slide-toggle.html b/src/material/slide-toggle/slide-toggle.html
index 969598d17454..a48e753e1138 100644
--- a/src/material/slide-toggle/slide-toggle.html
+++ b/src/material/slide-toggle/slide-toggle.html
@@ -7,8 +7,9 @@
[class.mdc-switch--unselected]="!checked"
[class.mdc-switch--checked]="checked"
[class.mdc-switch--disabled]="disabled"
- [tabIndex]="disabled ? -1 : tabIndex"
- [disabled]="disabled"
+ [class.mat-mdc-slide-toggle-disabled-interactive]="disabledInteractive"
+ [tabIndex]="disabled && !disabledInteractive ? -1 : tabIndex"
+ [disabled]="disabled && !disabledInteractive"
[attr.id]="buttonId"
[attr.name]="name"
[attr.aria-label]="ariaLabel"
@@ -16,6 +17,7 @@
[attr.aria-describedby]="ariaDescribedby"
[attr.aria-required]="required || null"
[attr.aria-checked]="checked"
+ [attr.aria-disabled]="disabledInteractive && disabledInteractive ? 'true' : null"
(click)="_handleClick()"
#switch>
diff --git a/src/material/slide-toggle/slide-toggle.scss b/src/material/slide-toggle/slide-toggle.scss
index b499bdb7cf09..2ee4027d2520 100644
--- a/src/material/slide-toggle/slide-toggle.scss
+++ b/src/material/slide-toggle/slide-toggle.scss
@@ -6,6 +6,7 @@
$_mdc-slots: (tokens-mdc-switch.$prefix, tokens-mdc-switch.get-token-slots());
$_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
+$_interactive-disabled-selector: '.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled';
.mdc-switch {
align-items: center;
@@ -20,11 +21,15 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
padding: 0;
position: relative;
- &:disabled {
+ &.mdc-switch--disabled {
cursor: default;
pointer-events: none;
}
+ &.mat-mdc-slide-toggle-disabled-interactive {
+ pointer-events: auto;
+ }
+
@include token-utils.use-tokens($_mdc-slots...) {
@include token-utils.create-token-slot(width, track-width);
}
@@ -39,7 +44,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(height, track-height);
@include token-utils.create-token-slot(border-radius, track-shape);
- .mdc-switch:disabled & {
+ .mdc-switch--disabled.mdc-switch & {
@include token-utils.create-token-slot(opacity, disabled-track-opacity);
}
}
@@ -117,7 +122,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(background, unselected-pressed-track-color);
}
- .mdc-switch:disabled & {
+ #{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
+ #{$_interactive-disabled-selector}:focus:not(:active) &,
+ #{$_interactive-disabled-selector}:active &,
+ .mdc-switch.mdc-switch--disabled & {
@include token-utils.create-token-slot(background, disabled-unselected-track-color);
}
}
@@ -161,7 +169,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(background, selected-pressed-track-color);
}
- .mdc-switch:disabled & {
+ #{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
+ #{$_interactive-disabled-selector}:focus:not(:active) &,
+ #{$_interactive-disabled-selector}:active &,
+ .mdc-switch.mdc-switch--disabled & {
@include token-utils.create-token-slot(background, disabled-selected-track-color);
}
}
@@ -310,7 +321,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(background, selected-pressed-handle-color);
}
- .mdc-switch--selected:disabled & {
+ #{$_interactive-disabled-selector}.mdc-switch--selected:hover:not(:focus):not(:active) &,
+ #{$_interactive-disabled-selector}.mdc-switch--selected:focus:not(:active) &,
+ #{$_interactive-disabled-selector}.mdc-switch--selected:active &,
+ .mdc-switch--selected.mdc-switch--disabled & {
@include token-utils.create-token-slot(background, disabled-selected-handle-color);
}
@@ -330,7 +344,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(background, unselected-pressed-handle-color);
}
- .mdc-switch--unselected:disabled & {
+ .mdc-switch--unselected.mdc-switch--disabled & {
@include token-utils.create-token-slot(background, disabled-unselected-handle-color);
}
}
@@ -354,7 +368,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(box-shadow, handle-elevation-shadow);
}
- .mdc-switch:disabled & {
+ #{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
+ #{$_interactive-disabled-selector}:focus:not(:active) &,
+ #{$_interactive-disabled-selector}:active &,
+ .mdc-switch.mdc-switch--disabled & {
@include token-utils.create-token-slot(box-shadow, disabled-handle-elevation-shadow);
}
}
@@ -376,10 +393,14 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
content: '';
opacity: 0;
- .mdc-switch:disabled & {
+ .mdc-switch--disabled & {
display: none;
}
+ .mat-mdc-slide-toggle-disabled-interactive & {
+ display: block;
+ }
+
.mdc-switch:hover & {
opacity: 0.04;
transition: 75ms opacity cubic-bezier(0, 0, 0.2, 1);
@@ -391,6 +412,9 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
}
@include token-utils.use-tokens($_mdc-slots...) {
+ #{$_interactive-disabled-selector}:enabled:focus &,
+ #{$_interactive-disabled-selector}:enabled:active &,
+ #{$_interactive-disabled-selector}:enabled:hover:not(:focus) &,
.mdc-switch--unselected:enabled:hover:not(:focus) & {
@include token-utils.create-token-slot(background, unselected-hover-state-layer-color);
}
@@ -429,11 +453,11 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
z-index: 1;
@include token-utils.use-tokens($_mdc-slots...) {
- .mdc-switch--unselected:disabled & {
+ .mdc-switch--disabled.mdc-switch--unselected & {
@include token-utils.create-token-slot(opacity, disabled-unselected-icon-opacity);
}
- .mdc-switch--selected:disabled & {
+ .mdc-switch--disabled.mdc-switch--selected & {
@include token-utils.create-token-slot(opacity, disabled-selected-icon-opacity);
}
}
@@ -456,7 +480,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(fill, unselected-icon-color);
}
- .mdc-switch--unselected:disabled & {
+ .mdc-switch--unselected.mdc-switch--disabled & {
@include token-utils.create-token-slot(fill, disabled-unselected-icon-color);
}
@@ -466,7 +490,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
@include token-utils.create-token-slot(fill, selected-icon-color);
}
- .mdc-switch--selected:disabled & {
+ .mdc-switch--selected.mdc-switch--disabled & {
@include token-utils.create-token-slot(fill, disabled-selected-icon-color);
}
}
diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts
index 546d6e656934..1cf3b83a6973 100644
--- a/src/material/slide-toggle/slide-toggle.spec.ts
+++ b/src/material/slide-toggle/slide-toggle.spec.ts
@@ -379,6 +379,40 @@ describe('MDC-based MatSlideToggle without forms', () => {
expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
}));
+
+ it('should be able to mark a slide toggle as interactive while it is disabled', fakeAsync(() => {
+ testComponent.isDisabled = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(buttonElement.disabled).toBe(true);
+ expect(buttonElement.hasAttribute('aria-disabled')).toBe(false);
+ expect(buttonElement.getAttribute('tabindex')).toBe('-1');
+ expect(buttonElement.classList).not.toContain('mat-mdc-slide-toggle-disabled-interactive');
+
+ testComponent.disabledInteractive = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ expect(buttonElement.disabled).toBe(false);
+ expect(buttonElement.getAttribute('aria-disabled')).toBe('true');
+ expect(buttonElement.getAttribute('tabindex')).toBe('0');
+ expect(buttonElement.classList).toContain('mat-mdc-slide-toggle-disabled-interactive');
+ }));
+
+ it('should not change its state when clicked while disabled and interactive', fakeAsync(() => {
+ expect(slideToggle.checked).toBe(false);
+
+ testComponent.isDisabled = testComponent.disabledInteractive = true;
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+
+ buttonElement.click();
+ fixture.detectChanges();
+ tick();
+
+ expect(slideToggle.checked).toBe(false);
+ }));
});
describe('custom template', () => {
@@ -828,33 +862,36 @@ describe('MDC-based MatSlideToggle with forms', () => {
@Component({
template: `
-
+
Test Slide Toggle
`,
standalone: true,
imports: [MatSlideToggleModule, BidiModule],
})
class SlideToggleBasic {
- isDisabled: boolean = false;
- isRequired: boolean = false;
- disableRipple: boolean = false;
- slideChecked: boolean = false;
+ isDisabled = false;
+ isRequired = false;
+ disableRipple = false;
+ slideChecked = false;
slideColor: string;
slideId: string | null;
slideName: string | null;
@@ -864,10 +901,11 @@ class SlideToggleBasic {
slideTabindex: number;
lastEvent: MatSlideToggleChange;
labelPosition: string;
- toggleTriggered: number = 0;
- dragTriggered: number = 0;
+ toggleTriggered = 0;
+ dragTriggered = 0;
direction: Direction = 'ltr';
hideIcon = false;
+ disabledInteractive = false;
onSlideClick: (event?: Event) => void = () => {};
onSlideChange = (event: MatSlideToggleChange) => (this.lastEvent = event);
diff --git a/src/material/slide-toggle/slide-toggle.ts b/src/material/slide-toggle/slide-toggle.ts
index 2e4501a8b3b6..28837bcf85c9 100644
--- a/src/material/slide-toggle/slide-toggle.ts
+++ b/src/material/slide-toggle/slide-toggle.ts
@@ -187,6 +187,9 @@ export class MatSlideToggle
/** Whether to hide the icon inside of the slide toggle. */
@Input({transform: booleanAttribute}) hideIcon: boolean;
+ /** Whether the slide toggle should remain interactive when it is disabled. */
+ @Input({transform: booleanAttribute}) disabledInteractive: boolean;
+
/** An event will be dispatched each time the slide-toggle changes its value. */
@Output() readonly change = new EventEmitter();
@@ -215,6 +218,7 @@ export class MatSlideToggle
this._noopAnimations = animationMode === 'NoopAnimations';
this.id = this._uniqueId = `mat-mdc-slide-toggle-${++nextUniqueId}`;
this.hideIcon = defaults.hideIcon ?? false;
+ this.disabledInteractive = defaults.disabledInteractive ?? false;
this._labelId = this._uniqueId + '-label';
}
@@ -295,12 +299,14 @@ export class MatSlideToggle
/** Method being called whenever the underlying button is clicked. */
_handleClick() {
- this.toggleChange.emit();
+ if (!this.disabled) {
+ this.toggleChange.emit();
- if (!this.defaults.disableToggleValue) {
- this.checked = !this.checked;
- this._onChange(this.checked);
- this.change.emit(new MatSlideToggleChange(this, this.checked));
+ if (!this.defaults.disableToggleValue) {
+ this.checked = !this.checked;
+ this._onChange(this.checked);
+ this.change.emit(new MatSlideToggleChange(this, this.checked));
+ }
}
}
diff --git a/tools/public_api_guard/material/slide-toggle.md b/tools/public_api_guard/material/slide-toggle.md
index faf590458716..1e14c8840e4a 100644
--- a/tools/public_api_guard/material/slide-toggle.md
+++ b/tools/public_api_guard/material/slide-toggle.md
@@ -53,6 +53,7 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
// (undocumented)
defaults: MatSlideToggleDefaultOptions;
disabled: boolean;
+ disabledInteractive: boolean;
disableRipple: boolean;
protected _emitChangeEvent(): void;
focus(): void;
@@ -73,6 +74,8 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
// (undocumented)
static ngAcceptInputType_disabled: unknown;
// (undocumented)
+ static ngAcceptInputType_disabledInteractive: unknown;
+ // (undocumented)
static ngAcceptInputType_disableRipple: unknown;
// (undocumented)
static ngAcceptInputType_hideIcon: unknown;
@@ -99,7 +102,7 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
validate(control: AbstractControl): ValidationErrors | null;
writeValue(value: any): void;
// (undocumented)
- static ɵcmp: i0.ɵɵComponentDeclaration;
+ static ɵcmp: i0.ɵɵComponentDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
@@ -116,6 +119,7 @@ export class MatSlideToggleChange {
// @public
export interface MatSlideToggleDefaultOptions {
color?: ThemePalette;
+ disabledInteractive?: boolean;
disableToggleValue?: boolean;
hideIcon?: boolean;
}