mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(slide-toggle): input not updated after drag (#3067)
The `MdSlideToggle` uses the OnPush ChangeDetection strategy for the component. **Problem Explanation** When the thumb is being dragged and the value changes, the new value is currently applied after a timeout. This timeout does not trigger any change detection, and the new value won't be applied to the underlying input. **Solution** With this change, the new value is applied immediately in the `dragend` event (because events trigger a change detection) and just the `dragging` state will be updated in the next tick. The timeout is required to ensure that there will be no `click` event after the `dragend` event.
This commit is contained in:
committed by
Andrew Seguin
parent
bc9d25b2bb
commit
5cdeb755e0
@@ -438,6 +438,7 @@ describe('MdSlideToggle', () => {
|
||||
let slideToggleElement: HTMLElement;
|
||||
let slideToggleControl: NgControl;
|
||||
let slideThumbContainer: HTMLElement;
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(SlideToggleTestApp);
|
||||
@@ -453,6 +454,8 @@ describe('MdSlideToggle', () => {
|
||||
slideToggleElement = slideToggleDebug.nativeElement;
|
||||
slideToggleControl = slideToggleDebug.injector.get(NgControl);
|
||||
slideThumbContainer = thumbContainerDebug.nativeElement;
|
||||
|
||||
inputElement = slideToggleElement.querySelector('input');
|
||||
}));
|
||||
|
||||
it('should drag from start to end', fakeAsync(() => {
|
||||
@@ -495,7 +498,7 @@ describe('MdSlideToggle', () => {
|
||||
expect(slideThumbContainer.classList).not.toContain('mat-dragging');
|
||||
}));
|
||||
|
||||
it('should not drag when disbaled', fakeAsync(() => {
|
||||
it('should not drag when disabled', fakeAsync(() => {
|
||||
slideToggle.disabled = true;
|
||||
|
||||
expect(slideToggle.checked).toBe(false);
|
||||
@@ -538,6 +541,28 @@ describe('MdSlideToggle', () => {
|
||||
expect(testComponent.lastEvent.checked).toBe(true);
|
||||
}));
|
||||
|
||||
it('should update the checked property of the input', fakeAsync(() => {
|
||||
expect(inputElement.checked).toBe(false);
|
||||
|
||||
gestureConfig.emitEventForElement('slidestart', slideThumbContainer);
|
||||
|
||||
expect(slideThumbContainer.classList).toContain('mat-dragging');
|
||||
|
||||
gestureConfig.emitEventForElement('slide', slideThumbContainer, {
|
||||
deltaX: 200 // Arbitrary, large delta that will be clamped to the end of the slide-toggle.
|
||||
});
|
||||
|
||||
gestureConfig.emitEventForElement('slideend', slideThumbContainer);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(inputElement.checked).toBe(true);
|
||||
|
||||
// Flush the timeout for the slide ending.
|
||||
tick();
|
||||
|
||||
expect(slideThumbContainer.classList).not.toContain('mat-dragging');
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('with a FormControl', () => {
|
||||
|
||||
@@ -134,7 +134,7 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
|
||||
event.stopPropagation();
|
||||
|
||||
// Once a drag is currently in progress, we do not want to toggle the slide-toggle on a click.
|
||||
if (!this.disabled && !this._slideRenderer.isDragging()) {
|
||||
if (!this.disabled && !this._slideRenderer.dragging) {
|
||||
this.toggle();
|
||||
|
||||
// Emit our custom change event if the native input emitted one.
|
||||
@@ -255,22 +255,20 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
|
||||
}
|
||||
|
||||
_onDrag(event: HammerInput) {
|
||||
if (this._slideRenderer.isDragging()) {
|
||||
if (this._slideRenderer.dragging) {
|
||||
this._slideRenderer.updateThumbPosition(event.deltaX);
|
||||
}
|
||||
}
|
||||
|
||||
_onDragEnd() {
|
||||
if (!this._slideRenderer.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Notice that we have to stop outside of the current event handler,
|
||||
// because otherwise the click event will be fired and will reset the new checked variable.
|
||||
setTimeout(() => {
|
||||
this.checked = this._slideRenderer.stopThumbDrag();
|
||||
if (this._slideRenderer.dragging) {
|
||||
this.checked = this._slideRenderer.dragPercentage > 50;
|
||||
this._emitChangeEvent();
|
||||
}, 0);
|
||||
|
||||
// The drag should be stopped outside of the current event handler, because otherwise the
|
||||
// click event will be fired before and will revert the drag change.
|
||||
setTimeout(() => this._slideRenderer.stopThumbDrag());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -280,48 +278,57 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
|
||||
*/
|
||||
class SlideToggleRenderer {
|
||||
|
||||
/** Reference to the thumb HTMLElement. */
|
||||
private _thumbEl: HTMLElement;
|
||||
|
||||
/** Reference to the thumb bar HTMLElement. */
|
||||
private _thumbBarEl: HTMLElement;
|
||||
|
||||
/** Width of the thumb bar of the slide-toggle. */
|
||||
private _thumbBarWidth: number;
|
||||
private _checked: boolean;
|
||||
private _percentage: number;
|
||||
|
||||
/** Previous checked state before drag started. */
|
||||
private _previousChecked: boolean;
|
||||
|
||||
/** Percentage of the thumb while dragging. */
|
||||
dragPercentage: number;
|
||||
|
||||
/** Whether the thumb is currently being dragged. */
|
||||
dragging: boolean = false;
|
||||
|
||||
constructor(private _elementRef: ElementRef) {
|
||||
this._thumbEl = _elementRef.nativeElement.querySelector('.mat-slide-toggle-thumb-container');
|
||||
this._thumbBarEl = _elementRef.nativeElement.querySelector('.mat-slide-toggle-bar');
|
||||
}
|
||||
|
||||
/** Whether the slide-toggle is currently dragging. */
|
||||
isDragging(): boolean {
|
||||
return !!this._thumbBarWidth;
|
||||
}
|
||||
|
||||
|
||||
/** Initializes the drag of the slide-toggle. */
|
||||
startThumbDrag(checked: boolean) {
|
||||
if (!this.isDragging()) {
|
||||
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
|
||||
this._checked = checked;
|
||||
this._thumbEl.classList.add('mat-dragging');
|
||||
}
|
||||
if (this.dragging) { return; }
|
||||
|
||||
this._thumbBarWidth = this._thumbBarEl.clientWidth - this._thumbEl.clientWidth;
|
||||
this._thumbEl.classList.add('mat-dragging');
|
||||
|
||||
this._previousChecked = checked;
|
||||
this.dragging = true;
|
||||
}
|
||||
|
||||
/** Stops the current drag and returns the new checked value. */
|
||||
/** Resets the current drag and returns the new checked value. */
|
||||
stopThumbDrag(): boolean {
|
||||
if (this.isDragging()) {
|
||||
this._thumbBarWidth = null;
|
||||
this._thumbEl.classList.remove('mat-dragging');
|
||||
if (!this.dragging) { return; }
|
||||
|
||||
applyCssTransform(this._thumbEl, '');
|
||||
this.dragging = false;
|
||||
this._thumbEl.classList.remove('mat-dragging');
|
||||
|
||||
return this._percentage > 50;
|
||||
}
|
||||
// Reset the transform because the component will take care of the thumb position after drag.
|
||||
applyCssTransform(this._thumbEl, '');
|
||||
|
||||
return this.dragPercentage > 50;
|
||||
}
|
||||
|
||||
/** Updates the thumb containers position from the specified distance. */
|
||||
updateThumbPosition(distance: number) {
|
||||
this._percentage = this._getThumbPercentage(distance);
|
||||
applyCssTransform(this._thumbEl, `translate3d(${this._percentage}%, 0, 0)`);
|
||||
this.dragPercentage = this._getThumbPercentage(distance);
|
||||
applyCssTransform(this._thumbEl, `translate3d(${this.dragPercentage}%, 0, 0)`);
|
||||
}
|
||||
|
||||
/** Retrieves the percentage of thumb from the moved distance. */
|
||||
@@ -329,7 +336,7 @@ class SlideToggleRenderer {
|
||||
let percentage = (distance / this._thumbBarWidth) * 100;
|
||||
|
||||
// When the toggle was initially checked, then we have to start the drag at the end.
|
||||
if (this._checked) {
|
||||
if (this._previousChecked) {
|
||||
percentage += 100;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user