Skip to content

Commit

Permalink
add ability to set custom step for Range slider
Browse files Browse the repository at this point in the history
  • Loading branch information
SerhiiTsybulskyi committed Feb 19, 2024
1 parent 4d6d2bd commit 744e7bf
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 17 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions src/form/Range/Range.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,34 @@ export const SingleDisabled = () => {
);
};

export const CustomFloatStep = () => {
const [state, setState] = useState<number>(3.5);
return (
<RangeSingle
onChange={setState}
min={0.5}
max={10}
step={0.5}
value={state}
style={{ width: 250, marginTop: 30 }}
/>
);
};

export const CustomIntegerStep = () => {
const [state, setState] = useState<number>(75);
return (
<RangeSingle
onChange={setState}
min={50}
max={250}
step={25}
value={state}
style={{ width: 250, marginTop: 30 }}
/>
);
};

export const Double = () => {
const [state, setState] = useState<[number, number]>([20, 40]);

Expand All @@ -52,6 +80,42 @@ export const Double = () => {
);
};

export const DoubleFloatStep = () => {
const [state, setState] = useState<[number, number]>([5, 10]);

const debounceRange = () =>
debounce((min: number, max: number) => setState([min, max]));

return (
<RangeDouble
onChange={debounceRange}
min={0.5}
max={12.5}
step={0.1}
value={state}
style={{ width: 250 }}
/>
);
};

export const DoubleIntegerStep = () => {
const [state, setState] = useState<[number, number]>([20, 40]);

const debounceRange = () =>
debounce((min: number, max: number) => setState([min, max]));

return (
<RangeDouble
onChange={debounceRange}
min={10}
max={50}
step={5}
value={state}
style={{ width: 250 }}
/>
);
};

export const DoubleDisabled = () => {
const [state, setState] = useState<[number, number]>([20, 40]);
return (
Expand Down
46 changes: 34 additions & 12 deletions src/form/Range/RangeDouble.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React, { useCallback, useEffect, useRef, useState, FC } from 'react';
import React, {
useCallback,
useEffect,
useRef,
useState,
FC,
useMemo
} from 'react';
import classNames from 'classnames';
import { motion, useMotionValue } from 'framer-motion';
import { RangeProps, RangeTooltip } from './RangeTooltip';
import css from './Range.module.css';

const MIN_VALUE_BETWEEN = 1;

export const RangeDouble: FC<RangeProps<[number, number]>> = ({
disabled,
style,
Expand All @@ -14,13 +19,15 @@ export const RangeDouble: FC<RangeProps<[number, number]>> = ({
min,
max,
value,
onChange
onChange,
step = 1
}) => {
const minValueBetween = step;
const [minValue, maxValue] = value;
const initialMinValue = Math.max(minValue, min);
const initalMaxValue = Math.min(
maxValue < initialMinValue + MIN_VALUE_BETWEEN
? initialMinValue + MIN_VALUE_BETWEEN
maxValue < initialMinValue + minValueBetween
? initialMinValue + minValueBetween
: maxValue,
max
);
Expand All @@ -35,41 +42,56 @@ export const RangeDouble: FC<RangeProps<[number, number]>> = ({
const minX = useMotionValue(0);
const maxX = useMotionValue(0);

const fractionDigits = useMemo(
() => step.toString()?.[1]?.length || 0,
[step]
);

const getValue = (xPosition: number): number => {
const draggedWidth = xPosition - rangeLeft;
const draggedWidthPercentage = (draggedWidth * 100) / rangeWidth;
return Math.round(min + ((max - min) * draggedWidthPercentage) / 100);

const scaledStep = (step / (max - min)) * 100;
const scaledValue =
Math.round(draggedWidthPercentage / scaledStep) * scaledStep;
const scaledValueWithStep = (scaledValue / 100) * (max - min) + min;
const rawValue = Math.round(scaledValueWithStep / step) * step;
// Fix floating point precision. Example 3.50000000000000004
const newValue =
fractionDigits > 0 ? +rawValue.toFixed(fractionDigits) : rawValue;

return Math.max(min, Math.min(newValue, max));
};

const getPosition = useCallback(
(value: number): number => ((value - min) / (max - min)) * rangeWidth,
[min, max, rangeWidth]
);

const minSpaceBetween = getPosition(min + MIN_VALUE_BETWEEN);
const minSpaceBetween = getPosition(min + minValueBetween);

const updateCurrentMin = useCallback(
(newMin: number, notifyChange = false) => {
newMin = Math.max(newMin, min);
if (newMin <= currentMax - MIN_VALUE_BETWEEN) {
if (newMin <= currentMax - minValueBetween) {
setCurrentMin(newMin);
minX.set(getPosition(newMin));
notifyChange && onChange?.([newMin, currentMax]);
}
},
[currentMax, min, minX, getPosition, onChange]
[currentMax, min, minX, getPosition, onChange, minValueBetween]
);

const updateCurrentMax = useCallback(
(newMax: number, notifyChange = false) => {
newMax = Math.min(newMax, max);
if (newMax >= currentMin + MIN_VALUE_BETWEEN) {
if (newMax >= currentMin + minValueBetween) {
setCurrentMax(newMax);
maxX.set(getPosition(newMax));
notifyChange && onChange?.([currentMin, newMax]);
}
},
[currentMin, max, maxX, getPosition, onChange]
[currentMin, max, maxX, getPosition, onChange, minValueBetween]
);

useEffect(() => {
Expand Down
28 changes: 25 additions & 3 deletions src/form/Range/RangeSingle.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useRef, useState, FC } from 'react';
import React, {
useCallback,
useEffect,
useRef,
useState,
FC,
useMemo
} from 'react';
import classNames from 'classnames';
import { motion, useMotionValue } from 'framer-motion';
import { RangeProps, RangeTooltip } from './RangeTooltip';
Expand All @@ -12,7 +19,8 @@ export const RangeSingle: FC<RangeProps<number>> = ({
className,
min,
max,
value
value,
step = 1
}) => {
const [currentValue, setCurrentValue] = useState<number>(value);

Expand All @@ -22,10 +30,23 @@ export const RangeSingle: FC<RangeProps<number>> = ({

const valueX = useMotionValue(0);

const fractionDigits = useMemo(
() => step.toString()?.[1]?.length || 0,
[step]
);

const getValue = (xPosition: number): number => {
const draggedWidth = xPosition - rangeLeft;
const draggedWidthPercentage = (draggedWidth * 100) / rangeWidth;
return Math.round(min + ((max - min) * draggedWidthPercentage) / 100);
const scaledStep = (step / (max - min)) * 100;
const scaledValue =
Math.round(draggedWidthPercentage / scaledStep) * scaledStep;
const rawValue = min + ((max - min) * scaledValue) / 100;
// Fix floating point precision. Example 3.50000000000000004
const newValue =
fractionDigits > 0 ? +rawValue.toFixed(fractionDigits) : rawValue;

return Math.max(min, Math.min(newValue, max));
};

const getPosition = useCallback(
Expand Down Expand Up @@ -88,6 +109,7 @@ export const RangeSingle: FC<RangeProps<number>> = ({
type="range"
min={min}
max={max}
step={0.5}
value={currentValue}
disabled={disabled}
onChange={e => updateCurrentValue(e.target.valueAsNumber)}
Expand Down
6 changes: 6 additions & 0 deletions src/form/Range/RangeTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface RangeProps<Value> {
*/
max: number;

/**
* The value will be a multiple of step
* The default is 1
*/
step?: number;

/**
* The value of the range
*/
Expand Down

0 comments on commit 744e7bf

Please sign in to comment.