Skip to content

Commit 4292e1b

Browse files
authored
feat(material/slide-toggle): add the ability to interact with disabled toggle (#29502)
Adds the `disabledInteractive` input which allows users to interact with slide toggles that are disabled.
1 parent c9078d1 commit 4292e1b

File tree

8 files changed

+124
-46
lines changed

8 files changed

+124
-46
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<mat-slide-toggle color="primary" [(ngModel)]="firstToggle">Default Slide Toggle</mat-slide-toggle>
33
<mat-slide-toggle [(ngModel)]="firstToggle" disabled>Disabled Slide Toggle</mat-slide-toggle>
44
<mat-slide-toggle [disabled]="firstToggle">Disable Bound</mat-slide-toggle>
5+
<mat-slide-toggle disabled disabledInteractive [(ngModel)]="firstToggle">Disabled Interactive Toggle</mat-slide-toggle>
56
<mat-slide-toggle hideIcon [(ngModel)]="firstToggle">No icon</mat-slide-toggle>
67

78
<p>With label before the slide toggle.</p>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import {MatSlideToggleModule} from '@angular/material/slide-toggle';
2020
changeDetection: ChangeDetectionStrategy.OnPush,
2121
})
2222
export class SlideToggleDemo {
23-
firstToggle: boolean = false;
24-
formToggle: boolean = false;
23+
firstToggle = false;
24+
formToggle = false;
2525

2626
onFormSubmit() {
2727
alert(`You submitted the form. Value: ${this.formToggle}.`);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ export interface MatSlideToggleDefaultOptions {
2424

2525
/** Whether to hide the icon inside the slide toggle. */
2626
hideIcon?: boolean;
27+
28+
/** Whether disabled slide toggles should remain interactive. */
29+
disabledInteractive?: boolean;
2730
}
2831

2932
/** Injection token to be used to override the default options for `mat-slide-toggle`. */
3033
export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatSlideToggleDefaultOptions>(
3134
'mat-slide-toggle-default-options',
3235
{
3336
providedIn: 'root',
34-
factory: () => ({disableToggleValue: false, hideIcon: false}),
37+
factory: () => ({disableToggleValue: false, hideIcon: false, disabledInteractive: false}),
3538
},
3639
);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
[class.mdc-switch--unselected]="!checked"
88
[class.mdc-switch--checked]="checked"
99
[class.mdc-switch--disabled]="disabled"
10-
[tabIndex]="disabled ? -1 : tabIndex"
11-
[disabled]="disabled"
10+
[class.mat-mdc-slide-toggle-disabled-interactive]="disabledInteractive"
11+
[tabIndex]="disabled && !disabledInteractive ? -1 : tabIndex"
12+
[disabled]="disabled && !disabledInteractive"
1213
[attr.id]="buttonId"
1314
[attr.name]="name"
1415
[attr.aria-label]="ariaLabel"
1516
[attr.aria-labelledby]="_getAriaLabelledBy()"
1617
[attr.aria-describedby]="ariaDescribedby"
1718
[attr.aria-required]="required || null"
1819
[attr.aria-checked]="checked"
20+
[attr.aria-disabled]="disabledInteractive && disabledInteractive ? 'true' : null"
1921
(click)="_handleClick()"
2022
#switch>
2123
<span class="mdc-switch__track"></span>

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

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
$_mdc-slots: (tokens-mdc-switch.$prefix, tokens-mdc-switch.get-token-slots());
88
$_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
9+
$_interactive-disabled-selector: '.mat-mdc-slide-toggle-disabled-interactive.mdc-switch--disabled';
910

1011
.mdc-switch {
1112
align-items: center;
@@ -20,11 +21,15 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
2021
padding: 0;
2122
position: relative;
2223

23-
&:disabled {
24+
&.mdc-switch--disabled {
2425
cursor: default;
2526
pointer-events: none;
2627
}
2728

29+
&.mat-mdc-slide-toggle-disabled-interactive {
30+
pointer-events: auto;
31+
}
32+
2833
@include token-utils.use-tokens($_mdc-slots...) {
2934
@include token-utils.create-token-slot(width, track-width);
3035
}
@@ -39,7 +44,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
3944
@include token-utils.create-token-slot(height, track-height);
4045
@include token-utils.create-token-slot(border-radius, track-shape);
4146

42-
.mdc-switch:disabled & {
47+
.mdc-switch--disabled.mdc-switch & {
4348
@include token-utils.create-token-slot(opacity, disabled-track-opacity);
4449
}
4550
}
@@ -117,7 +122,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
117122
@include token-utils.create-token-slot(background, unselected-pressed-track-color);
118123
}
119124

120-
.mdc-switch:disabled & {
125+
#{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
126+
#{$_interactive-disabled-selector}:focus:not(:active) &,
127+
#{$_interactive-disabled-selector}:active &,
128+
.mdc-switch.mdc-switch--disabled & {
121129
@include token-utils.create-token-slot(background, disabled-unselected-track-color);
122130
}
123131
}
@@ -161,7 +169,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
161169
@include token-utils.create-token-slot(background, selected-pressed-track-color);
162170
}
163171

164-
.mdc-switch:disabled & {
172+
#{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
173+
#{$_interactive-disabled-selector}:focus:not(:active) &,
174+
#{$_interactive-disabled-selector}:active &,
175+
.mdc-switch.mdc-switch--disabled & {
165176
@include token-utils.create-token-slot(background, disabled-selected-track-color);
166177
}
167178
}
@@ -310,7 +321,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
310321
@include token-utils.create-token-slot(background, selected-pressed-handle-color);
311322
}
312323

313-
.mdc-switch--selected:disabled & {
324+
#{$_interactive-disabled-selector}.mdc-switch--selected:hover:not(:focus):not(:active) &,
325+
#{$_interactive-disabled-selector}.mdc-switch--selected:focus:not(:active) &,
326+
#{$_interactive-disabled-selector}.mdc-switch--selected:active &,
327+
.mdc-switch--selected.mdc-switch--disabled & {
314328
@include token-utils.create-token-slot(background, disabled-selected-handle-color);
315329
}
316330

@@ -330,7 +344,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
330344
@include token-utils.create-token-slot(background, unselected-pressed-handle-color);
331345
}
332346

333-
.mdc-switch--unselected:disabled & {
347+
.mdc-switch--unselected.mdc-switch--disabled & {
334348
@include token-utils.create-token-slot(background, disabled-unselected-handle-color);
335349
}
336350
}
@@ -354,7 +368,10 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
354368
@include token-utils.create-token-slot(box-shadow, handle-elevation-shadow);
355369
}
356370

