Skip to content

Commit 7f36f0d

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 b6cdf65 commit 7f36f0d

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,16 @@ export class MatSlider extends _MatSliderMixinBase
626626
/** The display value of the end thumb. */
627627
_endValueIndicatorText: string;
628628

629+
/**
630+
* Whether the browser supports pointer events.
631+
*
632+
* We exclude iOS to mirror the MDC Foundation. The MDC Foundation cannot use pointer events on
633+
* iOS because of this open bug - https://bugs.webkit.org/show_bug.cgi?id=220196.
634+
*/
635+
private _SUPPORTS_POINTER_EVENTS = typeof PointerEvent !== 'undefined'
636+
&& !!PointerEvent
637+
&& !this._platform.IOS;
638+
629639
/** Subscription to changes to the directionality (LTR / RTL) context for the application. */
630640
private _dirChangeSubscription: Subscription;
631641

@@ -643,6 +653,7 @@ export class MatSlider extends _MatSliderMixinBase
643653
this._document = document;
644654
this._window = this._document.defaultView || window;
645655
this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange());
656+
this._attachUISyncEventListener();
646657
}
647658

648659
ngAfterViewInit() {
@@ -676,13 +687,48 @@ export class MatSlider extends _MatSliderMixinBase
676687
this._foundation.destroy();
677688
}
678689
this._dirChangeSubscription.unsubscribe();
690+
this._removeUISyncEventListener();
679691
}
680692

681693
/** Returns true if the language direction for this slider element is right to left. */
682694
_isRTL() {
683695
return this._dir && this._dir.value === 'rtl';
684696
}
685697

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

0 commit comments

Comments
 (0)