1import React, { useState, useCallback, ChangeEvent, FunctionComponent, FocusEvent } from 'react';
2import SliderComponent from 'rc-slider';
3
4import { cx } from '@emotion/css';
5import { Global } from '@emotion/react';
6import { useTheme2 } from '../../themes/ThemeContext';
7import { getStyles } from './styles';
8import { SliderProps } from './types';
9import { Input } from '../Input/Input';
10
11/**
12 * @public
13 */
14export const Slider: FunctionComponent<SliderProps> = ({
15  min,
16  max,
17  onChange,
18  onAfterChange,
19  orientation = 'horizontal',
20  reverse,
21  step,
22  value,
23  ariaLabelForHandle,
24  marks,
25  included,
26}) => {
27  const isHorizontal = orientation === 'horizontal';
28  const theme = useTheme2();
29  const styles = getStyles(theme, isHorizontal, Boolean(marks));
30  const SliderWithTooltip = SliderComponent;
31  const [sliderValue, setSliderValue] = useState<number>(value ?? min);
32
33  const onSliderChange = useCallback(
34    (v: number) => {
35      setSliderValue(v);
36
37      if (onChange) {
38        onChange(v);
39      }
40    },
41    [setSliderValue, onChange]
42  );
43
44  const onSliderInputChange = useCallback(
45    (e: ChangeEvent<HTMLInputElement>) => {
46      let v = +e.target.value;
47
48      if (Number.isNaN(v)) {
49        v = 0;
50      }
51
52      setSliderValue(v);
53
54      if (onChange) {
55        onChange(v);
56      }
57
58      if (onAfterChange) {
59        onAfterChange(v);
60      }
61    },
62    [onChange, onAfterChange]
63  );
64
65  // Check for min/max on input blur so user is able to enter
66  // custom values that might seem above/below min/max on first keystroke
67  const onSliderInputBlur = useCallback(
68    (e: FocusEvent<HTMLInputElement>) => {
69      const v = +e.target.value;
70
71      if (v > max) {
72        setSliderValue(max);
73      } else if (v < min) {
74        setSliderValue(min);
75      }
76    },
77    [max, min]
78  );
79
80  const sliderInputClassNames = !isHorizontal ? [styles.sliderInputVertical] : [];
81  const sliderInputFieldClassNames = !isHorizontal ? [styles.sliderInputFieldVertical] : [];
82
83  return (
84    <div className={cx(styles.container, styles.slider)}>
85      {/** Slider tooltip's parent component is body and therefore we need Global component to do css overrides for it. */}
86      <Global styles={styles.tooltip} />
87      <label className={cx(styles.sliderInput, ...sliderInputClassNames)}>
88        <SliderWithTooltip
89          min={min}
90          max={max}
91          step={step}
92          defaultValue={value}
93          value={sliderValue}
94          onChange={onSliderChange}
95          onAfterChange={onAfterChange}
96          vertical={!isHorizontal}
97          reverse={reverse}
98          ariaLabelForHandle={ariaLabelForHandle}
99          marks={marks}
100          included={included}
101        />
102        {/* Uses text input so that the number spinners are not shown */}
103        <Input
104          type="text"
105          className={cx(styles.sliderInputField, ...sliderInputFieldClassNames)}
106          value={`${sliderValue}`} // to fix the react leading zero issue
107          onChange={onSliderInputChange}
108          onBlur={onSliderInputBlur}
109          min={min}
110          max={max}
111        />
112      </label>
113    </div>
114  );
115};
116
117Slider.displayName = 'Slider';
118