Skip to content

Commit

Permalink
Add new state for unrounded percentageValues
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Jan 3, 2025
1 parent 961bdad commit 4235041
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 87 deletions.
4 changes: 3 additions & 1 deletion packages/react/src/slider/control/SliderControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const testRootContext: SliderRootContext = {
disabled: false,
getFingerState: () => ({
value: 0,
closestThumbIndex: 0,
percentageValue: 0,
percentageValues: [0],
thumbIndex: 0,
}),
setValue: NOOP,
largeStep: 10,
Expand Down Expand Up @@ -42,6 +43,7 @@ const testRootContext: SliderRootContext = {
registerSliderControl: NOOP,
setActive: NOOP,
setDragging: NOOP,
setPercentageValues: NOOP,
setThumbMap: NOOP,
step: 1,
tabIndex: null,
Expand Down
20 changes: 8 additions & 12 deletions packages/react/src/slider/control/useSliderControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ export function useSliderControl(
return;
}

focusThumb(finger.closestThumbIndex, controlRef, setActive);
focusThumb(finger.thumbIndex, controlRef, setActive);

if (validateMinimumDistance(finger.value, step, minStepsBetweenValues)) {
if (!dragging && moveCountRef.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) {
setDragging(true);
}

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

Expand Down Expand Up @@ -130,9 +130,9 @@ export function useSliderControl(
return;
}

focusThumb(finger.closestThumbIndex, controlRef, setActive);
focusThumb(finger.thumbIndex, controlRef, setActive);

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

moveCountRef.current = 0;
Expand Down Expand Up @@ -203,20 +203,16 @@ export function useSliderControl(
return;
}

focusThumb(finger.closestThumbIndex, controlRef, setActive);
focusThumb(finger.thumbIndex, controlRef, setActive);

// if the event lands on a thumb, don't change the value, just get the
// percentageValue difference represented by the distance between the click origin
// and the coordinates of the value on the track area
if (thumbRefs.current.includes(event.target as HTMLElement)) {
const targetThumbIndex = (event.target as HTMLElement).getAttribute('data-index');

const offset =
percentageValues[Number(targetThumbIndex)] / 100 - finger.percentageValue;

offsetRef.current = offset;
offsetRef.current =
percentageValues[finger.thumbIndex] / 100 - finger.percentageValue;
} else {
setValue(finger.value, finger.closestThumbIndex, event.nativeEvent);
setValue(finger.value, finger.percentageValues, finger.thumbIndex, event.nativeEvent);
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/slider/indicator/SliderIndicator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ const testRootContext: SliderRootContext = {
disabled: false,
getFingerState: () => ({
value: 0,
closestThumbIndex: 0,
percentageValue: 0,
percentageValues: [0],
thumbIndex: 0,
}),
setValue: NOOP,
largeStep: 10,
Expand Down Expand Up @@ -42,6 +43,7 @@ const testRootContext: SliderRootContext = {
registerSliderControl: NOOP,
setActive: NOOP,
setDragging: NOOP,
setPercentageValues: NOOP,
setThumbMap: NOOP,
step: 1,
tabIndex: null,
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/slider/root/SliderRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export namespace SliderRoot {
/**
* The raw number value of the slider.
*/
values: ReadonlyArray<number>;
values: readonly number[];
}

export interface Props
Expand All @@ -182,7 +182,7 @@ export namespace SliderRoot {
*
* To render a controlled slider, use the `value` prop instead.
*/
defaultValue?: number | ReadonlyArray<number>;
defaultValue?: number | readonly number[];
/**
* Whether the component should ignore user interaction.
* @default false
Expand All @@ -200,7 +200,7 @@ export namespace SliderRoot {
* The value of the slider.
* For ranged sliders, provide an array with two values.
*/
value?: number | ReadonlyArray<number>;
value?: number | readonly number[];
}
}

Expand Down
138 changes: 93 additions & 45 deletions packages/react/src/slider/root/useSliderRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { useFieldControlValidation } from '../../field/control/useFieldControlVa
import { asc } from '../utils/asc';
import { getSliderValue } from '../utils/getSliderValue';
import { percentToValue } from '../utils/percentToValue';
import { replaceArrayItemAtIndex } from '../utils/replaceArrayItemAtIndex';
import { roundValueToStep } from '../utils/roundValueToStep';
import { setValueIndex } from '../utils/setValueIndex';
import { ThumbMetadata } from '../thumb/useSliderThumb';

function areValuesEqual(
Expand All @@ -35,7 +35,7 @@ function areValuesEqual(
return false;
}

function findClosest(values: number[], currentValue: number) {
function findClosest(values: readonly number[], currentValue: number) {
const { index: closestIndex } =
values.reduce<{ distance: number; index: number } | null>(
(acc, value: number, index: number) => {
Expand Down Expand Up @@ -208,14 +208,41 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
[inputValidationRef],
);

const range = Array.isArray(valueUnwrapped);

const values = React.useMemo(() => {
return (range ? valueUnwrapped.slice().sort(asc) : [valueUnwrapped]).map((val) =>
val == null ? min : clamp(val, min, max),
);
}, [max, min, range, valueUnwrapped]);

function initializePercentageValues() {
// console.log('initializePercentageValues');
const vals = [];
for (let i = 0; i < values.length; i += 1) {
vals.push(valueToPercent(values[i], min, max));
}
return vals;
}

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

const setValue = useEventCallback(
(newValue: number | readonly number[], thumbIndex: number, event: Event) => {
(
newValue: number | readonly number[],
newPercentageValues: readonly number[],
thumbIndex: number,
event: Event,
) => {
if (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 @@ -232,14 +259,6 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
},
);

const range = Array.isArray(valueUnwrapped);

const values = React.useMemo(() => {
return (range ? valueUnwrapped.slice().sort(asc) : [valueUnwrapped]).map((val) =>
val == null ? min : clamp(val, min, max),
);
}, [max, min, range, valueUnwrapped]);

const handleRootRef = useForkRef(rootRef, sliderRef);

const handleInputChange = useEventCallback(
Expand All @@ -258,7 +277,20 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
}

if (validateMinimumDistance(newValue, step, minStepsBetweenValues)) {
setValue(newValue, index, event.nativeEvent);
if (Array.isArray(newValue)) {
setValue(
newValue,
replaceArrayItemAtIndex(
percentageValues,
index,
valueToPercent(newValue[index], min, max),
),
index,
event.nativeEvent,
);
} else {
setValue(newValue, [valueToPercent(newValue, min, max)], index, event.nativeEvent);
}
setDirty(newValue !== validityData.initialValue);
setTouched(true);
commitValidation(newValue);
Expand All @@ -272,12 +304,14 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
const getFingerState = useEventCallback(
(
fingerPosition: FingerPosition | null,

/**
* When `true`, closestThumbIndexRef is updated.
* It's `true` when called by touchstart or pointerdown.
*/
shouldCaptureThumbIndex: boolean = false,
/**
* The pixel distance between the finger origin and the center of the thumb.
*/
offset: number = 0,
) => {
if (fingerPosition == null) {
Expand All @@ -294,30 +328,29 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
const isVertical = orientation === 'vertical';

const { width, height, bottom, left } = sliderControl.getBoundingClientRect();
let percent;

if (isVertical) {
percent = (bottom - fingerPosition.y) / height + offset;
} else {
percent = (fingerPosition.x - left) / width + offset * (isRtl ? -1 : 1);
}
// percent is a value between 0 and 1, e.g. "41%" is `0.41`
const valueRescaled = isVertical
? (bottom - fingerPosition.y) / height + offset
: (fingerPosition.x - left) / width + offset * (isRtl ? -1 : 1);

percent = Math.min(percent, 1);
let percentageValue = Math.min(valueRescaled, 1);

if (isRtl && !isVertical) {
percent = 1 - percent;
}

let newValue;
newValue = percentToValue(percent, min, max);
if (step) {
newValue = roundValueToStep(newValue, step, min);
percentageValue = 1 - percentageValue;
}

let newValue = percentToValue(percentageValue, min, max);
newValue = roundValueToStep(newValue, step, min);
newValue = clamp(newValue, min, max);

if (!range) {
return { value: newValue, percentageValue: percent, closestThumbIndex: 0 };
return {
value: newValue,
percentageValue,
percentageValues: [percentageValue * 100],
thumbIndex: 0,
};
}

if (shouldCaptureThumbIndex) {
Expand All @@ -333,13 +366,16 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
values[closestThumbIndex + 1] - minStepsBetweenValues || Infinity,
);

newValue = setValueIndex({
values,
newValue,
index: closestThumbIndex,
});

return { value: newValue, percentageValue: percent, closestThumbIndex };
return {
value: replaceArrayItemAtIndex(values, closestThumbIndex, newValue),
percentageValue,
percentageValues: replaceArrayItemAtIndex(
percentageValues,
closestThumbIndex,
percentageValue * 100,
),
thumbIndex: closestThumbIndex,
};
},
);

Expand Down Expand Up @@ -386,11 +422,12 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
name,
onValueCommitted,
orientation,
percentageValues: values.map((v) => valueToPercent(v, min, max)),
percentageValues,
range,
registerSliderControl,
setActive,
setDragging,
setPercentageValues,
setThumbMap,
setValue,
step,
Expand All @@ -414,10 +451,12 @@ export function useSliderRoot(parameters: useSliderRoot.Parameters): useSliderRo
name,
onValueCommitted,
orientation,
percentageValues,
range,
registerSliderControl,
setActive,
setDragging,
setPercentageValues,
setThumbMap,
setValue,
step,
Expand Down Expand Up @@ -448,7 +487,7 @@ export namespace useSliderRoot {
/**
* The default value. Use when the component is not controlled.
*/
defaultValue?: number | ReadonlyArray<number>;
defaultValue?: number | readonly number[];
/**
* Sets the direction. For right-to-left languages, the lowest value is on the right-hand side.
* @default 'ltr'
Expand Down Expand Up @@ -526,7 +565,7 @@ export namespace useSliderRoot {
* The value of the slider.
* For ranged sliders, provide an array with two values.
*/
value?: number | ReadonlyArray<number>;
value?: number | readonly number[];
}

export interface ReturnValue {
Expand All @@ -548,17 +587,23 @@ export namespace useSliderRoot {
disabled: boolean;
getFingerState: (
fingerPosition: FingerPosition | null,
move?: boolean,
shouldCaptureThumbIndex?: boolean,
offset?: number,
) => {
value: number | number[];
percentageValue: number;
closestThumbIndex: number;
percentageValues: number[];
thumbIndex: number;
} | null;
/**
* Callback to invoke change handlers after internal value state is updated.
*/
setValue: (newValue: number | readonly number[], activeThumb: number, event: Event) => void;
setValue: (
newValue: number | readonly number[],
newPercentageValue: readonly number[],
activeThumb: number,
event: Event,
) => void;
/**
* The large step value of the slider when incrementing or decrementing while the shift key is held,
* or when using Page-Up or Page-Down keys. Snaps to multiples of this value.
Expand Down Expand Up @@ -589,9 +634,12 @@ export namespace useSliderRoot {
* The value(s) of the slider as percentages
*/
percentageValues: readonly number[];
setActive: (activeIndex: number) => void;
setDragging: (isDragging: boolean) => void;
setThumbMap: (map: Map<Node, CompositeMetadata<ThumbMetadata> | null>) => void;
setActive: React.Dispatch<React.SetStateAction<number>>;
setDragging: React.Dispatch<React.SetStateAction<boolean>>;
setPercentageValues: React.Dispatch<React.SetStateAction<readonly number[]>>;
setThumbMap: React.Dispatch<
React.SetStateAction<Map<Node, CompositeMetadata<ThumbMetadata> | null>>
>;
/**
* 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

0 comments on commit 4235041

Please sign in to comment.