Skip to content

Commit 675b29a

Browse files
committed
fix(material/slider): match active & focus state on IOS (#27546)
* fix(material/slider): match active & focus state on IOS * On IOS, the slider is only draggable if the pointer event happens directly on the slider thumb * On IOS, the slider never receives focus from pointer events * fixup! fix(material/slider): match active & focus state on IOS * fixup! fix(material/slider): match active & focus state on IOS * fixup! fix(material/slider): match active & focus state on IOS * fixup! fix(material/slider): match active & focus state on IOS * fixup! fix(material/slider): match active & focus state on IOS (cherry picked from commit 776e530)
1 parent 18e537a commit 675b29a

File tree

6 files changed

+50
-22
lines changed

6 files changed

+50
-22
lines changed

src/material/slider/slider-input.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ElementRef,
1919
EventEmitter,
2020
forwardRef,
21+
inject,
2122
Inject,
2223
Input,
2324
NgZone,
@@ -36,6 +37,7 @@ import {
3637
MAT_SLIDER_THUMB,
3738
MAT_SLIDER,
3839
} from './slider-interface';
40+
import {Platform} from '@angular/cdk/platform';
3941

4042
/**
4143
* Provider that allows the slider thumb to register as a ControlValueAccessor.
@@ -259,6 +261,8 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
259261
*/
260262
protected _isControlInitialized = false;
261263

264+
private _platform = inject(Platform);
265+
262266
constructor(
263267
readonly _ngZone: NgZone,
264268
readonly _elementRef: ElementRef<HTMLInputElement>,
@@ -363,8 +367,22 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
363367
return;
364368
}
365369

366-
this._isActive = true;
367-
this._setIsFocused(true);
370+
// On IOS, dragging only works if the pointer down happens on the
371+
// slider thumb and the slider does not receive focus from pointer events.
372+
if (this._platform.IOS) {
373+
const isCursorOnSliderThumb = this._slider._isCursorOnSliderThumb(
374+
event,
375+
this._slider._getThumb(this.thumbPosition)._hostElement.getBoundingClientRect(),
376+
);
377+
378+
if (isCursorOnSliderThumb) {
379+
this._isActive = true;
380+
}
381+
} else {
382+
this._isActive = true;
383+
this._setIsFocused(true);
384+
}
385+
368386
this._updateWidthActive();
369387
this._slider._updateDimensions();
370388

@@ -452,7 +470,12 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
452470
if (this._isActive) {
453471
this._isActive = false;
454472
this.dragEnd.emit({source: this, parent: this._slider, value: this.value});
455-
setTimeout(() => this._updateWidthInactive());
473+
474+
// This setTimeout is to prevent the pointerup from triggering a value
475+
// change on the input based on the inactive width. It's not clear why
476+
// but for some reason on IOS this race condition is even more common so
477+
// the timeout needs to be increased.
478+
setTimeout(() => this._updateWidthInactive(), this._platform.IOS ? 10 : 0);
456479
}
457480
}
458481

src/material/slider/slider-interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export class MatSliderChange {
8282
}
8383

8484
export interface _MatSlider {
85+
/** Whether the given pointer event occurred within the bounds of the slider pointer's DOM Rect. */
86+
_isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect): boolean;
87+
8588
/** Gets the slider thumb input of the given thumb position. */
8689
_getInput(thumbPosition: _MatThumb): _MatSliderThumb | _MatSliderRangeThumb | undefined;
8790

src/material/slider/slider-thumb.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewIni
137137
}
138138

139139
const rect = this._hostElement.getBoundingClientRect();
140-
const isHovered = this._isSliderThumbHovered(event, rect);
140+
const isHovered = this._slider._isCursorOnSliderThumb(event, rect);
141141
this._isHovered = isHovered;
142142

143143
if (isHovered) {
@@ -299,13 +299,4 @@ export class MatSliderVisualThumb implements _MatSliderVisualThumb, AfterViewIni
299299
this._isShowingRipple(this._activeRippleRef)
300300
);
301301
}
302-
303-
private _isSliderThumbHovered(event: PointerEvent, rect: DOMRect) {
304-
const radius = rect.width / 2;
305-
const centerX = rect.x + radius;
306-
const centerY = rect.y + radius;
307-
const dx = event.clientX - centerX;
308-
const dy = event.clientY - centerY;
309-
return Math.pow(dx, 2) + Math.pow(dy, 2) < Math.pow(radius, 2);
310-
}
311302
}

