Skip to content

[Slider] Position thumb based on value instead of pointer location when dragging #1750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/reference/generated/slider-thumb.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@
"onPointerOver": {
"type": "PointerEventHandler<Element>"
},
"percentageValues": {
"type": "number[]",
"description": "The value(s) of the slider as percentages"
},
"tabIndex": {
"type": "number | null",
"default": "null",
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/slider/control/SliderControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ const SliderControl = React.forwardRef(function SliderControl(
dragging,
getFingerState,
lastChangedValueRef,
max,
min,
minStepsBetweenValues,
percentageValues,
registerSliderControl,
setActive,
setDragging,
setValue,
state,
step,
thumbRefs,
values,
} = useSliderRootContext();

const { getRootProps } = useSliderControl({
Expand All @@ -42,15 +44,17 @@ const SliderControl = React.forwardRef(function SliderControl(
dragging,
getFingerState,
lastChangedValueRef,
max,
min,
minStepsBetweenValues,
percentageValues,
registerSliderControl,
rootRef: forwardedRef,
setActive,
setDragging,
setValue,
step,
thumbRefs,
values,
});

const { renderElement } = useComponentRenderer({
Expand Down
26 changes: 14 additions & 12 deletions packages/react/src/slider/control/useSliderControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ownerDocument } from '../../utils/owner';
import type { GenericHTMLProps } from '../../utils/types';
import { useForkRef } from '../../utils/useForkRef';
import { useEventCallback } from '../../utils/useEventCallback';
import { valueToPercent } from '../../utils/valueToPercent';
import {
focusThumb,
validateMinimumDistance,
Expand Down Expand Up @@ -50,16 +51,18 @@ export function useSliderControl(
dragging,
getFingerState,
lastChangedValueRef,
max,
min,
minStepsBetweenValues,
commitValue,
percentageValues,
registerSliderControl,
rootRef: externalRef,
setActive,
setDragging,
setValue,
step,
thumbRefs,
values,
} = parameters;

const { commitValidation } = useFieldControlValidation();
Expand Down Expand Up @@ -107,7 +110,7 @@ export function useSliderControl(
setDragging(true);
}

setValue(finger.value, finger.percentageValues, finger.thumbIndex, nativeEvent);
setValue(finger.value, finger.thumbIndex, nativeEvent);
}
});

Expand Down Expand Up @@ -157,7 +160,7 @@ export function useSliderControl(

focusThumb(finger.thumbIndex, controlRef, setActive);

setValue(finger.value, finger.percentageValues, finger.thumbIndex, nativeEvent);
setValue(finger.value, finger.thumbIndex, nativeEvent);
}

moveCountRef.current = 0;
Expand Down Expand Up @@ -236,14 +239,9 @@ export function useSliderControl(
// and the coordinates of the value on the track area
if (thumbRefs.current.includes(event.target as HTMLElement)) {
offsetRef.current =
percentageValues[finger.thumbIndex] / 100 - finger.valueRescaled;
valueToPercent(values[finger.thumbIndex], min, max) / 100 - finger.valueRescaled;
} else {
setValue(
finger.value,
finger.percentageValues,
finger.thumbIndex,
event.nativeEvent,
);
setValue(finger.value, finger.thumbIndex, event.nativeEvent);
}
}

Expand All @@ -263,10 +261,12 @@ export function useSliderControl(
handleRootRef,
handleTouchMove,
handleTouchEnd,
max,
min,
setValue,
percentageValues,
setActive,
thumbRefs,
values,
],
);

Expand All @@ -286,15 +286,17 @@ export namespace useSliderControl {
| 'dragging'
| 'getFingerState'
| 'lastChangedValueRef'
| 'max'
| 'min'
| 'minStepsBetweenValues'
| 'commitValue'
| 'percentageValues'
| 'registerSliderControl'
| 'setActive'
| 'setDragging'
| 'setValue'
| 'step'
| 'thumbRefs'
| 'values'
> {
/**
* The ref attached to the control area of the Slider.
Expand Down
6 changes: 4 additions & 2 deletions packages/react/src/slider/indicator/SliderIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ const SliderIndicator = React.forwardRef(function SliderIndicator(
) {
const { render, className, ...otherProps } = props;

const { disabled, orientation, state, percentageValues } = useSliderRootContext();
const { disabled, max, min, orientation, state, values } = useSliderRootContext();

const { getRootProps } = useSliderIndicator({
disabled,
max,
min,
orientation,
percentageValues,
values,
});

const { renderElement } = useComponentRenderer({
Expand Down
11 changes: 8 additions & 3 deletions packages/react/src/slider/indicator/useSliderIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import { mergeProps } from '../../merge-props';
import type { GenericHTMLProps, Orientation } from '../../utils/types';
import type { useSliderRoot } from '../root/useSliderRoot';
import { valueArrayToPercentages, type useSliderRoot } from '../root/useSliderRoot';

function getRangeStyles(
orientation: Orientation,
Expand Down Expand Up @@ -31,7 +31,9 @@ function getRangeStyles(
export function useSliderIndicator(
parameters: useSliderIndicator.Parameters,
): useSliderIndicator.ReturnValue {
const { orientation, percentageValues } = parameters;
const { max, min, orientation, values } = parameters;

const percentageValues = valueArrayToPercentages(values.slice(), min, max);

let internalStyles: React.CSSProperties;

Expand Down Expand Up @@ -78,7 +80,10 @@ export function useSliderIndicator(

export namespace useSliderIndicator {
export interface Parameters
extends Pick<useSliderRoot.ReturnValue, 'disabled' | 'orientation' | 'percentageValues'> {}
extends Pick<
useSliderRoot.ReturnValue,
'disabled' | 'orientation' | 'values' | 'max' | 'min'
> {}

export interface ReturnValue {
getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
Expand Down
82 changes: 7 additions & 75 deletions packages/react/src/slider/root/useSliderRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { asc } from '../utils/asc';
import { getSliderValue } from '../utils/getSliderValue';
import { replaceArrayItemAtIndex } from '../utils/replaceArrayItemAtIndex';
import { roundValueToStep } from '../utils/roundValueToStep';
import { ThumbMetadata } from '../thumb/useSliderThumb';
import type { ThumbMetadata } from '../thumb/useSliderThumb';
import { useEventCallback } from '../../utils/useEventCallback';
import { SliderThumbDataAttributes } from '../thumb/SliderThumbDataAttributes';

Expand Down Expand Up @@ -57,7 +57,7 @@ function getClosestThumbIndex(values: readonly number[], currentValue: number, m
return closestIndex;
}

function valueArrayToPercentages(values: number[], min: number, max: number) {
export function valueArrayToPercentages(values: number[], min: number, max: number) {
const output = [];
for (let i = 0; i < values.length; i += 1) {
output.push(clamp(valueToPercent(values[i], min, max), 0, 100));
Expand Down Expand Up @@ -225,27 +225,13 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
return valueUnwrapped.slice().sort(asc);
}, [max, min, range, valueUnwrapped]);

function initializePercentageValues() {
return valueArrayToPercentages(values, min, max);
}

const [percentageValues, setPercentageValues] = React.useState<readonly number[]>(
initializePercentageValues,
);

const setValue = useEventCallback(
(
newValue: number | number[],
newPercentageValues: readonly number[],
thumbIndex: number,
event: Event,
) => {
(newValue: number | number[], thumbIndex: number, event: Event) => {
if (Number.isNaN(newValue) || areValuesEqual(newValue, valueUnwrapped)) {
return;
}

setValueUnwrapped(newValue);
setPercentageValues(newPercentageValues);
// Redefine target to allow name and value to be read.
// This allows seamless integration with the most popular form libraries.
// https://github.com/mui/material-ui/issues/13485#issuecomment-676048492
Expand All @@ -265,14 +251,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo

// for pointer drag only
const commitValue = useEventCallback((value: number | readonly number[], event: Event) => {
if (Array.isArray(value)) {
const newPercentageValues = valueArrayToPercentages(value, min, max);
if (!areArraysEqual(newPercentageValues, percentageValues)) {
setPercentageValues(newPercentageValues);
}
} else if (typeof value === 'number') {
setPercentageValues([valueToPercent(value, min, max)]);
}
onValueCommitted(value, event);
});

Expand All @@ -289,18 +267,9 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo

if (validateMinimumDistance(newValue, step, minStepsBetweenValues)) {
if (Array.isArray(newValue)) {
setValue(
newValue,
replaceArrayItemAtIndex(
percentageValues,
index,
valueToPercent(newValue[index], min, max),
),
index,
event.nativeEvent,
);
setValue(newValue, index, event.nativeEvent);
} else {
setValue(newValue, [valueToPercent(newValue, min, max)], index, event.nativeEvent);
setValue(newValue, index, event.nativeEvent);
}
setDirty(newValue !== validityData.initialValue);
setTouched(true);
Expand Down Expand Up @@ -367,7 +336,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
return {
value: newValue,
valueRescaled,
percentageValues: [valueRescaled * 100],
thumbIndex: 0,
};
}
Expand All @@ -378,7 +346,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo

const closestThumbIndex = closestThumbIndexRef.current ?? 0;
const minValueDifference = minStepsBetweenValues * step;
const minPercentageDifference = (minValueDifference * 100) / (max - min);

// Bound the new value to the thumb's neighbours.
newValue = clamp(
Expand All @@ -387,20 +354,9 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
values[closestThumbIndex + 1] - minValueDifference || Infinity,
);

const newPercentageValue = clamp(
valueRescaled * 100,
percentageValues[closestThumbIndex - 1] + minPercentageDifference || -Infinity,
percentageValues[closestThumbIndex + 1] - minPercentageDifference || Infinity,
);

return {
value: replaceArrayItemAtIndex(values, closestThumbIndex, newValue),
valueRescaled,
percentageValues: replaceArrayItemAtIndex(
percentageValues,
closestThumbIndex,
newPercentageValue,
),
thumbIndex: closestThumbIndex,
};
},
Expand All @@ -414,19 +370,7 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
if (min >= max) {
warn('Slider `max` must be greater than `min`');
}

if (typeof valueUnwrapped === 'number') {
const newPercentageValue = clamp(valueToPercent(valueUnwrapped, min, max), 0, 100);
if (newPercentageValue !== percentageValues[0] && !Number.isNaN(newPercentageValue)) {
setPercentageValues([newPercentageValue]);
}
} else if (Array.isArray(valueUnwrapped)) {
const newPercentageValues = valueArrayToPercentages(valueUnwrapped, min, max);
if (!areArraysEqual(newPercentageValues, percentageValues)) {
setPercentageValues(newPercentageValues);
}
}
}, [dragging, min, max, percentageValues, setPercentageValues, valueProp, valueUnwrapped]);
}, [dragging, min, max, valueProp]);

useEnhancedEffect(() => {
const activeEl = activeElement(ownerDocument(sliderRef.current));
Expand Down Expand Up @@ -475,7 +419,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
name,
onValueCommitted,
orientation,
percentageValues,
range,
registerSliderControl,
setActive,
Expand Down Expand Up @@ -504,7 +447,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
name,
onValueCommitted,
orientation,
percentageValues,
range,
registerSliderControl,
setActive,
Expand All @@ -527,7 +469,6 @@ export interface FingerPosition {
interface FingerState {
value: number | number[];
valueRescaled: number;
percentageValues: number[];
thumbIndex: number;
}

Expand Down Expand Up @@ -664,10 +605,6 @@ export namespace useSliderRoot {
* @default 'horizontal'
*/
orientation: Orientation;
/**
* The value(s) of the slider as percentages
*/
percentageValues: readonly number[];
registerSliderControl: (element: HTMLElement | null) => void;
setActive: React.Dispatch<React.SetStateAction<number>>;
setDragging: React.Dispatch<React.SetStateAction<boolean>>;
Expand All @@ -677,12 +614,7 @@ export namespace useSliderRoot {
/**
* Callback fired when dragging and invokes onValueChange.
*/
setValue: (
newValue: number | number[],
newPercentageValues: readonly number[],
activeThumb: number,
event: Event,
) => void;
setValue: (newValue: number | number[], activeThumb: number, event: Event) => void;
/**
* The step increment of the slider when incrementing or decrementing. It will snap
* to multiples of this value. Decimal values are supported.
Expand Down
Loading