Загрузка данных
import classNames from 'classnames';
import { type ChangeEvent, type PointerEvent, useId, useRef } from 'react';
import styles from './index.module.scss';
interface CommonRangeFieldProps {
label?: string;
min?: number;
max?: number;
step?: number;
color?: string;
suffix?: string;
size?: 'sm' | 'md';
disabled?: boolean;
}
interface SingleRangeFieldProps extends CommonRangeFieldProps {
mode?: 'single';
value: number;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
interface IntervalRangeFieldProps extends CommonRangeFieldProps {
mode: 'range';
value: [number, number];
onRangeChange: (value: [number, number]) => void;
}
interface RangeDragState {
pointerX: number;
from: number;
to: number;
trackWidth: number;
}
type RangeFieldProps = SingleRangeFieldProps | IntervalRangeFieldProps;
export const RangeField = (props: RangeFieldProps) => {
const {
label,
min = 0,
max = 100,
step = 1,
color = 'var(--blue-1)',
suffix = '%',
size = 'md',
disabled = false,
} = props;
const inputId = useId();
const trackRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<RangeDragState | null>(null);
if (props.mode !== 'range') {
return (
<div className={classNames(styles.range, { [styles.range_sm]: size === 'sm' })}>
{label && (
<label
htmlFor={inputId}
className={styles.range_label}
>
{label}
</label>
)}
<div className={styles.range_wrapper}>
<input
id={inputId}
type="range"
min={min}
max={max}
step={step}
value={props.value}
className={styles.range_input}
style={{ ['--range-color' as string]: color }}
onChange={props.onChange}
disabled={disabled}
/>
<span className={styles.range_value}>
{props.value}
{suffix}
</span>
</div>
</div>
);
}
const {
value: [from, to],
onRangeChange,
} = props;
const span = max - min;
const left = span > 0 ? ((from - min) / span) * 100 : 0;
const right = span > 0 ? ((to - min) / span) * 100 : 0;
const isDisabled = disabled || span <= 0;
const handleDragEnd = () => {
dragRef.current = null;
};
const handleDragStart = (event: PointerEvent<HTMLDivElement>) => {
if (isDisabled || event.button !== 0 || !trackRef.current) return;
const { width } = trackRef.current.getBoundingClientRect();
if (width <= 0) return;
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
dragRef.current = { pointerX: event.clientX, from, to, trackWidth: width };
};
const handleDragMove = (event: PointerEvent<HTMLDivElement>) => {
const drag = dragRef.current;
if (!drag || isDisabled) return;
const pointerOffset = ((event.clientX - drag.pointerX) / drag.trackWidth) * span;
const steppedOffset = Math.round(pointerOffset / step) * step;
const offset = Math.max(min - drag.from, Math.min(max - drag.to, steppedOffset));
onRangeChange([drag.from + offset, drag.to + offset]);
};
const handleFromChange = (event: ChangeEvent<HTMLInputElement>) => {
onRangeChange([Math.min(event.currentTarget.valueAsNumber, to), to]);
};
const handleToChange = (event: ChangeEvent<HTMLInputElement>) => {
onRangeChange([from, Math.max(event.currentTarget.valueAsNumber, from)]);
};
return (
<div
className={classNames(styles.range, styles.range_interval, {
[styles.range_sm]: size === 'sm',
[styles.range_disabled]: isDisabled,
})}
>
{label && <span className={styles.range_label}>{label}</span>}
<div
ref={trackRef}
className={styles.range_track}
style={{ ['--range-color' as string]: color }}
>
<div className={styles.range_rail} />
<div
className={styles.range_selection}
style={{ left: `${left}%`, width: `${right - left}%` }}
onPointerDown={handleDragStart}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
onPointerCancel={handleDragEnd}
onLostPointerCapture={handleDragEnd}
/>
<input
id={`${inputId}-from`}
type="range"
min={min}
max={max}
step={step}
value={from}
disabled={isDisabled}
className={classNames(styles.range_handle, styles.range_handle_start, {
[styles.range_handle_behind]: from === to && from === min,
})}
onChange={handleFromChange}
/>
<input
id={`${inputId}-to`}
type="range"
min={min}
max={max}
step={step}
value={to}
disabled={isDisabled}
className={classNames(styles.range_handle, styles.range_handle_end)}
onChange={handleToChange}
/>
</div>
<div className={styles.range_values}>
<span>
{from}
{suffix}
</span>
<span>
{to}
{suffix}
</span>
</div>
</div>
);
};