Skip to content

Commit e97d98b

Browse files
committed
fix(material/slider): slider tx imprecision (#28283)
* Fixes a bug where the slider's min and max value could go beyond the first and last tick marks. This caused the slider thumb to never truly line up with the tick marks except at the exact center of the slider track. (cherry picked from commit 5c7674a)
1 parent 66a3232 commit e97d98b

File tree

5 files changed

+44
-41
lines changed

5 files changed

+44
-41
lines changed

src/material/slider/slider-input.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
123123
*/
124124
get translateX(): number {
125125
if (this._slider.min >= this._slider.max) {
126-
this._translateX = 0;
126+
this._translateX = this._tickMarkOffset;
127127
return this._translateX;
128128
}
129129
if (this._translateX === undefined) {
@@ -209,6 +209,9 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
209209
/** The radius of a native html slider's knob. */
210210
_knobRadius: number = 8;
211211

212+
/** The distance in px from the start of the slider track to the first tick mark. */
213+
_tickMarkOffset = 3;
214+
212215
/** Whether user's cursor is currently in a mouse down state on the input. */
213216
_isActive: boolean = false;
214217

@@ -483,15 +486,22 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
483486
}
484487

485488
_clamp(v: number): number {
486-
return Math.max(Math.min(v, this._slider._cachedWidth), 0);
489+
const min = this._tickMarkOffset;
490+
const max = this._slider._cachedWidth - this._tickMarkOffset;
491+
return Math.max(Math.min(v, max), min);
487492
}
488493

489494
_calcTranslateXByValue(): number {
490495
if (this._slider._isRtl) {
491-
return (1 - this.percentage) * this._slider._cachedWidth;
496+
return (
497+
(1 - this.percentage) * (this._slider._cachedWidth - this._tickMarkOffset * 2) +
498+
this._tickMarkOffset
499+
);
492500
}
493-
const tickMarkOffset = 3; // The spaces before & after the start & end tick marks.
494-
return this.percentage * (this._slider._cachedWidth - tickMarkOffset * 2) + tickMarkOffset;
501+
return (
502+
this.percentage * (this._slider._cachedWidth - this._tickMarkOffset * 2) +
503+
this._tickMarkOffset
504+
);
495505
}
496506

497507
_calcTranslateXByPointerEvent(event: PointerEvent): number {
@@ -502,19 +512,18 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
502512
* Used to set the slider width to the correct
503513
* dimensions while the user is dragging.
504514
*/
505-
_updateWidthActive(): void {
506-
this._hostElement.style.padding = `0 ${this._slider._inputPadding}px`;
507-
this._hostElement.style.width = `calc(100% + ${this._slider._inputPadding}px)`;
508-
}
515+
_updateWidthActive(): void {}
509516

510517
/**
511518
* Sets the slider input to disproportionate dimensions to allow for touch
512519
* events to be captured on touch devices.
513520
*/
514521
_updateWidthInactive(): void {
515-
this._hostElement.style.padding = '0px';
516-
this._hostElement.style.width = 'calc(100% + 48px)';
517-
this._hostElement.style.left = '-24px';
522+
this._hostElement.style.padding = `0 ${this._slider._inputPadding}px`;
523+
this._hostElement.style.width = `calc(100% + ${
524+
this._slider._inputPadding - this._tickMarkOffset * 2
525+
}px)`;
526+
this._hostElement.style.left = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
518527
}
519528

520529
_updateThumbUIByValue(options?: {withAnimation: boolean}): void {
@@ -609,7 +618,7 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan
609618
if (!this._isLeftThumb && sibling) {
610619
return sibling.translateX;
611620
}
612-
return 0;
621+
return this._tickMarkOffset;
613622
}
614623

615624
/**
@@ -621,7 +630,7 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan
621630
if (this._isLeftThumb && sibling) {
622631
return sibling.translateX;
623632
}
624-
return this._slider._cachedWidth;
633+
return this._slider._cachedWidth - this._tickMarkOffset;
625634
}
626635

627636
_setIsLeftThumb(): void {
@@ -717,7 +726,8 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan
717726

718727
override _updateWidthActive(): void {
719728
const minWidth = this._slider._rippleRadius * 2 - this._slider._inputPadding * 2;
720-
const maxWidth = this._slider._cachedWidth + this._slider._inputPadding - minWidth;
729+
const maxWidth =
730+
this._slider._cachedWidth + this._slider._inputPadding - minWidth - this._tickMarkOffset * 2;
721731
const percentage =
722732
this._slider.min < this._slider.max
723733
? (this.max - this.min) / (this._slider.max - this._slider.min)
@@ -732,7 +742,7 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan
732742
if (!sibling) {
733743
return;
734744
}
735-
const maxWidth = this._slider._cachedWidth;
745+
const maxWidth = this._slider._cachedWidth - this._tickMarkOffset * 2;
736746
const midValue = this._isEndThumb
737747
? this.value - (this.value - sibling.value) / 2
738748
: this.value + (sibling.value - this.value) / 2;
@@ -760,11 +770,11 @@ export class MatSliderRangeThumb extends MatSliderThumb implements _MatSliderRan
760770
this._hostElement.style.padding = '0px';
761771

762772
if (this._isLeftThumb) {
763-
this._hostElement.style.left = '-24px';
773+
this._hostElement.style.left = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
764774
this._hostElement.style.right = 'auto';
765775
} else {
766776
this._hostElement.style.left = 'auto';
767-
this._hostElement.style.right = '-24px';
777+
this._hostElement.style.right = `-${this._slider._rippleRadius - this._tickMarkOffset}px`;
768778
}
769779
}
770780

src/material/slider/slider-interface.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,6 @@ export interface _MatSlider {
121121
*/
122122
_inputPadding: number;
123123

124-
/**
125-
* The offset represents left most translateX of the slider knob. Inversely,
126-
* (slider width - offset) = the right most translateX of the slider knob.
127-
*
128-
* Note:
129-
* * The native slider knob differs from the visual slider. It's knob cannot slide past
130-
* the end of the track AT ALL.
131-
* * The visual slider knob CAN slide past the end of the track slightly. It's knob can slide
132-
* past the end of the track such that it's center lines up with the end of the track.
133-
*/
134-
_inputOffset: number;
135-
136124
/** The radius of the visual slider's ripple. */
137125
_rippleRadius: number;
138126

src/material/slider/slider.spec.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,23 @@ describe('MDC-based MatSlider', () => {
6060
expect(input.max).withContext('max').toBe(max);
6161
expect(input.value).withContext('value').toBe(value);
6262

63-
// Note: This ±6 is here to account for the slight shift of the slider
64-
// thumb caused by the tick marks being 3px away from the track start
65-
// and end.
63+
// The discrepancy between the "ideal" and "actual" translateX comes from
64+
// the 3px offset from the start & end of the slider track to the first
65+
// and last tick marks.
6666
//
67-
// This check is meant to ensure the "ideal" estimate is within 3px of the
68-
// actual slider thumb position.
69-
expect(input.translateX - 6 < translateX && input.translateX + 6 > translateX)
67+
// The "actual" translateX is calculated based on a slider that is 6px
68+
// smaller than the width of the slider. Using this "actual" translateX in
69+
// tests would make it even more difficult than it already is to tell if
70+
// the translateX is off, so we abstract things in here so tests can be
71+
// more intuitive.
72+
//
73+
// The most clear way to compare the two tx's is to just turn them into
74+
// percentages by dividing by their (total height) / 100.
75+
const idealTXPercentage = Math.round(translateX / 3);
76+
const actualTXPercentage = Math.round((input.translateX - 3) / 2.94);
77+
expect(actualTXPercentage)
7078
.withContext(`translateX: ${input.translateX} should be close to ${translateX}`)
71-
.toBeTrue();
79+
.toBe(idealTXPercentage);
7280
if (step !== undefined) {
7381
expect(input.step).withContext('step').toBe(step);
7482
}

src/material/slider/slider.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,6 @@ export class MatSlider
427427
_knobRadius: number = 8;
428428

429429
_inputPadding: number;
430-
_inputOffset: number;
431430

432431
ngAfterViewInit(): void {
433432
if (this._platform.isBrowser) {
@@ -450,7 +449,6 @@ export class MatSlider
450449
const thumb = this._getThumb(_MatThumb.END);
451450
this._rippleRadius = thumb._ripple.radius;
452451
this._inputPadding = this._rippleRadius - this._knobRadius;
453-
this._inputOffset = this._knobRadius;
454452

455453
this._isRange
456454
? this._initUIRange(eInput as _MatSliderRangeThumb, sInput as _MatSliderRangeThumb)

tools/public_api_guard/material/slider.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ export class MatSlider extends _MatSliderMixinBase implements AfterViewInit, Can
5353
_hasAnimation: boolean;
5454
_input: _MatSliderThumb;
5555
// (undocumented)
56-
_inputOffset: number;
57-
// (undocumented)
5856
_inputPadding: number;
5957
_inputs: QueryList<_MatSliderRangeThumb>;
6058
_isCursorOnSliderThumb(event: PointerEvent, rect: DOMRect): boolean;
@@ -254,6 +252,7 @@ export class MatSliderThumb implements _MatSliderThumb, OnDestroy, ControlValueA
254252
get step(): number;
255253
set step(v: NumberInput);
256254
thumbPosition: _MatThumb;
255+
_tickMarkOffset: number;
257256
get translateX(): number;
258257
set translateX(v: number);
259258
// (undocumented)

0 commit comments

Comments
 (0)