src/material/slider/slider.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,8 @@ describe('MDC-based MatSlider', () => {
891891
it('should set the aria-valuetext attribute with the given `displayWith` function', fakeAsync(() => {
892892
expect(input._hostElement.getAttribute('aria-valuetext')).toBe('$1');
893893
setValueByClick(slider, input, 199);
894+
fixture.detectChanges();
895+
flush();
894896
expect(input._hostElement.getAttribute('aria-valuetext')).toBe('$199');
895897
}));
896898

@@ -1177,7 +1179,6 @@ describe('MDC-based MatSlider', () => {
11771179
const startInput = slider._getInput(_MatThumb.START) as MatSliderRangeThumb;
11781180
const endInput = slider._getInput(_MatThumb.END) as MatSliderRangeThumb;
11791181
flush();
1180-
console.log('result: ', startInput.value);
11811182
checkInput(startInput, {min: -1, max: -0.3, value: -0.7, translateX: 90});
11821183
checkInput(endInput, {min: -0.7, max: 0, value: -0.3, translateX: 210});
11831184
}));
@@ -1733,7 +1734,7 @@ function slideToValue(slider: MatSlider, input: MatSliderThumb, value: number) {
17331734
dispatchEvent(input._hostElement, new Event('input'));
17341735
dispatchPointerEvent(sliderElement, 'pointerup', endX, endY);
17351736
dispatchEvent(input._hostElement, new Event('change'));
1736-
tick();
1737+
tick(10);
17371738
}
17381739

17391740
/** Returns the x and y coordinates for the given slider value. */

src/material/slider/slider.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ContentChild,
2323
ContentChildren,
2424
ElementRef,
25+
inject,
2526
Inject,
2627
Input,
2728
NgZone,
@@ -404,10 +405,11 @@ export class MatSlider
404405

405406
private _resizeTimer: null | ReturnType<typeof setTimeout> = null;
406407

408+
private _platform = inject(Platform);
409+
407410
constructor(
408411
readonly _ngZone: NgZone,
409412
readonly _cdr: ChangeDetectorRef,
410-
readonly _platform: Platform,
411413
elementRef: ElementRef<HTMLElement>,
412414
@Optional() readonly _dir: Directionality,
413415
@Optional()
@@ -930,12 +932,22 @@ export class MatSlider
930932
}
931933

932934
_setTransition(withAnimation: boolean): void {
933-
this._hasAnimation = withAnimation && !this._noopAnimations;
935+
this._hasAnimation = !this._platform.IOS && withAnimation && !this._noopAnimations;
934936
this._elementRef.nativeElement.classList.toggle(
935937
'mat-mdc-slider-with-animation',
936938
this._hasAnimation,
937939
);
938940
}
941+
942+
/** Whether the given pointer event occurred within the bounds of the slider pointer's DOM Rect. */
943+
_isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect) {
944+
const radius = rect.width / 2;
945+
const centerX = rect.x + radius;
946+
const centerY = rect.y + radius;
947+
const dx = event.clientX - centerX;
948+
const dy = event.clientY - centerY;
949+
return Math.pow(dx, 2) + Math.pow(dy, 2) < Math.pow(radius, 2);
950+
}
939951
}
940952

941953
/** Ensures that there is not an invalid configuration for the slider thumb inputs. */

tools/public_api_guard/material/slider.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ import { MatRipple } from '@angular/material/core';
2222
import { NgZone } from '@angular/core';
2323
import { NumberInput } from '@angular/cdk/coercion';
2424
import { OnDestroy } from '@angular/core';
25-
import { Platform } from '@angular/cdk/platform';
2625
import { QueryList } from '@angular/core';
2726
import { RippleGlobalOptions } from '@angular/material/core';
2827
import { Subject } from 'rxjs';
2928

3029
// @public
3130
export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, CanDisableRipple, OnDestroy, _MatSlider {
32-
constructor(_ngZone: NgZone, _cdr: ChangeDetectorRef, _platform: Platform, elementRef: ElementRef<HTMLElement>, _dir: Directionality, _globalRippleOptions?: RippleGlobalOptions | undefined, animationMode?: string);
31+
constructor(_ngZone: NgZone, _cdr: ChangeDetectorRef, elementRef: ElementRef<HTMLElement>, _dir: Directionality, _globalRippleOptions?: RippleGlobalOptions | undefined, animationMode?: string);
3332
// (undocumented)
3433
_cachedLeft: number;
3534
// (undocumented)
@@ -59,6 +58,7 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, Can
5958
// (undocumented)
6059
_inputPadding: number;
6160
_inputs: QueryList<_MatSliderRangeThumb>;
61+
_isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect): boolean;
6262
// (undocumented)
6363
_isRange: boolean;
6464
_isRtl: boolean;
@@ -85,8 +85,6 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, Can
8585
// (undocumented)
8686
_onValueChange(source: _MatSliderThumb): void;
8787
// (undocumented)
88-
readonly _platform: Platform;
89-
// (undocumented)
9088
_rippleRadius: number;
9189
_setTrackActiveStyles(styles: {
9290
left: string;
@@ -115,7 +113,7 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, Can
115113
// (undocumented)
116114
static ɵcmp: i0.ɵɵComponentDeclaration<MatSlider, "mat-slider", ["matSlider"], { "color": { "alias": "color"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "discrete": { "alias": "discrete"; "required": false; }; "showTickMarks": { "alias": "showTickMarks"; "required": false; }; "min": { "alias": "min"; "required": false; }; "max": { "alias": "max"; "required": false; }; "step": { "alias": "step"; "required": false; }; "displayWith": { "alias": "displayWith"; "required": false; }; }, {}, ["_input", "_inputs"], ["*"], false, never>;
117115
// (undocumented)
118-
static ɵfac: i0.ɵɵFactoryDeclaration<MatSlider, [null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }]>;
116+
static ɵfac: i0.ɵɵFactoryDeclaration<MatSlider, [null, null, null, { optional: true; }, { optional: true; }, { optional: true; }]>;
119117
}
120118

121119
// @public @deprecated

0 commit comments

Comments
 (0)