357-
.mdc-switch:disabled & {
371+
#{$_interactive-disabled-selector}:hover:not(:focus):not(:active) &,
372+
#{$_interactive-disabled-selector}:focus:not(:active) &,
373+
#{$_interactive-disabled-selector}:active &,
374+
.mdc-switch.mdc-switch--disabled & {
358375
@include token-utils.create-token-slot(box-shadow, disabled-handle-elevation-shadow);
359376
}
360377
}
@@ -376,10 +393,14 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
376393
content: '';
377394
opacity: 0;
378395

379-
.mdc-switch:disabled & {
396+
.mdc-switch--disabled & {
380397
display: none;
381398
}
382399

400+
.mat-mdc-slide-toggle-disabled-interactive & {
401+
display: block;
402+
}
403+
383404
.mdc-switch:hover & {
384405
opacity: 0.04;
385406
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());
391412
}
392413

393414
@include token-utils.use-tokens($_mdc-slots...) {
415+
#{$_interactive-disabled-selector}:enabled:focus &,
416+
#{$_interactive-disabled-selector}:enabled:active &,
417+
#{$_interactive-disabled-selector}:enabled:hover:not(:focus) &,
394418
.mdc-switch--unselected:enabled:hover:not(:focus) & {
395419
@include token-utils.create-token-slot(background, unselected-hover-state-layer-color);
396420
}
@@ -429,11 +453,11 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
429453
z-index: 1;
430454

431455
@include token-utils.use-tokens($_mdc-slots...) {
432-
.mdc-switch--unselected:disabled & {
456+
.mdc-switch--disabled.mdc-switch--unselected & {
433457
@include token-utils.create-token-slot(opacity, disabled-unselected-icon-opacity);
434458
}
435459

436-
.mdc-switch--selected:disabled & {
460+
.mdc-switch--disabled.mdc-switch--selected & {
437461
@include token-utils.create-token-slot(opacity, disabled-selected-icon-opacity);
438462
}
439463
}
@@ -456,7 +480,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
456480
@include token-utils.create-token-slot(fill, unselected-icon-color);
457481
}
458482

459-
.mdc-switch--unselected:disabled & {
483+
.mdc-switch--unselected.mdc-switch--disabled & {
460484
@include token-utils.create-token-slot(fill, disabled-unselected-icon-color);
461485
}
462486

@@ -466,7 +490,7 @@ $_mat-slots: (tokens-mat-switch.$prefix, tokens-mat-switch.get-token-slots());
466490
@include token-utils.create-token-slot(fill, selected-icon-color);
467491
}
468492

469-
.mdc-switch--selected:disabled & {
493+
.mdc-switch--selected.mdc-switch--disabled & {
470494
@include token-utils.create-token-slot(fill, disabled-selected-icon-color);
471495
}
472496
}

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

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,40 @@ describe('MDC-based MatSlideToggle without forms', () => {
379379

380380
expect(slideToggleElement.querySelector('.mdc-switch__icons')).toBeFalsy();
381381
}));
382+
383+
it('should be able to mark a slide toggle as interactive while it is disabled', fakeAsync(() => {
384+
testComponent.isDisabled = true;
385+
fixture.changeDetectorRef.markForCheck();
386+
fixture.detectChanges();
387+
388+
expect(buttonElement.disabled).toBe(true);
389+
expect(buttonElement.hasAttribute('aria-disabled')).toBe(false);
390+
expect(buttonElement.getAttribute('tabindex')).toBe('-1');
391+
expect(buttonElement.classList).not.toContain('mat-mdc-slide-toggle-disabled-interactive');
392+
393+
testComponent.disabledInteractive = true;
394+
fixture.changeDetectorRef.markForCheck();
395+
fixture.detectChanges();
396+
397+
expect(buttonElement.disabled).toBe(false);
398+
expect(buttonElement.getAttribute('aria-disabled')).toBe('true');
399+
expect(buttonElement.getAttribute('tabindex')).toBe('0');
400+
expect(buttonElement.classList).toContain('mat-mdc-slide-toggle-disabled-interactive');
401+
}));
402+
403+
it('should not change its state when clicked while disabled and interactive', fakeAsync(() => {
404+
expect(slideToggle.checked).toBe(false);
405+
406+
testComponent.isDisabled = testComponent.disabledInteractive = true;
407+
fixture.changeDetectorRef.markForCheck();
408+
fixture.detectChanges();
409+
410+
buttonElement.click();
411+
fixture.detectChanges();
412+
tick();
413+
414+
expect(slideToggle.checked).toBe(false);
415+
}));
382416
});
383417

