@spaced-out/ui-design-system
Version:
Sense UI components library
203 lines (182 loc) • 5.67 kB
Flow
// @flow strict
import * as React from 'react';
import {
colorFillPrimary,
colorGrayLightest,
colorTextDisabled,
} from '../../styles/variables/_color';
import classify from '../../utils/classify';
import type {ButtonSize} from '../Button';
import {Button, BUTTON_SIZE} from '../Button';
import {Input} from '../Input';
import {BodySmall} from '../Text';
import css from './RangeSlider.module.css';
type ClassNames = $ReadOnly<{wrapper?: string}>;
export type RangeSliderProps = {
min?: number,
max?: number,
step?: number,
value?: number,
onChange?: (value: number) => void, // We don't want to send in the event because the icon buttons also modify the value
showTicks?: boolean,
iconLeftName?: string,
btnLeftSize?: ButtonSize,
iconRightName?: string,
btnRightSize?: ButtonSize,
disabled?: boolean,
hideLeftBtn?: boolean,
hideRightBtn?: boolean,
classNames?: ClassNames,
ariaLabel?: string,
showRange?: boolean,
hideValueInput?: boolean,
...
};
export const RangeSlider: React$AbstractComponent<
RangeSliderProps,
HTMLDivElement,
> = React.forwardRef<RangeSliderProps, HTMLDivElement>(
(
{
classNames,
disabled,
min = 0,
max = 100,
step = 1,
value = min,
onChange,
showTicks,
iconLeftName = 'minus',
btnLeftSize = BUTTON_SIZE.small,
iconRightName = 'plus',
btnRightSize = BUTTON_SIZE.small,
hideLeftBtn,
hideRightBtn,
ariaLabel = 'Slider',
showRange,
hideValueInput,
...restInputProps
}: RangeSliderProps,
ref,
) => {
const progress = ((value - min) / (max - min)) * 100;
const btnLeftDisabled = disabled || value <= min;
const btnRightDisabled = disabled || value >= max;
const progressColor = disabled ? colorTextDisabled : colorFillPrimary;
const handleInputChange = (e: SyntheticEvent<HTMLInputElement>) => {
let inputValue = parseFloat(e.currentTarget.value);
// Validate and adjust the value
if (isNaN(inputValue) || inputValue < min) {
inputValue = min;
} else if (inputValue > max) {
inputValue = max;
} else if (step > 0) {
const nearestStep = Math.round(inputValue / step) * step;
inputValue = Math.round(nearestStep * 100) / 100; // Adjust to nearest valid step
}
onChange?.(inputValue);
};
const handleChange = (e: SyntheticEvent<HTMLInputElement>) => {
emitRoundedValue(e.currentTarget.value);
};
const emitRoundedValue = (value: number | string) => {
const newValue = parseFloat(value);
const roundedValue = Math.round(newValue * 100) / 100; // Rounds to two decimal places
onChange?.(roundedValue);
};
return (
<div
ref={ref}
data-testid="RangeSlider"
className={classify(
css.wrapper,
{[css.disabled]: disabled},
classNames?.wrapper,
)}
>
{!hideLeftBtn && !showRange && (
<Button
type="ghost"
iconLeftName={iconLeftName}
size={btnLeftSize}
ariaLabel="Decrease Value"
disabled={btnLeftDisabled}
onClick={() => emitRoundedValue(value - step)}
/>
)}
{showRange && (
<BodySmall
className={css.rangeText}
color={disabled ? 'disabled' : 'primary'}
>
{min}
</BodySmall>
)}
<div className={css.sliderContainer}>
<input
{...restInputProps}
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
className={css.slider}
list={showTicks ? 'ticks' : undefined}
disabled={disabled}
aria-label={ariaLabel}
style={{
background: `linear-gradient(to right, ${progressColor} ${progress}%, ${colorGrayLightest} ${progress}%)`,
}}
/>
{showTicks && step > 0 && (
<datalist id="ticks" className={css.sliderTicks}>
{/*
Note(Nishant): Currently, ticks are shown based on the step value.
This can be easily extended in the future to support custom tick intervals
by allowing user-defined brackets or ranges for tick marks.
*/}
{Array.from({length: (max - min) / step + 1}, (_, index) => (
<option
key={index}
value={min + index * step}
className={classify({[css.disabled]: disabled})}
/>
))}
</datalist>
)}
</div>
{showRange && (
<BodySmall
className={css.rangeText}
color={disabled ? 'disabled' : 'primary'}
>
{max}
</BodySmall>
)}
{!hideRightBtn && !showRange && (
<Button
type="ghost"
iconRightName={iconRightName}
size={btnRightSize}
ariaLabel="Increase Value"
disabled={btnRightDisabled}
onClick={() => emitRoundedValue(value + step)}
/>
)}
{!hideValueInput && (
<Input
size="small"
type="number"
value={String(value)}
classNames={{wrapper: css.valueInputWrapper}}
disabled={disabled}
onChange={handleInputChange}
disallowExponents
hideNumberSpinner
/>
)}
</div>
);
},
);