From 7f36f0d880b66c36895c5b61f92b5a84fde4025f Mon Sep 17 00:00:00 2001 From: wagnermaciel Date: Thu, 29 Apr 2021 11:45:43 -0700 Subject: [PATCH] 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. --- .../mdc-slider/slider.spec.ts | 19 ++++++-- .../mdc-slider/slider.ts | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/material-experimental/mdc-slider/slider.spec.ts b/src/material-experimental/mdc-slider/slider.spec.ts index 625853d271c5..608c3fe7f7d7 100644 --- a/src/material-experimental/mdc-slider/slider.spec.ts +++ b/src/material-experimental/mdc-slider/slider.spec.ts @@ -98,6 +98,13 @@ describe('MDC-based MatSlider' , () => { setValueByClick(sliderInstance, 0, platform.IOS); expect(document.activeElement).toBe(inputInstance._hostElement); }); + + it('should not break on when the page layout changes', () => { + sliderInstance._elementRef.nativeElement.style.marginLeft = '100px'; + setValueByClick(sliderInstance, 10, platform.IOS); + expect(inputInstance.value).toBe(10); + sliderInstance._elementRef.nativeElement.style.marginLeft = 'initial'; + }); }); describe('standard range slider', () => { @@ -1891,6 +1898,7 @@ function setValueByClick(slider: MatSlider, value: number, isIOS: boolean) { const sliderElement = slider._elementRef.nativeElement; const {x, y} = getCoordsForValue(slider, value); + dispatchPointerEvent(sliderElement, 'mouseenter', x, y); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, x, y, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, x, y, isIOS); } @@ -1901,6 +1909,7 @@ function slideToValue(slider: MatSlider, value: number, thumbPosition: Thumb, is const {x: startX, y: startY} = getCoordsForValue(slider, slider._getInput(thumbPosition).value); const {x: endX, y: endY} = getCoordsForValue(slider, value); + dispatchPointerEvent(sliderElement, 'mouseenter', startX, startY); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_DOWN, startX, startY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_MOVE, endX, endY, isIOS); dispatchPointerOrTouchEvent(sliderElement, PointerEventType.POINTER_UP, endX, endY, isIOS); @@ -1921,11 +1930,11 @@ function getCoordsForValue(slider: MatSlider, value: number): Point { /** Dispatch a pointerdown or pointerup event if supported, otherwise dispatch the touch event. */ function dispatchPointerOrTouchEvent( node: Node, type: PointerEventType, x: number, y: number, isIOS: boolean) { - if (isIOS) { - dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y); - } else { - dispatchPointerEvent(node, type, x, y); - } + if (isIOS) { + dispatchTouchEvent(node, pointerEventTypeToTouchEventType(type), x, y, x, y); + } else { + dispatchPointerEvent(node, type, x, y); + } } /** Returns the touch event equivalent of the given pointer event. */ diff --git a/src/material-experimental/mdc-slider/slider.ts b/src/material-experimental/mdc-slider/slider.ts index 54459b694c54..44a70e413d88 100644 --- a/src/material-experimental/mdc-slider/slider.ts +++ b/src/material-experimental/mdc-slider/slider.ts @@ -626,6 +626,16 @@ export class MatSlider extends _MatSliderMixinBase /** The display value of the end thumb. */ _endValueIndicatorText: string; + /** + * Whether the browser supports pointer events. + * + * We exclude iOS to mirror the MDC Foundation. The MDC Foundation cannot use pointer events on + * iOS because of this open bug - https://bugs.webkit.org/show_bug.cgi?id=220196. + */ + private _SUPPORTS_POINTER_EVENTS = typeof PointerEvent !== 'undefined' + && !!PointerEvent + && !this._platform.IOS; + /** Subscription to changes to the directionality (LTR / RTL) context for the application. */ private _dirChangeSubscription: Subscription; @@ -643,6 +653,7 @@ export class MatSlider extends _MatSliderMixinBase this._document = document; this._window = this._document.defaultView || window; this._dirChangeSubscription = this._dir.change.subscribe(() => this._onDirChange()); + this._attachUISyncEventListener(); } ngAfterViewInit() { @@ -676,6 +687,7 @@ export class MatSlider extends _MatSliderMixinBase this._foundation.destroy(); } this._dirChangeSubscription.unsubscribe(); + this._removeUISyncEventListener(); } /** Returns true if the language direction for this slider element is right to left. */ @@ -683,6 +695,40 @@ export class MatSlider extends _MatSliderMixinBase return this._dir && this._dir.value === 'rtl'; } + /** + * Attaches an event listener that keeps sync the slider UI and the foundation in sync. + * + * Because the MDC Foundation stores the value of the bounding client rect when layout is called, + * we need to keep calling layout to avoid the position of the slider getting out of sync with + * what the foundation has stored. If we don't do this, the foundation will not be able to + * correctly calculate the slider value on click/slide. + */ + _attachUISyncEventListener(): void { + // Implementation detail: It may seem weird that we are using "mouseenter" instead of + // "mousedown" as the default for when a browser does not support pointer events. While we + // would prefer to use "mousedown" as the default, for some reason it does not work (the + // callback is never triggered). + if (this._SUPPORTS_POINTER_EVENTS) { + this._elementRef.nativeElement.addEventListener('pointerdown', this._layout); + } else { + this._elementRef.nativeElement.addEventListener('mouseenter', this._layout); + this._elementRef.nativeElement.addEventListener('touchstart', this._layout); + } + } + + /** Removes the event listener that keeps sync the slider UI and the foundation in sync. */ + _removeUISyncEventListener(): void { + if (this._SUPPORTS_POINTER_EVENTS) { + this._elementRef.nativeElement.removeEventListener('pointerdown', this._layout); + } else { + this._elementRef.nativeElement.removeEventListener('mouseenter', this._layout); + this._elementRef.nativeElement.removeEventListener('touchstart', this._layout); + } + } + + /** Wrapper function for calling layout (needed for adding & removing an event listener). */ + private _layout = () => this._foundation.layout(); + /** * Reinitializes the slider foundation and input state(s). *