384418
describe('custom template', () => {
@@ -828,33 +862,36 @@ describe('MDC-based MatSlideToggle with forms', () => {
828862

829863
@Component({
830864
template: `
831-
<mat-slide-toggle [dir]="direction" [required]="isRequired"
832-
[disabled]="isDisabled"
833-
[color]="slideColor"
834-
[id]="slideId"
835-
[checked]="slideChecked"
836-
[name]="slideName"
837-
[aria-label]="slideLabel"
838-
[aria-labelledby]="slideLabelledBy"
839-
[aria-describedby]="slideAriaDescribedBy"
840-
[tabIndex]="slideTabindex"
841-
[labelPosition]="labelPosition"
842-
[disableRipple]="disableRipple"
843-
[hideIcon]="hideIcon"
844-
(toggleChange)="onSlideToggleChange()"
845-
(dragChange)="onSlideDragChange()"
846-
(change)="onSlideChange($event)"
847-
(click)="onSlideClick($event)">
865+
<mat-slide-toggle
866+
[dir]="direction"
867+
[required]="isRequired"
868+
[disabled]="isDisabled"
869+
[color]="slideColor"
870+
[id]="slideId"
871+
[checked]="slideChecked"
872+
[name]="slideName"
873+
[aria-label]="slideLabel"
874+
[aria-labelledby]="slideLabelledBy"
875+
[aria-describedby]="slideAriaDescribedBy"
876+
[tabIndex]="slideTabindex"
877+
[labelPosition]="labelPosition"
878+
[disableRipple]="disableRipple"
879+
[hideIcon]="hideIcon"
880+
[disabledInteractive]="disabledInteractive"
881+
(toggleChange)="onSlideToggleChange()"
882+
(dragChange)="onSlideDragChange()"
883+
(change)="onSlideChange($event)"
884+
(click)="onSlideClick($event)">
848885
<span>Test Slide Toggle</span>
849886
</mat-slide-toggle>`,
850887
standalone: true,
851888
imports: [MatSlideToggleModule, BidiModule],
852889
})
853890
class SlideToggleBasic {
854-
isDisabled: boolean = false;
855-
isRequired: boolean = false;
856-
disableRipple: boolean = false;
857-
slideChecked: boolean = false;
891+
isDisabled = false;
892+
isRequired = false;
893+
disableRipple = false;
894+
slideChecked = false;
858895
slideColor: string;
859896
slideId: string | null;
860897
slideName: string | null;
@@ -864,10 +901,11 @@ class SlideToggleBasic {
864901
slideTabindex: number;
865902
lastEvent: MatSlideToggleChange;
866903
labelPosition: string;
867-
toggleTriggered: number = 0;
868-
dragTriggered: number = 0;
904+
toggleTriggered = 0;
905+
dragTriggered = 0;
869906
direction: Direction = 'ltr';
870907
hideIcon = false;
908+
disabledInteractive = false;
871909

872910
onSlideClick: (event?: Event) => void = () => {};
873911
onSlideChange = (event: MatSlideToggleChange) => (this.lastEvent = event);

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ export class MatSlideToggle
187187
/** Whether to hide the icon inside of the slide toggle. */
188188
@Input({transform: booleanAttribute}) hideIcon: boolean;
189189

190+
/** Whether the slide toggle should remain interactive when it is disabled. */
191+
@Input({transform: booleanAttribute}) disabledInteractive: boolean;
192+
190193
/** An event will be dispatched each time the slide-toggle changes its value. */
191194
@Output() readonly change = new EventEmitter<MatSlideToggleChange>();
192195

@@ -215,6 +218,7 @@ export class MatSlideToggle
215218
this._noopAnimations = animationMode === 'NoopAnimations';
216219
this.id = this._uniqueId = `mat-mdc-slide-toggle-${++nextUniqueId}`;
217220
this.hideIcon = defaults.hideIcon ?? false;
221+
this.disabledInteractive = defaults.disabledInteractive ?? false;
218222
this._labelId = this._uniqueId + '-label';
219223
}
220224

@@ -295,12 +299,14 @@ export class MatSlideToggle
295299

296300
/** Method being called whenever the underlying button is clicked. */
297301
_handleClick() {
298-
this.toggleChange.emit();
302+
if (!this.disabled) {
303+
this.toggleChange.emit();
299304

300-
if (!this.defaults.disableToggleValue) {
301-
this.checked = !this.checked;
302-
this._onChange(this.checked);
303-
this.change.emit(new MatSlideToggleChange(this, this.checked));
305+
if (!this.defaults.disableToggleValue) {
306+
this.checked = !this.checked;
307+
this._onChange(this.checked);
308+
this.change.emit(new MatSlideToggleChange(this, this.checked));
309+
}
304310
}
305311
}
306312

tools/public_api_guard/material/slide-toggle.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
5353
// (undocumented)
5454
defaults: MatSlideToggleDefaultOptions;
5555
disabled: boolean;
56+
disabledInteractive: boolean;
5657
disableRipple: boolean;
5758
protected _emitChangeEvent(): void;
5859
focus(): void;
@@ -73,6 +74,8 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
7374
// (undocumented)
7475
static ngAcceptInputType_disabled: unknown;
7576
// (undocumented)
77+
static ngAcceptInputType_disabledInteractive: unknown;
78+
// (undocumented)
7679
static ngAcceptInputType_disableRipple: unknown;
7780
// (undocumented)
7881
static ngAcceptInputType_hideIcon: unknown;
@@ -99,7 +102,7 @@ export class MatSlideToggle implements OnDestroy, AfterContentInit, OnChanges, C
99102
validate(control: AbstractControl<boolean>): ValidationErrors | null;
100103
writeValue(value: any): void;
101104
// (undocumented)
102-
static ɵcmp: i0.ɵɵComponentDeclaration<MatSlideToggle, "mat-slide-toggle", ["matSlideToggle"], { "name": { "alias": "name"; "required": false; }; "id": { "alias": "id"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "ariaDescribedby": { "alias": "aria-describedby"; "required": false; }; "required": { "alias": "required"; "required": false; }; "color": { "alias": "color"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "checked": { "alias": "checked"; "required": false; }; "hideIcon": { "alias": "hideIcon"; "required": false; }; }, { "change": "change"; "toggleChange": "toggleChange"; }, never, ["*"], true, never>;
105+
static ɵcmp: i0.ɵɵComponentDeclaration<MatSlideToggle, "mat-slide-toggle", ["matSlideToggle"], { "name": { "alias": "name"; "required": false; }; "id": { "alias": "id"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "ariaDescribedby": { "alias": "aria-describedby"; "required": false; }; "required": { "alias": "required"; "required": false; }; "color": { "alias": "color"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "checked": { "alias": "checked"; "required": false; }; "hideIcon": { "alias": "hideIcon"; "required": false; }; "disabledInteractive": { "alias": "disabledInteractive"; "required": false; }; }, { "change": "change"; "toggleChange": "toggleChange"; }, never, ["*"], true, never>;
103106
// (undocumented)
104107
static ɵfac: i0.ɵɵFactoryDeclaration<MatSlideToggle, [null, null, null, { attribute: "tabindex"; }, null, { optional: true; }]>;
105108
}
@@ -116,6 +119,7 @@ export class MatSlideToggleChange {
116119
// @public
117120
export interface MatSlideToggleDefaultOptions {
118121
color?: ThemePalette;
122+
disabledInteractive?: boolean;
119123
disableToggleValue?: boolean;
120124
hideIcon?: boolean;
121125
}

0 commit comments

Comments
 (0)