Skip to content

Commit 352c565

Browse files
committed
fix(material-experimental/mdc-slider): sync ui on mouseenter
* The MDC Foundation stores the bounding client rect when layout is first called. This means that if the position of the slider changes after the initial layout, the slider will break. To fix this broken behavior, we have to keep calling layout. * Added a unit test to ensure layout changes does not break the slider.
1 parent e15578b commit 352c565

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

src/material-experimental/mdc-slider/slider.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ describe('MDC-based MatSlider' , () => {
9898
setValueByClick(sliderInstance, 0, platform.IOS);
9999
expect(document.activeElement).toBe(inputInstance._hostElement);
100100
});
101+
102+
it('should not break on when the page layout changes', () => {
103+
sliderInstance._elementRef.nativeElement.style.marginLeft = '100px';
104+
setValueByClick(sliderInstance, 10, platform.IOS);
105+
expect(inputInstance.value).toBe(10);
106+
sliderInstance._elementRef.nativeElement.style.marginLeft = 'initial';
107+
});
101108
});
102109

103110
describe('standard range slider', () => {
@@ -1891,6 +1898,7 @@ function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) {
18911898
const sliderElement = slider._elementRef.nativeElement;
18921899
const {x, y} = getCoordsForValue(slider, value);
18931900

1901+
dispatchPointerEvent(sliderElement, 'mouseenter', x, y);
18941902
dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, x, y, isIOS);
18951903
dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, x, y, isIOS);
18961904
}
@@ -1901,6 +1909,7 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is
19011909
const {x: startX, y: startY} = getCoordsForValue(slider, slider._getInput(thumbPosition).value);
19021910
const {x: endX, y: endY} = getCoordsForValue(slider, value);
19031911

1912+
dispatchPointerEvent(sliderElement, 'mouseenter', startX, startY);
19041913
dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, startX, startY, isIOS);
19051914
dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_MOVE, endX, endY, isIOS);
19061915
dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS);
@@ -1921,11 +1930,11 @@ function getCoordsForValue(slider: MatSlider, value: number): Point {
19211930
/** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */
19221931
function dispatchPointerOrTouchEvent(
19231932
node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) {
1924-
if (isIOS) {
1925-
dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y);
1926-
} else {
1927-
dispatchPointerEvent(node, type, x, y);
1928-
}
1933+
if (isIOS) {
1934+
dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y);
1935+
} else {
1936+
dispatchPointerEvent(node, type, x, y);
1937+
}
19291938
}
19301939

19311940
/** Returns the touch event equivalent of the given pointer event. */

src/material-experimental/mdc-slider/slider.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,11 @@ export class MatSlider extends _MatSliderMixinBase
626626
/** The display value of the end thumb. */
627627
_endValueIndicatorText: string;
628628

629+
/** Whether the browser supports pointer events. */
630+
private _SUPPORTS_POINTER_EVENTS = typeof PointerEvent !== 'undefined'
631+
&& !!PointerEvent
632+
&& !this._platform.IOS; // We exclude iOS to mirror the MDC Foundation
633+
629634
/** Subscription to changes to the directionality (LTR / RTL) context for the application. */
630635
private _dirChangeSubscription: Subscription;
631636

@@ -643,6 +648,7 @@ export class MatSlider extends _MatSliderMixinBase
643648
this._document = document;
644649
this._window = this._document.defaultView || window;
645650
this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange());
651+
this._attachUISyncEventListener();
646652
}
647653

648654
ngAfterViewInit() {
@@ -676,13 +682,48 @@ export class MatSlider extends _MatSliderMixinBase
676682
this._foundation.destroy();
677683
}
678684
this._dirChangeSubscription.unsubscribe();
685+
this._removeUISyncEventListener();
679686
}
680687

681688
/** Returns true if the language direction for this slider element is right to left. */
682689
_isRTL() {
683690
return this._dir && this._dir.value === 'rtl';
684691
}
685692

693+
/**
694+
* Attaches an event listener that keeps sync the slider UI and the foundation in sync.
695+
*
696+
* Because the MDC Foundation stores the value of the bounding client rect when layout is called,
697+
* we need to keep calling layout to avoid the position of the slider getting out of sync with
698+
* what the foundation has stored. If we don't do this, the foundation will not be able to
699+
* correctly calculate the slider value on click/slide.
700+
*/
701+
_attachUISyncEventListener(): void {
702+
// Implementation detail: It may seem weird that we are using "mouseenter" instead of
703+
// "mousedown" as the default for when a browser does not support pointer events. While we
704+
// would prefer to use "mousedown" as the default, for some reason it does not work (the
705+
// callback is never triggered).
706+
if (this._SUPPORTS_POINTER_EVENTS) {
707+
this._elementRef.nativeElement.addEventListener('pointerdown', this._layout);
708+
} else {
709+
this._elementRef.nativeElement.addEventListener('mouseenter', this._layout);
710+
this._elementRef.nativeElement.addEventListener('touchstart', this._layout);
711+
}
712+
}
713+
714+
/** Removes the event listener that keeps sync the slider UI and the foundation in sync. */
715+
_removeUISyncEventListener(): void {
716+
if (this._SUPPORTS_POINTER_EVENTS) {
717+
this._elementRef.nativeElement.removeEventListener('pointerdown', this._layout);
718+
} else {
719+
this._elementRef.nativeElement.removeEventListener('mouseenter', this._layout);
720+
this._elementRef.nativeElement.removeEventListener('touchstart', this._layout);
721+
}
722+
}
723+
724+
/** Wrapper function for calling layout (needed for adding & removing an event listener). */
725+
private _layout = () => this._foundation.layout();
726+
686727
/**
687728
* Reinitializes the slider foundation and input state(s).
688729
*

0 commit comments

Comments
 (0)