Skip to content

Commit 32193e9

Browse files
authored
feat(slide-toggle): make slide-toggle click and drag actions configurable (#11719)
1 parent 50ea837 commit 32193e9

File tree

4 files changed

+155
-6
lines changed

4 files changed

+155
-6
lines changed

src/lib/slide-toggle/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99
export * from './slide-toggle-module';
1010
export * from './slide-toggle';
11-
11+
export * from './slide-toggle-config';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {InjectionToken} from '@angular/core';
9+
10+
11+
/** Default `mat-slide-toggle` options that can be overridden. */
12+
export interface MatSlideToggleDefaultOptions {
13+
/** Whether toggle action triggers value changes in slide toggle. */
14+
disableToggleValue?: boolean;
15+
/** Whether drag action triggers value changes in slide toggle. */
16+
disableDragValue?: boolean;
17+
}
18+
19+
/** Injection token to be used to override the default options for `mat-slide-toggle`. */
20+
export const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS =
21+
new InjectionToken<MatSlideToggleDefaultOptions>('mat-slide-toggle-default-options', {
22+
providedIn: 'root',
23+
factory: () => ({disableToggleValue: false, disableDragValue: false})
24+
});

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/f
66
import {defaultRippleAnimationConfig} from '@angular/material/core';
77
import {By, HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
88
import {TestGestureConfig} from '../slider/test-gesture-config';
9+
import {MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS} from './slide-toggle-config';
910
import {MatSlideToggle, MatSlideToggleChange, MatSlideToggleModule} from './index';
1011

1112
describe('MatSlideToggle without forms', () => {
@@ -355,6 +356,96 @@ describe('MatSlideToggle without forms', () => {
355356
}));
356357
});
357358

359+
describe('custom action configuration', () => {
360+
it('should not change value on click when click action is noop', fakeAsync(() => {
361+
TestBed
362+
.resetTestingModule()
363+
.configureTestingModule({
364+
imports: [MatSlideToggleModule],
365+
declarations: [SlideToggleBasic],
366+
providers: [
367+
{
368+
provide: HAMMER_GESTURE_CONFIG,
369+
useFactory: () => gestureConfig = new TestGestureConfig()
370+
},
371+
{provide: MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, useValue: {disableToggleValue: true}},
372+
]
373+
});
374+
const fixture = TestBed.createComponent(SlideToggleBasic);
375+
const testComponent = fixture.debugElement.componentInstance;
376+
const slideToggleDebug = fixture.debugElement.query(By.css('mat-slide-toggle'));
377+
378+
const slideToggle = slideToggleDebug.componentInstance;
379+
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
380+
const labelElement = fixture.debugElement.query(By.css('label')).nativeElement;
381+
382+
expect(testComponent.toggleTriggered).toBe(0);
383+
expect(testComponent.dragTriggered).toBe(0);
384+
expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
385+
386+
labelElement.click();
387+
fixture.detectChanges();
388+
389+
expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
390+
expect(testComponent.toggleTriggered).toBe(1, 'Expect toggle once');
391+
expect(testComponent.dragTriggered).toBe(0);
392+
393+
inputElement.click();
394+
fixture.detectChanges();
395+
396+
expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
397+
expect(testComponent.toggleTriggered).toBe(2, 'Expect toggle twice');
398+
expect(testComponent.dragTriggered).toBe(0);
399+
}));
400+
401+
it('should not change value on dragging when drag action is noop', fakeAsync(() => {
402+
TestBed
403+
.resetTestingModule()
404+
.configureTestingModule({
405+
imports: [MatSlideToggleModule],
406+
declarations: [SlideToggleBasic],
407+
providers: [
408+
{
409+
provide: HAMMER_GESTURE_CONFIG,
410+
useFactory: () => gestureConfig = new TestGestureConfig()
411+
},
412+
{provide: MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS, useValue: {disableDragValue: true}},
413+
]
414+
});
415+
const fixture = TestBed.createComponent(SlideToggleBasic);
416+
const testComponent = fixture.debugElement.componentInstance;
417+
const slideToggleDebug = fixture.debugElement.query(By.css('mat-slide-toggle'));
418+
const thumbContainerDebug = slideToggleDebug
419+
.query(By.css('.mat-slide-toggle-thumb-container'));
420+
421+
const slideThumbContainer = thumbContainerDebug.nativeElement;
422+
const slideToggle = slideToggleDebug.componentInstance;
423+
424+
expect(testComponent.toggleTriggered).toBe(0);
425+
expect(testComponent.dragTriggered).toBe(0);
426+
expect(slideToggle.checked).toBe(false);
427+
428+
gestureConfig.emitEventForElement('slidestart', slideThumbContainer);
429+
430+
expect(slideThumbContainer.classList).toContain('mat-dragging');
431+
432+
gestureConfig.emitEventForElement('slide', slideThumbContainer, {
433+
deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle.
434+
});
435+
436+
gestureConfig.emitEventForElement('slideend', slideThumbContainer);
437+
438+
// Flush the timeout for the slide ending.
439+
tick();
440+
441+
expect(slideToggle.checked).toBe(false, 'Expect slide toggle value not changed');
442+
expect(slideThumbContainer.classList).not.toContain('mat-dragging');
443+
expect(testComponent.lastEvent).toBeUndefined();
444+
expect(testComponent.toggleTriggered).toBe(0);
445+
expect(testComponent.dragTriggered).toBe(1, 'Expect drag once');
446+
}));
447+
});
448+
358449
describe('with dragging', () => {
359450
let fixture: ComponentFixture<any>;
360451

@@ -849,6 +940,8 @@ describe('MatSlideToggle with forms', () => {
849940
[tabIndex]="slideTabindex"
850941
[labelPosition]="labelPosition"
851942
[disableRipple]="disableRipple"
943+
(toggleChange)="onSlideToggleChange()"
944+
(dragChange)="onSlideDragChange()"
852945
(change)="onSlideChange($event)"
853946
(click)="onSlideClick($event)">
854947
<span>Test Slide Toggle</span>
@@ -867,9 +960,13 @@ class SlideToggleBasic {
867960
slideTabindex: number;
868961
lastEvent: MatSlideToggleChange;
869962
labelPosition: string;
963+
toggleTriggered: number = 0;
964+
dragTriggered: number = 0;
870965

871966
onSlideClick: (event?: Event) => void = () => {};
872967
onSlideChange = (event: MatSlideToggleChange) => this.lastEvent = event;
968+
onSlideToggleChange = () => this.toggleTriggered++;
969+
onSlideDragChange = () => this.dragTriggered++;
873970
}
874971

875972
@Component({

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ import {
4242
RippleRef,
4343
} from '@angular/material/core';
4444
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
45+
import {
46+
MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS,
47+
MatSlideToggleDefaultOptions
48+
} from './slide-toggle-config';
4549

4650
// Increasing integer for generating unique ids for slide-toggle components.
4751
let nextUniqueId = 0;
@@ -153,6 +157,21 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
153157
@Output() readonly change: EventEmitter<MatSlideToggleChange> =
154158
new EventEmitter<MatSlideToggleChange>();
155159

160+
/**
161+
* An event will be dispatched each time the slide-toggle input is toggled.
162+
* This event always fire when user toggle the slide toggle, but does not mean the slide toggle's
163+
* value is changed. The event does not fire when user drag to change the slide toggle value.
164+
*/
165+
@Output() readonly toggleChange: EventEmitter<void> = new EventEmitter<void>();
166+
167+
/**
168+
* An event will be dispatched each time the slide-toggle is dragged.
169+
* This event always fire when user drag the slide toggle to make a change that greater than 50%.
170+
* It does not mean the slide toggle's value is changed. The event does not fire when user toggle
171+
* the slide toggle to change the slide toggle's value.
172+
*/
173+
@Output() readonly dragChange: EventEmitter<void> = new EventEmitter<void>();
174+
156175
/** Returns the unique id for the visual hidden input. */
157176
get inputId(): string { return `${this.id || this._uniqueId}-input`; }
158177

@@ -172,8 +191,9 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
172191
private _changeDetectorRef: ChangeDetectorRef,
173192
@Attribute('tabindex') tabIndex: string,
174193
private _ngZone: NgZone,
194+
@Inject(MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS)
195+
public defaults: MatSlideToggleDefaultOptions,
175196
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
176-
177197
super(elementRef);
178198
this.tabIndex = parseInt(tabIndex) || 0;
179199
}
@@ -195,10 +215,15 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
195215
// emit its event object to the component's `change` output.
196216
event.stopPropagation();
197217

218+
if (!this._dragging) {
219+
this.toggleChange.emit();
220+
}
198221
// Releasing the pointer over the `<label>` element while dragging triggers another
199222
// click event on the `<label>` element. This means that the checked state of the underlying
200-
// input changed unintentionally and needs to be changed back.
201-
if (this._dragging) {
223+
// input changed unintentionally and needs to be changed back. Or when the slide toggle's config
224+
// disabled toggle change event by setting `disableToggleValue: true`, the slide toggle's value
225+
// does not change, and the checked state of the underlying input needs to be changed back.
226+
if (this._dragging || this.defaults.disableToggleValue) {
202227
this._inputElement.nativeElement.checked = this.checked;
203228
return;
204229
}
@@ -316,8 +341,11 @@ export class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestro
316341
const newCheckedValue = this._dragPercentage > 50;
317342

318343
if (newCheckedValue !== this.checked) {
319-
this.checked = newCheckedValue;
320-
this._emitChangeEvent();
344+
this.dragChange.emit();
345+
if (!this.defaults.disableDragValue) {
346+
this.checked = newCheckedValue;
347+
this._emitChangeEvent();
348+
}
321349
}
322350

323351
// The drag should be stopped outside of the current event handler, otherwise the

0 commit comments

Comments
 (0)