Skip to content

Commit 2f40a8d

Browse files
authored
fix(material/slider): incorrectly attributing touches for multiple touch events (#22615)
`mat-slider` is set up so that it binds global `touchmove` and `touchend` events after dragging starts so that the user can drag while not keeping their finger right on top of the slider. The problem is that currently we only use the first touch from the list of touches which means that if the user is dragging more than one slider on a multi-touch device, they'll all have the same value. These changes add some logic that attributes the events to the slider that started the dragging sequence. Fixes #22614.
1 parent 62be7b3 commit 2f40a8d

File tree

2 files changed

+103
-37
lines changed

2 files changed

+103
-37
lines changed

src/cdk/testing/testbed/fake-events/event-objects.ts

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

99
import {ModifierKeys} from '@angular/cdk/testing';
1010

11+
/** Used to generate unique IDs for events. */
12+
let uniqueIds = 0;
13+
1114
/**
1215
* Creates a browser MouseEvent with the specified options.
1316
* @docs-private
@@ -83,7 +86,7 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0, clientX = 0
8386
// In favor of creating events that work for most of the browsers, the event is created
8487
// as a basic UI Event. The necessary details for the event will be set manually.
8588
const event = document.createEvent('UIEvent');
86-
const touchDetails = {pageX, pageY, clientX, clientY};
89+
const touchDetails = {pageX, pageY, clientX, clientY, id: uniqueIds++};
8790

8891
// TS3.6 removes the initUIEvent method and suggests porting to "new UIEvent()".
8992
(event as any).initUIEvent(type, true, true, window, 0);

src/material/slider/slider.ts

Lines changed: 99 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,15 @@ export class MatSlider extends _MatSliderMixinBase
503503
/** Used to subscribe to global move and end events */
504504
protected _document: Document;
505505

506+
/**
507+
* Identifier used to attribute a touch event to a particular slider.
508+
* Will be undefined if one of the following conditions is true:
509+
* - The user isn't dragging using a touch device.
510+
* - The browser doesn't support `Touch.identifier`.
511+
* - Dragging hasn't started yet.
512+
*/
513+
private _touchId: number | undefined;
514+
506515
constructor(elementRef: ElementRef,
507516
private _focusMonitor: FocusMonitor,
508517
private _changeDetectorRef: ChangeDetectorRef,
@@ -636,21 +645,26 @@ export class MatSlider extends _MatSliderMixinBase
636645
}
637646

638647
this._ngZone.run(() => {
639-
const oldValue = this.value;
640-
const pointerPosition = getPointerPositionOnPage(event);
641-
this._isSliding = true;
642-
this._lastPointerEvent = event;
643-
event.preventDefault();
644-
this._focusHostElement();
645-
this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
646-
this._bindGlobalEvents(event);
647-
this._focusHostElement();
648-
this._updateValueFromPosition(pointerPosition);
649-
this._valueOnSlideStart = oldValue;
650-
651-
// Emit a change and input event if the value changed.
652-
if (oldValue != this.value) {
653-
this._emitInputEvent();
648+
this._touchId = isTouchEvent(event) ?
649+
getTouchIdForSlider(event, this._elementRef.nativeElement) : undefined;
650+
const pointerPosition = getPointerPositionOnPage(event, this._touchId);
651+
652+
if (pointerPosition) {
653+
const oldValue = this.value;
654+
this._isSliding = true;
655+
this._lastPointerEvent = event;
656+
event.preventDefault();
657+
this._focusHostElement();
658+
this._onMouseenter(); // Simulate mouseenter in case this is a mobile device.
659+
this._bindGlobalEvents(event);
660+
this._focusHostElement();
661+
this._updateValueFromPosition(pointerPosition);
662+
this._valueOnSlideStart = oldValue;
663+
664+
// Emit a change and input event if the value changed.
665+
if (oldValue != this.value) {
666+
this._emitInputEvent();
667+
}
654668
}
655669
});
656670
}
@@ -661,31 +675,41 @@ export class MatSlider extends _MatSliderMixinBase
661675
*/
662676
private _pointerMove = (event: TouchEvent | MouseEvent) => {
663677
if (this._isSliding) {
664-
// Prevent the slide from selecting anything else.
665-
event.preventDefault();
666-
const oldValue = this.value;
667-
this._lastPointerEvent = event;
668-
this._updateValueFromPosition(getPointerPositionOnPage(event));
669-
670-
// Native range elements always emit `input` events when the value changed while sliding.
671-
if (oldValue != this.value) {
672-
this._emitInputEvent();
678+
const pointerPosition = getPointerPositionOnPage(event, this._touchId);
679+
680+
if (pointerPosition) {
681+
// Prevent the slide from selecting anything else.
682+
event.preventDefault();
683+
const oldValue = this.value;
684+
this._lastPointerEvent = event;
685+
this._updateValueFromPosition(pointerPosition);
686+
687+
// Native range elements always emit `input` events when the value changed while sliding.
688+
if (oldValue != this.value) {
689+
this._emitInputEvent();
690+
}
673691
}
674692
}
675693
}
676694

677695
/** Called when the user has lifted their pointer. Bound on the document level. */
678696
private _pointerUp = (event: TouchEvent | MouseEvent) => {
679697
if (this._isSliding) {
680-
event.preventDefault();
681-
this._removeGlobalEvents();
682-
this._isSliding = false;
683-
684-
if (this._valueOnSlideStart != this.value && !this.disabled) {
685-
this._emitChangeEvent();
698+
if (!isTouchEvent(event) || typeof this._touchId !== 'number' ||
699+
// Note that we use `changedTouches`, rather than `touches` because it
700+
// seems like in most cases `touches` is empty for `touchend` events.
701+
findMatchingTouch(event.changedTouches, this._touchId)) {
702+
event.preventDefault();
703+
this._removeGlobalEvents();
704+
this._isSliding = false;
705+
this._touchId = undefined;
706+
707+
if (this._valueOnSlideStart != this.value && !this.disabled) {
708+
this._emitChangeEvent();
709+
}
710+
711+
this._valueOnSlideStart = this._lastPointerEvent = null;
686712
}
687-
688-
this._valueOnSlideStart = this._lastPointerEvent = null;
689713
}
690714
}
691715

@@ -919,8 +943,47 @@ function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
919943
}
920944

921945
/** Gets the coordinates of a touch or mouse event relative to the viewport. */
922-
function getPointerPositionOnPage(event: MouseEvent | TouchEvent) {
923-
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
924-
const point = isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event;
925-
return {x: point.clientX, y: point.clientY};
946+
function getPointerPositionOnPage(event: MouseEvent | TouchEvent, id: number|undefined) {
947+
let point: {clientX: number, clientY: number}|undefined;
948+
949+
if (isTouchEvent(event)) {
950+
// The `identifier` could be undefined if the browser doesn't support `TouchEvent.identifier`.
951+
// If that's the case, attribute the first touch to all active sliders. This should still cover
952+
// the most common case while only breaking multi-touch.
953+
if (typeof id === 'number') {
954+
point = findMatchingTouch(event.touches, id) || findMatchingTouch(event.changedTouches, id);
955+
} else {
956+
// `touches` will be empty for start/end events so we have to fall back to `changedTouches`.
957+
point = event.touches[0] || event.changedTouches[0];
958+
}
959+
} else {
960+
point = event;
961+
}
962+
963+
return point ? {x: point.clientX, y: point.clientY} : undefined;
964+
}
965+
966+
/** Finds a `Touch` with a specific ID in a `TouchList`. */
967+
function findMatchingTouch(touches: TouchList, id: number): Touch | undefined {
968+
for (let i = 0; i < touches.length; i++) {
969+
if (touches[i].identifier === id) {
970+
return touches[i];
971+
}
972+
}
973+
974+
return undefined;
975+
}
976+
977+
978+
/** Gets the unique ID of a touch that matches a specific slider. */
979+
function getTouchIdForSlider(event: TouchEvent, sliderHost: HTMLElement): number | undefined {
980+
for (let i = 0; i < event.touches.length; i++) {
981+
const target = event.touches[i].target as HTMLElement;
982+
983+
if (sliderHost === target || sliderHost.contains(target)) {
984+
return event.touches[i].identifier;
985+
}
986+
}
987+
988+
return undefined;
926989
}

0 commit comments

Comments
 (0)