UNPKG

@spaced-out/ui-design-system

Version:
203 lines (182 loc) 5.67 kB
// @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> ); }, );