9 |
--------------------------------------------------------------------------------
/docs/demo/editable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Editable
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/handle.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Handle
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/marks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Marks
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/mulitple.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Multiple
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/range.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Range
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/slider.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Slider
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/vertical.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Vertical
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/examples/components/TooltipSlider.tsx:
--------------------------------------------------------------------------------
1 | import type { SliderProps } from 'rc-slider';
2 | import Slider from 'rc-slider';
3 | import type { TooltipRef } from 'rc-tooltip';
4 | import Tooltip from 'rc-tooltip';
5 | import 'rc-tooltip/assets/bootstrap.css';
6 | import raf from 'rc-util/lib/raf';
7 | import * as React from 'react';
8 |
9 | interface HandleTooltipProps {
10 | value: number;
11 | children: React.ReactElement;
12 | visible: boolean;
13 | tipFormatter?: (value: number) => React.ReactNode;
14 | }
15 |
16 | const HandleTooltip: React.FCHere is a word that drag should not select it
67 |Slider with custom handle
15 |Reversed Slider with custom handle
19 |Slider with fixed values
23 |Range with custom tooltip
27 |Keyboard events disabled
37 |Slider with marks, `step=null`
32 |Range Slider with marks, `step=null`, pushable, draggableTrack
44 |Slider with marks and steps
59 |Reversed Slider with marks and steps
63 |Slider with marks, `included=false`
68 |Slider with marks and steps, `included=false`
72 |Range with marks
77 |Range with marks and steps
81 |Basic Range,`allowCross=false`
188 |Basic reverse Range`
192 |Basic Range,`step=20`
196 |Basic Range,`step=20, dots`
200 |Basic Range,disabled
204 |Controlled Range
208 |Controlled Range, not allow across
212 |Controlled Range, not allow across, pushable=5
216 |Multi Range, count=3 and pushable=true
220 |Multi Range with custom track and handle style and pushable
224 |Customized Range
236 |Range with dynamic `max` `min`
240 |Range as child component
244 |draggableTrack two points
248 |draggableTrack two points(reverse)
252 |draggableTrack multiple points
262 |Basic Slider
189 |Basic Slider, `startPoint=50`
193 |Slider reverse
197 |Basic Slider,`step=20`
201 |Basic Slider,`step=20, dots`
205 |209 | Basic Slider,`step=20, dots, dotStyle={"{borderColor: 'orange'}"}, activeDotStyle= 210 | {"{borderColor: 'yellow'}"}` 211 |
212 |Slider with tooltip, with custom `tipFormatter`
223 |231 | Slider with custom handle and track style.(old api, will be deprecated) 232 |
233 |249 | Slider with custom handle and track style.(The recommended new api) 250 |
251 |267 | Reversed Slider with custom handle and track style. 268 | (The recommended new api) 269 |
270 |Basic Slider, disabled
287 |Controlled Slider
291 |Customized Slider
295 |Slider with null value and reset button
299 |Range Slider with null value and reset button
303 |Slider with dynamic `min` `max` `step`
307 |Slider with marks, `step=null`
39 |Slider with marks, `step=null` and `startPoint=0`
43 |Reverse Slider with marks, `step=null`
55 |Slider with marks and steps
67 |Slider with marks, `included=false`
71 |Slider with marks and steps, `included=false`
75 |Range with marks
79 |Range with marks and steps
83 |Range with marks and draggableTrack
95 |Range with marks and draggableTrack(reverse)
106 |
19 | `,
20 | 'utf8',
21 | function(error) {
22 | if(error){
23 | console.log(error);
24 | return false;
25 | }
26 | console.log(`${name} 更新成功~`);
27 | }
28 | )
29 | });
30 |
--------------------------------------------------------------------------------
/src/Handles/Handle.tsx:
--------------------------------------------------------------------------------
1 | import cls from 'classnames';
2 | import KeyCode from 'rc-util/lib/KeyCode';
3 | import * as React from 'react';
4 | import SliderContext from '../context';
5 | import type { OnStartMove } from '../interface';
6 | import { getDirectionStyle, getIndex } from '../util';
7 |
8 | interface RenderProps {
9 | index: number;
10 | prefixCls: string;
11 | value: number;
12 | dragging: boolean;
13 | draggingDelete: boolean;
14 | }
15 |
16 | export interface HandleProps
17 | extends Omit, 'onFocus' | 'onMouseEnter'> {
18 | prefixCls: string;
19 | style?: React.CSSProperties;
20 | value: number;
21 | valueIndex: number;
22 | dragging: boolean;
23 | draggingDelete: boolean;
24 | onStartMove: OnStartMove;
25 | onDelete?: (index: number) => void;
26 | onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
27 | onFocus: (e: React.FocusEvent, index: number) => void;
28 | onMouseEnter: (e: React.MouseEvent, index: number) => void;
29 | render?: (
30 | origin: React.ReactElement>,
31 | props: RenderProps,
32 | ) => React.ReactElement;
33 | onChangeComplete?: () => void;
34 | mock?: boolean;
35 | }
36 |
37 | const Handle = React.forwardRef((props, ref) => {
38 | const {
39 | prefixCls,
40 | value,
41 | valueIndex,
42 | onStartMove,
43 | onDelete,
44 | style,
45 | render,
46 | dragging,
47 | draggingDelete,
48 | onOffsetChange,
49 | onChangeComplete,
50 | onFocus,
51 | onMouseEnter,
52 | ...restProps
53 | } = props;
54 | const {
55 | min,
56 | max,
57 | direction,
58 | disabled,
59 | keyboard,
60 | range,
61 | tabIndex,
62 | ariaLabelForHandle,
63 | ariaLabelledByForHandle,
64 | ariaRequired,
65 | ariaValueTextFormatterForHandle,
66 | styles,
67 | classNames,
68 | } = React.useContext(SliderContext);
69 |
70 | const handlePrefixCls = `${prefixCls}-handle`;
71 |
72 | // ============================ Events ============================
73 | const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
74 | if (!disabled) {
75 | onStartMove(e, valueIndex);
76 | }
77 | };
78 |
79 | const onInternalFocus = (e: React.FocusEvent) => {
80 | onFocus?.(e, valueIndex);
81 | };
82 |
83 | const onInternalMouseEnter = (e: React.MouseEvent) => {
84 | onMouseEnter(e, valueIndex);
85 | };
86 |
87 | // =========================== Keyboard ===========================
88 | const onKeyDown: React.KeyboardEventHandler = (e) => {
89 | if (!disabled && keyboard) {
90 | let offset: number | 'min' | 'max' = null;
91 |
92 | // Change the value
93 | switch (e.which || e.keyCode) {
94 | case KeyCode.LEFT:
95 | offset = direction === 'ltr' || direction === 'btt' ? -1 : 1;
96 | break;
97 |
98 | case KeyCode.RIGHT:
99 | offset = direction === 'ltr' || direction === 'btt' ? 1 : -1;
100 | break;
101 |
102 | // Up is plus
103 | case KeyCode.UP:
104 | offset = direction !== 'ttb' ? 1 : -1;
105 | break;
106 |
107 | // Down is minus
108 | case KeyCode.DOWN:
109 | offset = direction !== 'ttb' ? -1 : 1;
110 | break;
111 |
112 | case KeyCode.HOME:
113 | offset = 'min';
114 | break;
115 |
116 | case KeyCode.END:
117 | offset = 'max';
118 | break;
119 |
120 | case KeyCode.PAGE_UP:
121 | offset = 2;
122 | break;
123 |
124 | case KeyCode.PAGE_DOWN:
125 | offset = -2;
126 | break;
127 |
128 | case KeyCode.BACKSPACE:
129 | case KeyCode.DELETE:
130 | onDelete?.(valueIndex);
131 | break;
132 | }
133 |
134 | if (offset !== null) {
135 | e.preventDefault();
136 | onOffsetChange(offset, valueIndex);
137 | }
138 | }
139 | };
140 |
141 | const handleKeyUp = (e: React.KeyboardEvent) => {
142 | switch (e.which || e.keyCode) {
143 | case KeyCode.LEFT:
144 | case KeyCode.RIGHT:
145 | case KeyCode.UP:
146 | case KeyCode.DOWN:
147 | case KeyCode.HOME:
148 | case KeyCode.END:
149 | case KeyCode.PAGE_UP:
150 | case KeyCode.PAGE_DOWN:
151 | onChangeComplete?.();
152 | break;
153 | }
154 | };
155 |
156 | // ============================ Offset ============================
157 | const positionStyle = getDirectionStyle(direction, value, min, max);
158 |
159 | // ============================ Render ============================
160 | let divProps: React.HtmlHTMLAttributes = {};
161 |
162 | if (valueIndex !== null) {
163 | divProps = {
164 | tabIndex: disabled ? null : getIndex(tabIndex, valueIndex),
165 | role: 'slider',
166 | 'aria-valuemin': min,
167 | 'aria-valuemax': max,
168 | 'aria-valuenow': value,
169 | 'aria-disabled': disabled,
170 | 'aria-label': getIndex(ariaLabelForHandle, valueIndex),
171 | 'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex),
172 | 'aria-required': getIndex(ariaRequired, valueIndex),
173 | 'aria-valuetext': getIndex(ariaValueTextFormatterForHandle, valueIndex)?.(value),
174 | 'aria-orientation': direction === 'ltr' || direction === 'rtl' ? 'horizontal' : 'vertical',
175 | onMouseDown: onInternalStartMove,
176 | onTouchStart: onInternalStartMove,
177 | onFocus: onInternalFocus,
178 | onMouseEnter: onInternalMouseEnter,
179 | onKeyDown,
180 | onKeyUp: handleKeyUp,
181 | };
182 | }
183 |
184 | let handleNode = (
185 |
204 | );
205 |
206 | // Customize
207 | if (render) {
208 | handleNode = render(handleNode, {
209 | index: valueIndex,
210 | prefixCls,
211 | value,
212 | dragging,
213 | draggingDelete,
214 | });
215 | }
216 |
217 | return handleNode;
218 | });
219 |
220 | if (process.env.NODE_ENV !== 'production') {
221 | Handle.displayName = 'Handle';
222 | }
223 |
224 | export default Handle;
225 |
--------------------------------------------------------------------------------
/src/Handles/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { flushSync } from 'react-dom';
3 | import type { OnStartMove } from '../interface';
4 | import { getIndex } from '../util';
5 | import type { HandleProps } from './Handle';
6 | import Handle from './Handle';
7 |
8 | export interface HandlesProps {
9 | prefixCls: string;
10 | style?: React.CSSProperties | React.CSSProperties[];
11 | values: number[];
12 | onStartMove: OnStartMove;
13 | onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
14 | onFocus?: (e: React.FocusEvent) => void;
15 | onBlur?: (e: React.FocusEvent) => void;
16 | onDelete?: (index: number) => void;
17 | handleRender?: HandleProps['render'];
18 | /**
19 | * When config `activeHandleRender`,
20 | * it will render another hidden handle for active usage.
21 | * This is useful for accessibility or tooltip usage.
22 | */
23 | activeHandleRender?: HandleProps['render'];
24 | draggingIndex: number;
25 | draggingDelete: boolean;
26 | onChangeComplete?: () => void;
27 | }
28 |
29 | export interface HandlesRef {
30 | focus: (index: number) => void;
31 | hideHelp: VoidFunction;
32 | }
33 |
34 | const Handles = React.forwardRef((props, ref) => {
35 | const {
36 | prefixCls,
37 | style,
38 | onStartMove,
39 | onOffsetChange,
40 | values,
41 | handleRender,
42 | activeHandleRender,
43 | draggingIndex,
44 | draggingDelete,
45 | onFocus,
46 | ...restProps
47 | } = props;
48 | const handlesRef = React.useRef>({});
49 |
50 | // =========================== Active ===========================
51 | const [activeVisible, setActiveVisible] = React.useState(false);
52 | const [activeIndex, setActiveIndex] = React.useState(-1);
53 |
54 | const onActive = (index: number) => {
55 | setActiveIndex(index);
56 | setActiveVisible(true);
57 | };
58 |
59 | const onHandleFocus = (e: React.FocusEvent, index: number) => {
60 | onActive(index);
61 | onFocus?.(e);
62 | };
63 |
64 | const onHandleMouseEnter = (e: React.MouseEvent, index: number) => {
65 | onActive(index);
66 | };
67 |
68 | // =========================== Render ===========================
69 | React.useImperativeHandle(ref, () => ({
70 | focus: (index: number) => {
71 | handlesRef.current[index]?.focus();
72 | },
73 | hideHelp: () => {
74 | flushSync(() => {
75 | setActiveVisible(false);
76 | });
77 | },
78 | }));
79 |
80 | // =========================== Render ===========================
81 | // Handle Props
82 | const handleProps = {
83 | prefixCls,
84 | onStartMove,
85 | onOffsetChange,
86 | render: handleRender,
87 | onFocus: onHandleFocus,
88 | onMouseEnter: onHandleMouseEnter,
89 | ...restProps,
90 | };
91 |
92 | return (
93 | <>
94 | {values.map((value, index) => {
95 | const dragging = draggingIndex === index;
96 |
97 | return (
98 | {
100 | if (!node) {
101 | delete handlesRef.current[index];
102 | } else {
103 | handlesRef.current[index] = node;
104 | }
105 | }}
106 | dragging={dragging}
107 | draggingDelete={dragging && draggingDelete}
108 | style={getIndex(style, index)}
109 | key={index}
110 | value={value}
111 | valueIndex={index}
112 | {...handleProps}
113 | />
114 | );
115 | })}
116 |
117 | {/* Used for render tooltip, this is not a real handle */}
118 | {activeHandleRender && activeVisible && (
119 |
131 | )}
132 | >
133 | );
134 | });
135 |
136 | if (process.env.NODE_ENV !== 'production') {
137 | Handles.displayName = 'Handles';
138 | }
139 |
140 | export default Handles;
141 |
--------------------------------------------------------------------------------
/src/Marks/Mark.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import * as React from 'react';
3 | import SliderContext from '../context';
4 | import { getDirectionStyle } from '../util';
5 |
6 | export interface MarkProps {
7 | prefixCls: string;
8 | children?: React.ReactNode;
9 | style?: React.CSSProperties;
10 | value: number;
11 | onClick: (value: number) => void;
12 | }
13 |
14 | const Mark: React.FC = (props) => {
15 | const { prefixCls, style, children, value, onClick } = props;
16 | const { min, max, direction, includedStart, includedEnd, included } =
17 | React.useContext(SliderContext);
18 |
19 | const textCls = `${prefixCls}-text`;
20 |
21 | // ============================ Offset ============================
22 | const positionStyle = getDirectionStyle(direction, value, min, max);
23 |
24 | return (
25 | {
31 | e.stopPropagation();
32 | }}
33 | onClick={() => {
34 | onClick(value);
35 | }}
36 | >
37 | {children}
38 |
39 | );
40 | };
41 |
42 | export default Mark;
43 |
--------------------------------------------------------------------------------
/src/Marks/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Mark from './Mark';
3 |
4 | export interface MarkObj {
5 | style?: React.CSSProperties;
6 | label?: React.ReactNode;
7 | }
8 |
9 | export interface InternalMarkObj extends MarkObj {
10 | value: number;
11 | }
12 |
13 | export interface MarksProps {
14 | prefixCls: string;
15 | marks?: InternalMarkObj[];
16 | onClick: (value: number) => void;
17 | }
18 |
19 | const Marks: React.FC = (props) => {
20 | const { prefixCls, marks, onClick } = props;
21 |
22 | const markPrefixCls = `${prefixCls}-mark`;
23 |
24 | // Not render mark if empty
25 | if (!marks.length) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 | {marks.map(({ value, style, label }) => (
32 |
33 | {label}
34 |
35 | ))}
36 |
37 | );
38 | };
39 |
40 | export default Marks;
41 |
--------------------------------------------------------------------------------
/src/Slider.tsx:
--------------------------------------------------------------------------------
1 | import cls from 'classnames';
2 | import useEvent from 'rc-util/lib/hooks/useEvent';
3 | import useMergedState from 'rc-util/lib/hooks/useMergedState';
4 | import isEqual from 'rc-util/lib/isEqual';
5 | import warning from 'rc-util/lib/warning';
6 | import * as React from 'react';
7 | import type { HandlesProps, HandlesRef } from './Handles';
8 | import Handles from './Handles';
9 | import type { InternalMarkObj, MarkObj } from './Marks';
10 | import Marks from './Marks';
11 | import Steps from './Steps';
12 | import Tracks from './Tracks';
13 | import type { SliderContextProps } from './context';
14 | import SliderContext from './context';
15 | import useDrag from './hooks/useDrag';
16 | import useOffset from './hooks/useOffset';
17 | import useRange from './hooks/useRange';
18 | import type {
19 | AriaValueFormat,
20 | Direction,
21 | OnStartMove,
22 | SliderClassNames,
23 | SliderStyles,
24 | } from './interface';
25 |
26 | /**
27 | * New:
28 | * - click mark to update range value
29 | * - handleRender
30 | * - Fix handle with count not correct
31 | * - Fix pushable not work in some case
32 | * - No more FindDOMNode
33 | * - Move all position related style into inline style
34 | * - Key: up is plus, down is minus
35 | * - fix Key with step = null not align with marks
36 | * - Change range should not trigger onChange
37 | * - keyboard support pushable
38 | */
39 |
40 | export type RangeConfig = {
41 | editable?: boolean;
42 | draggableTrack?: boolean;
43 | /** Set min count when `editable` */
44 | minCount?: number;
45 | /** Set max count when `editable` */
46 | maxCount?: number;
47 | };
48 |
49 | export interface SliderProps {
50 | prefixCls?: string;
51 | className?: string;
52 | style?: React.CSSProperties;
53 |
54 | classNames?: SliderClassNames;
55 | styles?: SliderStyles;
56 |
57 | id?: string;
58 |
59 | // Status
60 | disabled?: boolean;
61 | keyboard?: boolean;
62 | autoFocus?: boolean;
63 | onFocus?: (e: React.FocusEvent) => void;
64 | onBlur?: (e: React.FocusEvent) => void;
65 |
66 | // Value
67 | range?: boolean | RangeConfig;
68 | /** @deprecated Use `range.minCount` or `range.maxCount` to handle this */
69 | count?: number;
70 | min?: number;
71 | max?: number;
72 | step?: number | null;
73 | value?: ValueType;
74 | defaultValue?: ValueType;
75 | onChange?: (value: ValueType) => void;
76 | /** @deprecated It's always better to use `onChange` instead */
77 | onBeforeChange?: (value: ValueType) => void;
78 | /** @deprecated Use `onChangeComplete` instead */
79 | onAfterChange?: (value: ValueType) => void;
80 | onChangeComplete?: (value: ValueType) => void;
81 |
82 | // Cross
83 | allowCross?: boolean;
84 | pushable?: boolean | number;
85 |
86 | // Direction
87 | reverse?: boolean;
88 | vertical?: boolean;
89 |
90 | // Style
91 | included?: boolean;
92 | startPoint?: number;
93 | /** @deprecated Please use `styles.track` instead */
94 | trackStyle?: React.CSSProperties | React.CSSProperties[];
95 | /** @deprecated Please use `styles.handle` instead */
96 | handleStyle?: React.CSSProperties | React.CSSProperties[];
97 | /** @deprecated Please use `styles.rail` instead */
98 | railStyle?: React.CSSProperties;
99 | dotStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
100 | activeDotStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
101 |
102 | // Decorations
103 | marks?: Record;
104 | dots?: boolean;
105 |
106 | // Components
107 | handleRender?: HandlesProps['handleRender'];
108 | activeHandleRender?: HandlesProps['handleRender'];
109 | track?: boolean;
110 |
111 | // Accessibility
112 | tabIndex?: number | number[];
113 | ariaLabelForHandle?: string | string[];
114 | ariaLabelledByForHandle?: string | string[];
115 | ariaRequired?: boolean;
116 | ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
117 | }
118 |
119 | export interface SliderRef {
120 | focus: () => void;
121 | blur: () => void;
122 | }
123 |
124 | const Slider = React.forwardRef>((props, ref) => {
125 | const {
126 | prefixCls = 'rc-slider',
127 | className,
128 | style,
129 | classNames,
130 | styles,
131 |
132 | id,
133 |
134 | // Status
135 | disabled = false,
136 | keyboard = true,
137 | autoFocus,
138 | onFocus,
139 | onBlur,
140 |
141 | // Value
142 | min = 0,
143 | max = 100,
144 | step = 1,
145 | value,
146 | defaultValue,
147 | range,
148 | count,
149 | onChange,
150 | onBeforeChange,
151 | onAfterChange,
152 | onChangeComplete,
153 |
154 | // Cross
155 | allowCross = true,
156 | pushable = false,
157 |
158 | // Direction
159 | reverse,
160 | vertical,
161 |
162 | // Style
163 | included = true,
164 | startPoint,
165 | trackStyle,
166 | handleStyle,
167 | railStyle,
168 | dotStyle,
169 | activeDotStyle,
170 |
171 | // Decorations
172 | marks,
173 | dots,
174 |
175 | // Components
176 | handleRender,
177 | activeHandleRender,
178 | track,
179 |
180 | // Accessibility
181 | tabIndex = 0,
182 | ariaLabelForHandle,
183 | ariaLabelledByForHandle,
184 | ariaRequired,
185 | ariaValueTextFormatterForHandle,
186 | } = props;
187 |
188 | const handlesRef = React.useRef(null);
189 | const containerRef = React.useRef(null);
190 |
191 | const direction = React.useMemo(() => {
192 | if (vertical) {
193 | return reverse ? 'ttb' : 'btt';
194 | }
195 | return reverse ? 'rtl' : 'ltr';
196 | }, [reverse, vertical]);
197 |
198 | // ============================ Range =============================
199 | const [rangeEnabled, rangeEditable, rangeDraggableTrack, minCount, maxCount] = useRange(range);
200 |
201 | const mergedMin = React.useMemo(() => (isFinite(min) ? min : 0), [min]);
202 | const mergedMax = React.useMemo(() => (isFinite(max) ? max : 100), [max]);
203 |
204 | // ============================= Step =============================
205 | const mergedStep = React.useMemo(() => (step !== null && step <= 0 ? 1 : step), [step]);
206 |
207 | // ============================= Push =============================
208 | const mergedPush = React.useMemo(() => {
209 | if (typeof pushable === 'boolean') {
210 | return pushable ? mergedStep : false;
211 | }
212 | return pushable >= 0 ? pushable : false;
213 | }, [pushable, mergedStep]);
214 |
215 | // ============================ Marks =============================
216 | const markList = React.useMemo(() => {
217 | return Object.keys(marks || {})
218 | .map((key) => {
219 | const mark = marks[key];
220 | const markObj: InternalMarkObj = {
221 | value: Number(key),
222 | };
223 |
224 | if (
225 | mark &&
226 | typeof mark === 'object' &&
227 | !React.isValidElement(mark) &&
228 | ('label' in mark || 'style' in mark)
229 | ) {
230 | markObj.style = mark.style;
231 | markObj.label = mark.label;
232 | } else {
233 | markObj.label = mark as React.ReactNode;
234 | }
235 |
236 | return markObj;
237 | })
238 | .filter(({ label }) => label || typeof label === 'number')
239 | .sort((a, b) => a.value - b.value);
240 | }, [marks]);
241 |
242 | // ============================ Format ============================
243 | const [formatValue, offsetValues] = useOffset(
244 | mergedMin,
245 | mergedMax,
246 | mergedStep,
247 | markList,
248 | allowCross,
249 | mergedPush,
250 | );
251 |
252 | // ============================ Values ============================
253 | const [mergedValue, setValue] = useMergedState(defaultValue, {
254 | value,
255 | });
256 |
257 | const rawValues = React.useMemo(() => {
258 | const valueList =
259 | mergedValue === null || mergedValue === undefined
260 | ? []
261 | : Array.isArray(mergedValue)
262 | ? mergedValue
263 | : [mergedValue];
264 |
265 | const [val0 = mergedMin] = valueList;
266 | let returnValues = mergedValue === null ? [] : [val0];
267 |
268 | // Format as range
269 | if (rangeEnabled) {
270 | returnValues = [...valueList];
271 |
272 | // When count provided or value is `undefined`, we fill values
273 | if (count || mergedValue === undefined) {
274 | const pointCount = count >= 0 ? count + 1 : 2;
275 | returnValues = returnValues.slice(0, pointCount);
276 |
277 | // Fill with count
278 | while (returnValues.length < pointCount) {
279 | returnValues.push(returnValues[returnValues.length - 1] ?? mergedMin);
280 | }
281 | }
282 | returnValues.sort((a, b) => a - b);
283 | }
284 |
285 | // Align in range
286 | returnValues.forEach((val, index) => {
287 | returnValues[index] = formatValue(val);
288 | });
289 |
290 | return returnValues;
291 | }, [mergedValue, rangeEnabled, mergedMin, count, formatValue]);
292 |
293 | // =========================== onChange ===========================
294 | const getTriggerValue = (triggerValues: number[]) =>
295 | rangeEnabled ? triggerValues : triggerValues[0];
296 |
297 | const triggerChange = useEvent((nextValues: number[]) => {
298 | // Order first
299 | const cloneNextValues = [...nextValues].sort((a, b) => a - b);
300 |
301 | // Trigger event if needed
302 | if (onChange && !isEqual(cloneNextValues, rawValues, true)) {
303 | onChange(getTriggerValue(cloneNextValues));
304 | }
305 |
306 | // We set this later since it will re-render component immediately
307 | setValue(cloneNextValues);
308 | });
309 |
310 | const finishChange = useEvent((draggingDelete?: boolean) => {
311 | // Trigger from `useDrag` will tell if it's a delete action
312 | if (draggingDelete) {
313 | handlesRef.current.hideHelp();
314 | }
315 |
316 | const finishValue = getTriggerValue(rawValues);
317 | onAfterChange?.(finishValue);
318 | warning(
319 | !onAfterChange,
320 | '[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
321 | );
322 | onChangeComplete?.(finishValue);
323 | });
324 |
325 | const onDelete = (index: number) => {
326 | if (disabled || !rangeEditable || rawValues.length <= minCount) {
327 | return;
328 | }
329 |
330 | const cloneNextValues = [...rawValues];
331 | cloneNextValues.splice(index, 1);
332 |
333 | onBeforeChange?.(getTriggerValue(cloneNextValues));
334 | triggerChange(cloneNextValues);
335 |
336 | const nextFocusIndex = Math.max(0, index - 1);
337 | handlesRef.current.hideHelp();
338 | handlesRef.current.focus(nextFocusIndex);
339 | };
340 |
341 | const [draggingIndex, draggingValue, draggingDelete, cacheValues, onStartDrag] = useDrag(
342 | containerRef,
343 | direction,
344 | rawValues,
345 | mergedMin,
346 | mergedMax,
347 | formatValue,
348 | triggerChange,
349 | finishChange,
350 | offsetValues,
351 | rangeEditable,
352 | minCount,
353 | );
354 |
355 | /**
356 | * When `rangeEditable` will insert a new value in the values array.
357 | * Else it will replace the value in the values array.
358 | */
359 | const changeToCloseValue = (newValue: number, e?: React.MouseEvent) => {
360 | if (!disabled) {
361 | // Create new values
362 | const cloneNextValues = [...rawValues];
363 |
364 | let valueIndex = 0;
365 | let valueBeforeIndex = 0; // Record the index which value < newValue
366 | let valueDist = mergedMax - mergedMin;
367 |
368 | rawValues.forEach((val, index) => {
369 | const dist = Math.abs(newValue - val);
370 | if (dist <= valueDist) {
371 | valueDist = dist;
372 | valueIndex = index;
373 | }
374 |
375 | if (val < newValue) {
376 | valueBeforeIndex = index;
377 | }
378 | });
379 |
380 | let focusIndex = valueIndex;
381 |
382 | if (rangeEditable && valueDist !== 0 && (!maxCount || rawValues.length < maxCount)) {
383 | cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue);
384 | focusIndex = valueBeforeIndex + 1;
385 | } else {
386 | cloneNextValues[valueIndex] = newValue;
387 | }
388 |
389 | // Fill value to match default 2 (only when `rawValues` is empty)
390 | if (rangeEnabled && !rawValues.length && count === undefined) {
391 | cloneNextValues.push(newValue);
392 | }
393 |
394 | const nextValue = getTriggerValue(cloneNextValues);
395 | onBeforeChange?.(nextValue);
396 | triggerChange(cloneNextValues);
397 |
398 | if (e) {
399 | (document.activeElement as HTMLElement)?.blur?.();
400 | handlesRef.current.focus(focusIndex);
401 | onStartDrag(e, focusIndex, cloneNextValues);
402 | } else {
403 | // https://github.com/ant-design/ant-design/issues/49997
404 | onAfterChange?.(nextValue);
405 | warning(
406 | !onAfterChange,
407 | '[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
408 | );
409 | onChangeComplete?.(nextValue);
410 | }
411 | }
412 | };
413 |
414 | // ============================ Click =============================
415 | const onSliderMouseDown: React.MouseEventHandler = (e) => {
416 | e.preventDefault();
417 |
418 | const { width, height, left, top, bottom, right } =
419 | containerRef.current.getBoundingClientRect();
420 | const { clientX, clientY } = e;
421 |
422 | let percent: number;
423 | switch (direction) {
424 | case 'btt':
425 | percent = (bottom - clientY) / height;
426 | break;
427 |
428 | case 'ttb':
429 | percent = (clientY - top) / height;
430 | break;
431 |
432 | case 'rtl':
433 | percent = (right - clientX) / width;
434 | break;
435 |
436 | default:
437 | percent = (clientX - left) / width;
438 | }
439 |
440 | const nextValue = mergedMin + percent * (mergedMax - mergedMin);
441 | changeToCloseValue(formatValue(nextValue), e);
442 | };
443 |
444 | // =========================== Keyboard ===========================
445 | const [keyboardValue, setKeyboardValue] = React.useState(null);
446 |
447 | const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
448 | if (!disabled) {
449 | const next = offsetValues(rawValues, offset, valueIndex);
450 |
451 | onBeforeChange?.(getTriggerValue(rawValues));
452 | triggerChange(next.values);
453 |
454 | setKeyboardValue(next.value);
455 | }
456 | };
457 |
458 | React.useEffect(() => {
459 | if (keyboardValue !== null) {
460 | const valueIndex = rawValues.indexOf(keyboardValue);
461 | if (valueIndex >= 0) {
462 | handlesRef.current.focus(valueIndex);
463 | }
464 | }
465 |
466 | setKeyboardValue(null);
467 | }, [keyboardValue]);
468 |
469 | // ============================= Drag =============================
470 | const mergedDraggableTrack = React.useMemo(() => {
471 | if (rangeDraggableTrack && mergedStep === null) {
472 | if (process.env.NODE_ENV !== 'production') {
473 | warning(false, '`draggableTrack` is not supported when `step` is `null`.');
474 | }
475 | return false;
476 | }
477 | return rangeDraggableTrack;
478 | }, [rangeDraggableTrack, mergedStep]);
479 |
480 | const onStartMove: OnStartMove = useEvent((e, valueIndex) => {
481 | onStartDrag(e, valueIndex);
482 |
483 | onBeforeChange?.(getTriggerValue(rawValues));
484 | });
485 |
486 | // Auto focus for updated handle
487 | const dragging = draggingIndex !== -1;
488 | React.useEffect(() => {
489 | if (!dragging) {
490 | const valueIndex = rawValues.lastIndexOf(draggingValue);
491 | handlesRef.current.focus(valueIndex);
492 | }
493 | }, [dragging]);
494 |
495 | // =========================== Included ===========================
496 | const sortedCacheValues = React.useMemo(
497 | () => [...cacheValues].sort((a, b) => a - b),
498 | [cacheValues],
499 | );
500 |
501 | // Provide a range values with included [min, max]
502 | // Used for Track, Mark & Dot
503 | const [includedStart, includedEnd] = React.useMemo(() => {
504 | if (!rangeEnabled) {
505 | return [mergedMin, sortedCacheValues[0]];
506 | }
507 |
508 | return [sortedCacheValues[0], sortedCacheValues[sortedCacheValues.length - 1]];
509 | }, [sortedCacheValues, rangeEnabled, mergedMin]);
510 |
511 | // ============================= Refs =============================
512 | React.useImperativeHandle(ref, () => ({
513 | focus: () => {
514 | handlesRef.current.focus(0);
515 | },
516 | blur: () => {
517 | const { activeElement } = document;
518 | if (containerRef.current?.contains(activeElement)) {
519 | (activeElement as HTMLElement)?.blur();
520 | }
521 | },
522 | }));
523 |
524 | // ========================== Auto Focus ==========================
525 | React.useEffect(() => {
526 | if (autoFocus) {
527 | handlesRef.current.focus(0);
528 | }
529 | }, []);
530 |
531 | // =========================== Context ============================
532 | const context = React.useMemo(
533 | () => ({
534 | min: mergedMin,
535 | max: mergedMax,
536 | direction,
537 | disabled,
538 | keyboard,
539 | step: mergedStep,
540 | included,
541 | includedStart,
542 | includedEnd,
543 | range: rangeEnabled,
544 | tabIndex,
545 | ariaLabelForHandle,
546 | ariaLabelledByForHandle,
547 | ariaRequired,
548 | ariaValueTextFormatterForHandle,
549 | styles: styles || {},
550 | classNames: classNames || {},
551 | }),
552 | [
553 | mergedMin,
554 | mergedMax,
555 | direction,
556 | disabled,
557 | keyboard,
558 | mergedStep,
559 | included,
560 | includedStart,
561 | includedEnd,
562 | rangeEnabled,
563 | tabIndex,
564 | ariaLabelForHandle,
565 | ariaLabelledByForHandle,
566 | ariaRequired,
567 | ariaValueTextFormatterForHandle,
568 | styles,
569 | classNames,
570 | ],
571 | );
572 |
573 | // ============================ Render ============================
574 | return (
575 |
576 |
588 |
592 |
593 | {track !== false && (
594 |
601 | )}
602 |
603 |
610 |
611 |
627 |
628 |
629 |
630 |
631 | );
632 | });
633 |
634 | if (process.env.NODE_ENV !== 'production') {
635 | Slider.displayName = 'Slider';
636 | }
637 |
638 | export default Slider;
639 |
--------------------------------------------------------------------------------
/src/Steps/Dot.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import * as React from 'react';
3 | import SliderContext from '../context';
4 | import { getDirectionStyle } from '../util';
5 |
6 | export interface DotProps {
7 | prefixCls: string;
8 | value: number;
9 | style?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
10 | activeStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
11 | }
12 |
13 | const Dot: React.FC = (props) => {
14 | const { prefixCls, value, style, activeStyle } = props;
15 | const { min, max, direction, included, includedStart, includedEnd } =
16 | React.useContext(SliderContext);
17 |
18 | const dotClassName = `${prefixCls}-dot`;
19 | const active = included && includedStart <= value && value <= includedEnd;
20 |
21 | // ============================ Offset ============================
22 | let mergedStyle: React.CSSProperties = {
23 | ...getDirectionStyle(direction, value, min, max),
24 | ...(typeof style === 'function' ? style(value) : style),
25 | };
26 |
27 | if (active) {
28 | mergedStyle = {
29 | ...mergedStyle,
30 | ...(typeof activeStyle === 'function' ? activeStyle(value) : activeStyle),
31 | };
32 | }
33 |
34 | return (
35 |
39 | );
40 | };
41 |
42 | export default Dot;
43 |
--------------------------------------------------------------------------------
/src/Steps/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { InternalMarkObj } from '../Marks';
3 | import SliderContext from '../context';
4 | import Dot from './Dot';
5 |
6 | export interface StepsProps {
7 | prefixCls: string;
8 | marks: InternalMarkObj[];
9 | dots?: boolean;
10 | style?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
11 | activeStyle?: React.CSSProperties | ((dotValue: number) => React.CSSProperties);
12 | }
13 |
14 | const Steps: React.FC = (props) => {
15 | const { prefixCls, marks, dots, style, activeStyle } = props;
16 | const { min, max, step } = React.useContext(SliderContext);
17 |
18 | const stepDots = React.useMemo(() => {
19 | const dotSet = new Set();
20 |
21 | // Add marks
22 | marks.forEach((mark) => {
23 | dotSet.add(mark.value);
24 | });
25 |
26 | // Fill dots
27 | if (dots && step !== null) {
28 | let current = min;
29 | while (current <= max) {
30 | dotSet.add(current);
31 | current += step;
32 | }
33 | }
34 |
35 | return Array.from(dotSet);
36 | }, [min, max, step, dots, marks]);
37 |
38 | return (
39 |
40 | {stepDots.map((dotValue) => (
41 |
48 | ))}
49 |
50 | );
51 | };
52 |
53 | export default Steps;
54 |
--------------------------------------------------------------------------------
/src/Tracks/Track.tsx:
--------------------------------------------------------------------------------
1 | import cls from 'classnames';
2 | import * as React from 'react';
3 | import SliderContext from '../context';
4 | import type { OnStartMove } from '../interface';
5 | import { getOffset } from '../util';
6 |
7 | export interface TrackProps {
8 | prefixCls: string;
9 | style?: React.CSSProperties;
10 | /** Replace with origin prefix concat className */
11 | replaceCls?: string;
12 | start: number;
13 | end: number;
14 | index: number;
15 | onStartMove?: OnStartMove;
16 | }
17 |
18 | const Track: React.FC = (props) => {
19 | const { prefixCls, style, start, end, index, onStartMove, replaceCls } = props;
20 | const { direction, min, max, disabled, range, classNames } = React.useContext(SliderContext);
21 |
22 | const trackPrefixCls = `${prefixCls}-track`;
23 |
24 | const offsetStart = getOffset(start, min, max);
25 | const offsetEnd = getOffset(end, min, max);
26 |
27 | // ============================ Events ============================
28 | const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
29 | if (!disabled && onStartMove) {
30 | onStartMove(e, -1);
31 | }
32 | };
33 |
34 | // ============================ Render ============================
35 | const positionStyle: React.CSSProperties = {};
36 |
37 | switch (direction) {
38 | case 'rtl':
39 | positionStyle.right = `${offsetStart * 100}%`;
40 | positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`;
41 | break;
42 |
43 | case 'btt':
44 | positionStyle.bottom = `${offsetStart * 100}%`;
45 | positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`;
46 | break;
47 |
48 | case 'ttb':
49 | positionStyle.top = `${offsetStart * 100}%`;
50 | positionStyle.height = `${offsetEnd * 100 - offsetStart * 100}%`;
51 | break;
52 |
53 | default:
54 | positionStyle.left = `${offsetStart * 100}%`;
55 | positionStyle.width = `${offsetEnd * 100 - offsetStart * 100}%`;
56 | }
57 |
58 | const className =
59 | replaceCls ||
60 | cls(
61 | trackPrefixCls,
62 | {
63 | [`${trackPrefixCls}-${index + 1}`]: index !== null && range,
64 | [`${prefixCls}-track-draggable`]: onStartMove,
65 | },
66 | classNames.track,
67 | );
68 |
69 | return (
70 |
76 | );
77 | };
78 |
79 | export default Track;
80 |
--------------------------------------------------------------------------------
/src/Tracks/index.tsx:
--------------------------------------------------------------------------------
1 | import cls from 'classnames';
2 | import * as React from 'react';
3 | import SliderContext from '../context';
4 | import type { OnStartMove } from '../interface';
5 | import { getIndex } from '../util';
6 | import Track from './Track';
7 |
8 | export interface TrackProps {
9 | prefixCls: string;
10 | style?: React.CSSProperties | React.CSSProperties[];
11 | values: number[];
12 | onStartMove?: OnStartMove;
13 | startPoint?: number;
14 | }
15 |
16 | const Tracks: React.FC = (props) => {
17 | const { prefixCls, style, values, startPoint, onStartMove } = props;
18 | const { included, range, min, styles, classNames } = React.useContext(SliderContext);
19 |
20 | // =========================== List ===========================
21 | const trackList = React.useMemo(() => {
22 | if (!range) {
23 | // null value do not have track
24 | if (values.length === 0) {
25 | return [];
26 | }
27 |
28 | const startValue = startPoint ?? min;
29 | const endValue = values[0];
30 |
31 | return [{ start: Math.min(startValue, endValue), end: Math.max(startValue, endValue) }];
32 | }
33 |
34 | // Multiple
35 | const list: { start: number; end: number }[] = [];
36 |
37 | for (let i = 0; i < values.length - 1; i += 1) {
38 | list.push({ start: values[i], end: values[i + 1] });
39 | }
40 |
41 | return list;
42 | }, [values, range, startPoint, min]);
43 |
44 | if (!included) {
45 | return null;
46 | }
47 |
48 | // ========================== Render ==========================
49 | const tracksNode =
50 | trackList?.length && (classNames.tracks || styles.tracks) ? (
51 |
59 | ) : null;
60 |
61 | return (
62 | <>
63 | {tracksNode}
64 | {trackList.map(({ start, end }, index) => (
65 |
74 | ))}
75 | >
76 | );
77 | };
78 |
79 | export default Tracks;
80 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { AriaValueFormat, Direction, SliderClassNames, SliderStyles } from './interface';
3 |
4 | export interface SliderContextProps {
5 | min: number;
6 | max: number;
7 | includedStart: number;
8 | includedEnd: number;
9 | direction: Direction;
10 | disabled?: boolean;
11 | keyboard?: boolean;
12 | included?: boolean;
13 | step: number | null;
14 | range?: boolean;
15 | tabIndex: number | number[];
16 | ariaLabelForHandle?: string | string[];
17 | ariaLabelledByForHandle?: string | string[];
18 | ariaRequired?: boolean;
19 | ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
20 | classNames: SliderClassNames;
21 | styles: SliderStyles;
22 | }
23 |
24 | const SliderContext = React.createContext({
25 | min: 0,
26 | max: 0,
27 | direction: 'ltr',
28 | step: 1,
29 | includedStart: 0,
30 | includedEnd: 0,
31 | tabIndex: 0,
32 | keyboard: true,
33 | styles: {},
34 | classNames: {},
35 | });
36 |
37 | export default SliderContext;
38 |
39 | export interface UnstableContextProps {
40 | onDragStart?: (info: {
41 | rawValues: number[];
42 | draggingIndex: number;
43 | draggingValue: number;
44 | }) => void;
45 | onDragChange?: (info: {
46 | rawValues: number[];
47 | deleteIndex: number;
48 | draggingIndex: number;
49 | draggingValue: number;
50 | }) => void;
51 | }
52 |
53 | /** @private NOT PROMISE AVAILABLE. DO NOT USE IN PRODUCTION. */
54 | export const UnstableContext = React.createContext({});
55 |
--------------------------------------------------------------------------------
/src/hooks/useDrag.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import useEvent from 'rc-util/lib/hooks/useEvent';
3 | import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
4 | import { UnstableContext } from '../context';
5 | import type { Direction, OnStartMove } from '../interface';
6 | import type { OffsetValues } from './useOffset';
7 |
8 | /** Drag to delete offset. It's a user experience number for dragging out */
9 | const REMOVE_DIST = 130;
10 |
11 | function getPosition(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) {
12 | const obj = 'targetTouches' in e ? e.targetTouches[0] : e;
13 |
14 | return { pageX: obj.pageX, pageY: obj.pageY };
15 | }
16 |
17 | function useDrag(
18 | containerRef: React.RefObject,
19 | direction: Direction,
20 | rawValues: number[],
21 | min: number,
22 | max: number,
23 | formatValue: (value: number) => number,
24 | triggerChange: (values: number[]) => void,
25 | finishChange: (draggingDelete: boolean) => void,
26 | offsetValues: OffsetValues,
27 | editable: boolean,
28 | minCount: number,
29 | ): [
30 | draggingIndex: number,
31 | draggingValue: number,
32 | draggingDelete: boolean,
33 | returnValues: number[],
34 | onStartMove: OnStartMove,
35 | ] {
36 | const [draggingValue, setDraggingValue] = React.useState(null);
37 | const [draggingIndex, setDraggingIndex] = React.useState(-1);
38 | const [draggingDelete, setDraggingDelete] = React.useState(false);
39 | const [cacheValues, setCacheValues] = React.useState(rawValues);
40 | const [originValues, setOriginValues] = React.useState(rawValues);
41 |
42 | const mouseMoveEventRef = React.useRef<(event: MouseEvent) => void>(null);
43 | const mouseUpEventRef = React.useRef<(event: MouseEvent) => void>(null);
44 | const touchEventTargetRef = React.useRef(null);
45 |
46 | const { onDragStart, onDragChange } = React.useContext(UnstableContext);
47 |
48 | useLayoutEffect(() => {
49 | if (draggingIndex === -1) {
50 | setCacheValues(rawValues);
51 | }
52 | }, [rawValues, draggingIndex]);
53 |
54 | // Clean up event
55 | React.useEffect(
56 | () => () => {
57 | document.removeEventListener('mousemove', mouseMoveEventRef.current);
58 | document.removeEventListener('mouseup', mouseUpEventRef.current);
59 | if (touchEventTargetRef.current) {
60 | touchEventTargetRef.current.removeEventListener('touchmove', mouseMoveEventRef.current);
61 | touchEventTargetRef.current.removeEventListener('touchend', mouseUpEventRef.current);
62 | }
63 | },
64 | [],
65 | );
66 |
67 | const flushValues = (nextValues: number[], nextValue?: number, deleteMark?: boolean) => {
68 | // Perf: Only update state when value changed
69 | if (nextValue !== undefined) {
70 | setDraggingValue(nextValue);
71 | }
72 | setCacheValues(nextValues);
73 |
74 | let changeValues = nextValues;
75 | if (deleteMark) {
76 | changeValues = nextValues.filter((_, i) => i !== draggingIndex);
77 | }
78 | triggerChange(changeValues);
79 |
80 | if (onDragChange) {
81 | onDragChange({
82 | rawValues: nextValues,
83 | deleteIndex: deleteMark ? draggingIndex : -1,
84 | draggingIndex,
85 | draggingValue: nextValue,
86 | });
87 | }
88 | };
89 |
90 | const updateCacheValue = useEvent(
91 | (valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
92 | if (valueIndex === -1) {
93 | // >>>> Dragging on the track
94 | const startValue = originValues[0];
95 | const endValue = originValues[originValues.length - 1];
96 | const maxStartOffset = min - startValue;
97 | const maxEndOffset = max - endValue;
98 |
99 | // Get valid offset
100 | let offset = offsetPercent * (max - min);
101 | offset = Math.max(offset, maxStartOffset);
102 | offset = Math.min(offset, maxEndOffset);
103 |
104 | // Use first value to revert back of valid offset (like steps marks)
105 | const formatStartValue = formatValue(startValue + offset);
106 | offset = formatStartValue - startValue;
107 | const cloneCacheValues = originValues.map((val) => val + offset);
108 | flushValues(cloneCacheValues);
109 | } else {
110 | // >>>> Dragging on the handle
111 | const offsetDist = (max - min) * offsetPercent;
112 |
113 | // Always start with the valueIndex origin value
114 | const cloneValues = [...cacheValues];
115 | cloneValues[valueIndex] = originValues[valueIndex];
116 |
117 | const next = offsetValues(cloneValues, offsetDist, valueIndex, 'dist');
118 |
119 | flushValues(next.values, next.value, deleteMark);
120 | }
121 | },
122 | );
123 |
124 | const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => {
125 | e.stopPropagation();
126 |
127 | // 如果是点击 track 触发的,需要传入变化后的初始值,而不能直接用 rawValues
128 | const initialValues = startValues || rawValues;
129 | const originValue = initialValues[valueIndex];
130 |
131 | setDraggingIndex(valueIndex);
132 | setDraggingValue(originValue);
133 | setOriginValues(initialValues);
134 | setCacheValues(initialValues);
135 | setDraggingDelete(false);
136 |
137 | const { pageX: startX, pageY: startY } = getPosition(e);
138 |
139 | // We declare it here since closure can't get outer latest value
140 | let deleteMark = false;
141 |
142 | // Internal trigger event
143 | if (onDragStart) {
144 | onDragStart({
145 | rawValues: initialValues,
146 | draggingIndex: valueIndex,
147 | draggingValue: originValue,
148 | });
149 | }
150 |
151 | // Moving
152 | const onMouseMove = (event: MouseEvent | TouchEvent) => {
153 | event.preventDefault();
154 |
155 | const { pageX: moveX, pageY: moveY } = getPosition(event);
156 | const offsetX = moveX - startX;
157 | const offsetY = moveY - startY;
158 |
159 | const { width, height } = containerRef.current.getBoundingClientRect();
160 |
161 | let offSetPercent: number;
162 | let removeDist: number;
163 |
164 | switch (direction) {
165 | case 'btt':
166 | offSetPercent = -offsetY / height;
167 | removeDist = offsetX;
168 | break;
169 |
170 | case 'ttb':
171 | offSetPercent = offsetY / height;
172 | removeDist = offsetX;
173 | break;
174 |
175 | case 'rtl':
176 | offSetPercent = -offsetX / width;
177 | removeDist = offsetY;
178 | break;
179 |
180 | default:
181 | offSetPercent = offsetX / width;
182 | removeDist = offsetY;
183 | }
184 |
185 | // Check if need mark remove
186 | deleteMark = editable
187 | ? Math.abs(removeDist) > REMOVE_DIST && minCount < cacheValues.length
188 | : false;
189 | setDraggingDelete(deleteMark);
190 |
191 | updateCacheValue(valueIndex, offSetPercent, deleteMark);
192 | };
193 |
194 | // End
195 | const onMouseUp = (event: MouseEvent | TouchEvent) => {
196 | event.preventDefault();
197 |
198 | document.removeEventListener('mouseup', onMouseUp);
199 | document.removeEventListener('mousemove', onMouseMove);
200 | if (touchEventTargetRef.current) {
201 | touchEventTargetRef.current.removeEventListener('touchmove', mouseMoveEventRef.current);
202 | touchEventTargetRef.current.removeEventListener('touchend', mouseUpEventRef.current);
203 | }
204 | mouseMoveEventRef.current = null;
205 | mouseUpEventRef.current = null;
206 | touchEventTargetRef.current = null;
207 |
208 | finishChange(deleteMark);
209 |
210 | setDraggingIndex(-1);
211 | setDraggingDelete(false);
212 | };
213 |
214 | document.addEventListener('mouseup', onMouseUp);
215 | document.addEventListener('mousemove', onMouseMove);
216 | e.currentTarget.addEventListener('touchend', onMouseUp);
217 | e.currentTarget.addEventListener('touchmove', onMouseMove);
218 | mouseMoveEventRef.current = onMouseMove;
219 | mouseUpEventRef.current = onMouseUp;
220 | touchEventTargetRef.current = e.currentTarget;
221 | };
222 |
223 | // Only return cache value when it mapping with rawValues
224 | const returnValues = React.useMemo(() => {
225 | const sourceValues = [...rawValues].sort((a, b) => a - b);
226 | const targetValues = [...cacheValues].sort((a, b) => a - b);
227 |
228 | const counts: Record = {};
229 | targetValues.forEach((val) => {
230 | counts[val] = (counts[val] || 0) + 1;
231 | });
232 | sourceValues.forEach((val) => {
233 | counts[val] = (counts[val] || 0) - 1;
234 | });
235 |
236 | const maxDiffCount = editable ? 1 : 0;
237 | const diffCount: number = Object.values(counts).reduce(
238 | (prev, next) => prev + Math.abs(next),
239 | 0,
240 | );
241 |
242 | return diffCount <= maxDiffCount ? cacheValues : rawValues;
243 | }, [rawValues, cacheValues, editable]);
244 |
245 | return [draggingIndex, draggingValue, draggingDelete, returnValues, onStartMove];
246 | }
247 |
248 | export default useDrag;
249 |
--------------------------------------------------------------------------------
/src/hooks/useOffset.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import type { InternalMarkObj } from '../Marks';
3 |
4 | /** Format the value in the range of [min, max] */
5 | type FormatRangeValue = (value: number) => number;
6 |
7 | /** Format value align with step */
8 | type FormatStepValue = (value: number) => number;
9 |
10 | /** Format value align with step & marks */
11 | type FormatValue = (value: number) => number;
12 |
13 | type OffsetMode = 'unit' | 'dist';
14 |
15 | type OffsetValue = (
16 | values: number[],
17 | offset: number | 'min' | 'max',
18 | valueIndex: number,
19 | mode?: OffsetMode,
20 | ) => number;
21 |
22 | export type OffsetValues = (
23 | values: number[],
24 | offset: number | 'min' | 'max',
25 | valueIndex: number,
26 | mode?: OffsetMode,
27 | ) => {
28 | value: number;
29 | values: number[];
30 | };
31 |
32 | export default function useOffset(
33 | min: number,
34 | max: number,
35 | step: number,
36 | markList: InternalMarkObj[],
37 | allowCross: boolean,
38 | pushable: false | number,
39 | ): [FormatValue, OffsetValues] {
40 | const formatRangeValue: FormatRangeValue = React.useCallback(
41 | (val) => Math.max(min, Math.min(max, val)),
42 | [min, max],
43 | );
44 |
45 | const formatStepValue: FormatStepValue = React.useCallback(
46 | (val) => {
47 | if (step !== null) {
48 | const stepValue = min + Math.round((formatRangeValue(val) - min) / step) * step;
49 |
50 | // Cut number in case to be like 0.30000000000000004
51 | const getDecimal = (num: number) => (String(num).split('.')[1] || '').length;
52 | const maxDecimal = Math.max(getDecimal(step), getDecimal(max), getDecimal(min));
53 | const fixedValue = Number(stepValue.toFixed(maxDecimal));
54 |
55 | return min <= fixedValue && fixedValue <= max ? fixedValue : null;
56 | }
57 | return null;
58 | },
59 | [step, min, max, formatRangeValue],
60 | );
61 |
62 | const formatValue = React.useCallback(
63 | (val) => {
64 | const formatNextValue = formatRangeValue(val);
65 |
66 | // List align values
67 | const alignValues = markList.map((mark) => mark.value);
68 | if (step !== null) {
69 | alignValues.push(formatStepValue(val));
70 | }
71 |
72 | // min & max
73 | alignValues.push(min, max);
74 |
75 | // Align with marks
76 | let closeValue = alignValues[0];
77 | let closeDist = max - min;
78 |
79 | alignValues.forEach((alignValue) => {
80 | const dist = Math.abs(formatNextValue - alignValue);
81 | if (dist <= closeDist) {
82 | closeValue = alignValue;
83 | closeDist = dist;
84 | }
85 | });
86 |
87 | return closeValue;
88 | },
89 | [min, max, markList, step, formatRangeValue, formatStepValue],
90 | );
91 |
92 | // ========================== Offset ==========================
93 | // Single Value
94 | const offsetValue: OffsetValue = (values, offset, valueIndex, mode = 'unit') => {
95 | if (typeof offset === 'number') {
96 | let nextValue: number;
97 | const originValue = values[valueIndex];
98 |
99 | // Only used for `dist` mode
100 | const targetDistValue = originValue + offset;
101 |
102 | // Compare next step value & mark value which is best match
103 | let potentialValues: number[] = [];
104 | markList.forEach((mark) => {
105 | potentialValues.push(mark.value);
106 | });
107 |
108 | // Min & Max
109 | potentialValues.push(min, max);
110 |
111 | // In case origin value is align with mark but not with step
112 | potentialValues.push(formatStepValue(originValue));
113 |
114 | // Put offset step value also
115 | const sign = offset > 0 ? 1 : -1;
116 |
117 | if (mode === 'unit') {
118 | potentialValues.push(formatStepValue(originValue + sign * step));
119 | } else {
120 | potentialValues.push(formatStepValue(targetDistValue));
121 | }
122 |
123 | // Find close one
124 | potentialValues = potentialValues
125 | .filter((val) => val !== null)
126 | // Remove reverse value
127 | .filter((val) => (offset < 0 ? val <= originValue : val >= originValue));
128 |
129 | if (mode === 'unit') {
130 | // `unit` mode can not contain itself
131 | potentialValues = potentialValues.filter((val) => val !== originValue);
132 | }
133 |
134 | const compareValue = mode === 'unit' ? originValue : targetDistValue;
135 |
136 | nextValue = potentialValues[0];
137 | let valueDist = Math.abs(nextValue - compareValue);
138 |
139 | potentialValues.forEach((potentialValue) => {
140 | const dist = Math.abs(potentialValue - compareValue);
141 | if (dist < valueDist) {
142 | nextValue = potentialValue;
143 | valueDist = dist;
144 | }
145 | });
146 |
147 | // Out of range will back to range
148 | if (nextValue === undefined) {
149 | return offset < 0 ? min : max;
150 | }
151 |
152 | // `dist` mode
153 | if (mode === 'dist') {
154 | return nextValue;
155 | }
156 |
157 | // `unit` mode may need another round
158 | if (Math.abs(offset) > 1) {
159 | const cloneValues = [...values];
160 | cloneValues[valueIndex] = nextValue;
161 |
162 | return offsetValue(cloneValues, offset - sign, valueIndex, mode);
163 | }
164 |
165 | return nextValue;
166 | } else if (offset === 'min') {
167 | return min;
168 | } else if (offset === 'max') {
169 | return max;
170 | }
171 | };
172 |
173 | /** Same as `offsetValue` but return `changed` mark to tell value changed */
174 | const offsetChangedValue = (
175 | values: number[],
176 | offset: number,
177 | valueIndex: number,
178 | mode: OffsetMode = 'unit',
179 | ) => {
180 | const originValue = values[valueIndex];
181 | const nextValue = offsetValue(values, offset, valueIndex, mode);
182 | return {
183 | value: nextValue,
184 | changed: nextValue !== originValue,
185 | };
186 | };
187 |
188 | const needPush = (dist: number) => {
189 | return (pushable === null && dist === 0) || (typeof pushable === 'number' && dist < pushable);
190 | };
191 |
192 | // Values
193 | const offsetValues: OffsetValues = (values, offset, valueIndex, mode = 'unit') => {
194 | const nextValues = values.map(formatValue);
195 | const originValue = nextValues[valueIndex];
196 | const nextValue = offsetValue(nextValues, offset, valueIndex, mode);
197 | nextValues[valueIndex] = nextValue;
198 |
199 | if (allowCross === false) {
200 | // >>>>> Allow Cross
201 | const pushNum = pushable || 0;
202 |
203 | // ============ AllowCross ===============
204 | if (valueIndex > 0 && nextValues[valueIndex - 1] !== originValue) {
205 | nextValues[valueIndex] = Math.max(
206 | nextValues[valueIndex],
207 | nextValues[valueIndex - 1] + pushNum,
208 | );
209 | }
210 |
211 | if (valueIndex < nextValues.length - 1 && nextValues[valueIndex + 1] !== originValue) {
212 | nextValues[valueIndex] = Math.min(
213 | nextValues[valueIndex],
214 | nextValues[valueIndex + 1] - pushNum,
215 | );
216 | }
217 | } else if (typeof pushable === 'number' || pushable === null) {
218 | // >>>>> Pushable
219 | // =============== Push ==================
220 |
221 | // >>>>>> Basic push
222 | // End values
223 | for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
224 | let changed = true;
225 | while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
226 | ({ value: nextValues[i], changed } = offsetChangedValue(nextValues, 1, i));
227 | }
228 | }
229 |
230 | // Start values
231 | for (let i = valueIndex; i > 0; i -= 1) {
232 | let changed = true;
233 | while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
234 | ({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
235 | }
236 | }
237 |
238 | // >>>>> Revert back to safe push range
239 | // End to Start
240 | for (let i = nextValues.length - 1; i > 0; i -= 1) {
241 | let changed = true;
242 | while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
243 | ({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
244 | }
245 | }
246 |
247 | // Start to End
248 | for (let i = 0; i < nextValues.length - 1; i += 1) {
249 | let changed = true;
250 | while (needPush(nextValues[i + 1] - nextValues[i]) && changed) {
251 | ({ value: nextValues[i + 1], changed } = offsetChangedValue(nextValues, 1, i + 1));
252 | }
253 | }
254 | }
255 |
256 | return {
257 | value: nextValues[valueIndex],
258 | values: nextValues,
259 | };
260 | };
261 |
262 | return [formatValue, offsetValues];
263 | }
264 |
--------------------------------------------------------------------------------
/src/hooks/useRange.ts:
--------------------------------------------------------------------------------
1 | import { warning } from 'rc-util/lib/warning';
2 | import { useMemo } from 'react';
3 | import type { SliderProps } from '../Slider';
4 |
5 | export default function useRange(
6 | range?: SliderProps['range'],
7 | ): [
8 | range: boolean,
9 | rangeEditable: boolean,
10 | rangeDraggableTrack: boolean,
11 | minCount: number,
12 | maxCount?: number,
13 | ] {
14 | return useMemo(() => {
15 | if (range === true || !range) {
16 | return [!!range, false, false, 0];
17 | }
18 |
19 | const { editable, draggableTrack, minCount, maxCount } = range;
20 |
21 | if (process.env.NODE_ENV !== 'production') {
22 | warning(!editable || !draggableTrack, '`editable` can not work with `draggableTrack`.');
23 | }
24 |
25 | return [true, editable, !editable && draggableTrack, minCount || 0, maxCount];
26 | }, [range]);
27 | }
28 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SliderProps, SliderRef } from './Slider';
2 | import Slider from './Slider';
3 | export { UnstableContext } from './context';
4 |
5 | export type { SliderProps, SliderRef };
6 |
7 | export default Slider;
8 |
--------------------------------------------------------------------------------
/src/interface.ts:
--------------------------------------------------------------------------------
1 | import type React from 'react';
2 |
3 | export type Direction = 'rtl' | 'ltr' | 'ttb' | 'btt';
4 |
5 | export type OnStartMove = (
6 | e: React.MouseEvent | React.TouchEvent,
7 | valueIndex: number,
8 | startValues?: number[],
9 | ) => void;
10 |
11 | export type AriaValueFormat = (value: number) => string;
12 |
13 | export type SemanticName = 'tracks' | 'track' | 'rail' | 'handle';
14 |
15 | export type SliderClassNames = Partial>;
16 |
17 | export type SliderStyles = Partial>;
18 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import type { Direction } from './interface';
2 |
3 | export function getOffset(value: number, min: number, max: number) {
4 | return (value - min) / (max - min);
5 | }
6 |
7 | export function getDirectionStyle(direction: Direction, value: number, min: number, max: number) {
8 | const offset = getOffset(value, min, max);
9 |
10 | const positionStyle: React.CSSProperties = {};
11 |
12 | switch (direction) {
13 | case 'rtl':
14 | positionStyle.right = `${offset * 100}%`;
15 | positionStyle.transform = 'translateX(50%)';
16 | break;
17 |
18 | case 'btt':
19 | positionStyle.bottom = `${offset * 100}%`;
20 | positionStyle.transform = 'translateY(50%)';
21 | break;
22 |
23 | case 'ttb':
24 | positionStyle.top = `${offset * 100}%`;
25 | positionStyle.transform = 'translateY(-50%)';
26 | break;
27 |
28 | default:
29 | positionStyle.left = `${offset * 100}%`;
30 | positionStyle.transform = 'translateX(-50%)';
31 | break;
32 | }
33 |
34 | return positionStyle;
35 | }
36 |
37 | /** Return index value if is list or return value directly */
38 | export function getIndex(value: T | T[], index: number) {
39 | return Array.isArray(value) ? value[index] : value;
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Range.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-undef, react/no-string-refs, no-param-reassign, max-classes-per-file */
2 | import '@testing-library/jest-dom';
3 | import { createEvent, fireEvent, render } from '@testing-library/react';
4 | import keyCode from 'rc-util/lib/KeyCode';
5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
6 | import { resetWarned } from 'rc-util/lib/warning';
7 | import React from 'react';
8 | import Slider from '../src';
9 |
10 | describe('Range', () => {
11 | beforeAll(() => {
12 | spyElementPrototypes(HTMLElement, {
13 | getBoundingClientRect: () => ({
14 | width: 100,
15 | height: 100,
16 | left: 0,
17 | top: 0,
18 | bottom: 100,
19 | right: 100,
20 | }),
21 | });
22 | });
23 |
24 | beforeEach(() => {
25 | resetWarned();
26 | });
27 |
28 | function doMouseDown(
29 | container: HTMLElement,
30 | start: number,
31 | element = 'rc-slider-handle',
32 | skipEventCheck = false,
33 | ) {
34 | const ele = container.getElementsByClassName(element)[0];
35 | const mouseDown = createEvent.mouseDown(ele);
36 | (mouseDown as any).pageX = start;
37 | (mouseDown as any).pageY = start;
38 |
39 | const preventDefault = jest.fn();
40 |
41 | Object.defineProperties(mouseDown, {
42 | clientX: { get: () => start },
43 | clientY: { get: () => start },
44 | preventDefault: { value: preventDefault },
45 | });
46 |
47 | fireEvent.mouseEnter(ele);
48 | fireEvent(ele, mouseDown);
49 |
50 | // Should not prevent default since focus will not change
51 | if (!skipEventCheck) {
52 | expect(preventDefault).not.toHaveBeenCalled();
53 | }
54 | }
55 |
56 | function doMouseDrag(end: number) {
57 | const mouseMove = createEvent.mouseMove(document);
58 | (mouseMove as any).pageX = end;
59 | (mouseMove as any).pageY = end;
60 | fireEvent(document, mouseMove);
61 | }
62 |
63 | function doMouseMove(
64 | container: HTMLElement,
65 | start: number,
66 | end: number,
67 | element = 'rc-slider-handle',
68 | ) {
69 | doMouseDown(container, start, element);
70 |
71 | // Drag
72 | doMouseDrag(end);
73 | }
74 |
75 | function doTouchMove(
76 | container: HTMLElement,
77 | start: number,
78 | end: number,
79 | element = 'rc-slider-handle',
80 | ) {
81 | const touchStart = createEvent.touchStart(container.getElementsByClassName(element)[0], {
82 | touches: [{}],
83 | targetTouches: [{}],
84 | });
85 | (touchStart as any).targetTouches[0].pageX = start;
86 | fireEvent(container.getElementsByClassName(element)[0], touchStart);
87 |
88 | // Drag
89 | const touchMove = createEvent.touchMove(container.getElementsByClassName(element)[0], {
90 | touches: [{}],
91 | targetTouches: [{}],
92 | });
93 | (touchMove as any).targetTouches[0].pageX = end;
94 | fireEvent(container.getElementsByClassName(element)[0], touchMove);
95 | }
96 |
97 | it('should render Range with correct DOM structure', () => {
98 | const { asFragment } = render( );
99 | expect(asFragment().firstChild).toMatchSnapshot();
100 | });
101 |
102 | it('should render Multi-Range with correct DOM structure', () => {
103 | const { asFragment } = render( );
104 | expect(asFragment().firstChild).toMatchSnapshot();
105 | });
106 |
107 | it('should render Range with value correctly', async () => {
108 | const { container } = render( );
109 |
110 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('left: 0%');
111 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('left: 50%');
112 |
113 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
114 | 'left: 0%; width: 50%',
115 | );
116 | });
117 |
118 | it('should render reverse Range with value correctly', () => {
119 | const { container } = render( );
120 |
121 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('right: 0%');
122 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('right: 50%');
123 |
124 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
125 | 'right: 0%; width: 50%',
126 | );
127 | });
128 |
129 | it('should render Range with tabIndex correctly', () => {
130 | const { container } = render( );
131 |
132 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
133 | 'tabIndex',
134 | '1',
135 | );
136 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
137 | 'tabIndex',
138 | '2',
139 | );
140 | });
141 |
142 | it('should render Range without tabIndex (equal null) correctly', () => {
143 | const { container } = render( );
144 | expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
145 | expect(container.getElementsByClassName('rc-slider-handle')[1]).not.toHaveAttribute('tabIndex');
146 | });
147 |
148 | it('it should trigger onAfterChange when key pressed', () => {
149 | const onAfterChange = jest.fn();
150 | const { container } = render(
151 | ,
152 | );
153 |
154 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
155 | keyCode: keyCode.RIGHT,
156 | });
157 | expect(onAfterChange).not.toHaveBeenCalled();
158 |
159 | fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[1], {
160 | keyCode: keyCode.RIGHT,
161 | });
162 |
163 | expect(onAfterChange).toHaveBeenCalled();
164 | });
165 |
166 | it('should not change value from keyboard events when disabled', () => {
167 | const onAfterChange = jest.fn();
168 | const { container } = render(
169 | ,
170 | );
171 |
172 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
173 | keyCode: keyCode.RIGHT,
174 | });
175 |
176 | expect(onAfterChange).not.toBeCalled();
177 | });
178 |
179 | it('should render Multi-Range with value correctly', () => {
180 | const { container } = render( );
181 |
182 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle('left: 0%');
183 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveStyle('left: 25%');
184 | expect(container.getElementsByClassName('rc-slider-handle')[2]).toHaveStyle('left: 50%');
185 | expect(container.getElementsByClassName('rc-slider-handle')[3]).toHaveStyle('left: 75%');
186 |
187 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle(
188 | 'left: 0%; width: 25%',
189 | );
190 |
191 | expect(container.getElementsByClassName('rc-slider-track')[1]).toHaveStyle(
192 | 'left: 25%; width: 25%',
193 | );
194 |
195 | expect(container.getElementsByClassName('rc-slider-track')[2]).toHaveStyle(
196 | 'left: 50%; width: 25%',
197 | );
198 | });
199 |
200 | it('should update Range correctly in controlled model', () => {
201 | const { container, rerender } = render( );
202 | expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(3);
203 |
204 | rerender( );
205 | expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(2);
206 | });
207 |
208 | it('not moved if controlled', () => {
209 | const onChange = jest.fn();
210 | const { container } = render( );
211 | doMouseMove(container, 0, 9999999);
212 |
213 | expect(onChange).toHaveBeenCalled();
214 |
215 | expect(container.querySelector('.rc-slider-handle-dragging')).toHaveStyle({
216 | left: '2%',
217 | });
218 | });
219 |
220 | // Not trigger onChange anymore
221 | // it('should only update bounds that are out of range', () => {
222 | // const props = { min: 0, max: 10000, value: [0.01, 10000], onChange: jest.fn() };
223 | // const range = mount( );
224 | // range.setProps({ min: 0, max: 500 });
225 |
226 | // expect(props.onChange).toHaveBeenCalledWith([0.01, 500]);
227 | // });
228 |
229 | // Not trigger onChange anymore
230 | // it('should only update bounds if they are out of range', () => {
231 | // const props = { min: 0, max: 10000, value: [0.01, 10000], onChange: jest.fn() };
232 | // const range = mount( );
233 | // range.setProps({ min: 0, max: 500, value: [0.01, 466] });
234 |
235 | // expect(props.onChange).toHaveBeenCalledTimes(0);
236 | // });
237 |
238 | // https://github.com/react-component/slider/pull/256
239 | // Move to antd instead
240 | // it('should handle multi handle mouseEnter correctly', () => {
241 | // const wrapper = mount( );
242 | // wrapper.find('.rc-slider-handle').at(1).simulate('mouseEnter');
243 | // expect(wrapper.state().visibles[0]).toBe(true);
244 | // wrapper.find('.rc-slider-handle').at(3).simulate('mouseEnter');
245 | // expect(wrapper.state().visibles[1]).toBe(true);
246 | // wrapper.find('.rc-slider-handle').at(1).simulate('mouseLeave');
247 | // expect(wrapper.state().visibles[0]).toBe(false);
248 | // wrapper.find('.rc-slider-handle').at(3).simulate('mouseLeave');
249 | // expect(wrapper.state().visibles[1]).toBe(false);
250 | // });
251 |
252 | it('should keep pushable when not allowCross', () => {
253 | const onChange = jest.fn();
254 | const { container } = render(
255 | ,
256 | );
257 |
258 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
259 | keyCode: keyCode.UP,
260 | });
261 | expect(onChange).toHaveBeenCalledWith([30, 40]);
262 |
263 | onChange.mockReset();
264 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
265 | keyCode: keyCode.UP,
266 | });
267 | expect(onChange).not.toHaveBeenCalled();
268 |
269 | onChange.mockReset();
270 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
271 | keyCode: keyCode.UP,
272 | });
273 | expect(onChange).toHaveBeenCalledWith([30, 41]);
274 |
275 | // Push to the edge
276 | for (let i = 0; i < 99; i += 1) {
277 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
278 | keyCode: keyCode.DOWN,
279 | });
280 | }
281 | expect(onChange).toHaveBeenCalledWith([30, 40]);
282 |
283 | onChange.mockReset();
284 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
285 | keyCode: keyCode.DOWN,
286 | });
287 | expect(onChange).not.toHaveBeenCalled();
288 | });
289 |
290 | it('pushable & allowCross', () => {
291 | const onChange = jest.fn();
292 | const { container } = render(
293 | ,
294 | );
295 |
296 | // Left to Right
297 | for (let i = 0; i < 99; i += 1) {
298 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
299 | keyCode: keyCode.UP,
300 | });
301 | }
302 | expect(onChange).toHaveBeenCalledWith([80, 90, 100]);
303 |
304 | // Center to Left
305 | for (let i = 0; i < 99; i += 1) {
306 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
307 | keyCode: keyCode.DOWN,
308 | });
309 | }
310 | expect(onChange).toHaveBeenCalledWith([0, 10, 100]);
311 |
312 | // Right to Right
313 | for (let i = 0; i < 99; i += 1) {
314 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[2], {
315 | keyCode: keyCode.DOWN,
316 | });
317 | }
318 | expect(onChange).toHaveBeenCalledWith([0, 10, 20]);
319 |
320 | // Center to Right
321 | for (let i = 0; i < 99; i += 1) {
322 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
323 | keyCode: keyCode.UP,
324 | });
325 | }
326 | expect(onChange).toHaveBeenCalledWith([0, 90, 100]);
327 | });
328 |
329 | describe('should render correctly when allowCross', () => {
330 | function testLTR(name, func) {
331 | it(name, () => {
332 | const onChange = jest.fn();
333 | const { container, unmount } = render(
334 | ,
335 | );
336 |
337 | // Do move
338 | func(container);
339 |
340 | expect(onChange).toHaveBeenCalledWith([40, 100]);
341 |
342 | unmount();
343 | });
344 | }
345 |
346 | testLTR('mouse', (container) => doMouseMove(container, 0, 9999));
347 | testLTR('touch', (container) => doTouchMove(container, 0, 9999));
348 |
349 | it('reverse', () => {
350 | const onChange = jest.fn();
351 | const { container } = render(
352 | ,
353 | );
354 |
355 | // Do move
356 | doMouseMove(container, 0, -10);
357 |
358 | expect(onChange).toHaveBeenCalledWith([30, 40]);
359 | });
360 |
361 | it('vertical', () => {
362 | const onChange = jest.fn();
363 | const { container } = render(
364 | ,
365 | );
366 |
367 | // Do move
368 | doMouseMove(container, 0, -10);
369 |
370 | expect(onChange).toHaveBeenCalledWith([30, 40]);
371 | });
372 |
373 | it('vertical & reverse', () => {
374 | const onChange = jest.fn();
375 | const { container } = render(
376 | ,
377 | );
378 |
379 | // Do move
380 | doMouseMove(container, 0, -10);
381 |
382 | expect(onChange).toHaveBeenCalledWith([10, 40]);
383 | });
384 | });
385 |
386 | describe('should keep pushable with pushable s defalutValue when not allowCross and setState', () => {
387 | function test(name, func) {
388 | it(name, () => {
389 | const onChange = jest.fn();
390 |
391 | const Demo = () => {
392 | const [value, setValue] = React.useState([20, 40]);
393 |
394 | return (
395 | {
398 | setValue(values);
399 | onChange(values);
400 | }}
401 | value={value}
402 | allowCross={false}
403 | pushable
404 | />
405 | );
406 | };
407 |
408 | global.error = true;
409 | const { container, unmount } = render( );
410 |
411 | // Do move
412 | func(container);
413 |
414 | expect(onChange).toHaveBeenCalledWith([39, 40]);
415 |
416 | unmount();
417 | });
418 | }
419 |
420 | test('mouse', (container) => doMouseMove(container, 0, 9999));
421 | test('touch', (container) => doTouchMove(container, 0, 9999));
422 | });
423 |
424 | describe('track draggable', () => {
425 | function test(name, func) {
426 | it(name, () => {
427 | const onChange = jest.fn();
428 |
429 | const { container, unmount } = render(
430 | ,
431 | );
432 |
433 | // Do move
434 | func(container);
435 |
436 | expect(onChange).toHaveBeenCalledWith([20, 50]);
437 |
438 | unmount();
439 | });
440 | }
441 |
442 | test('mouse', (container) => doMouseMove(container, 0, 20, 'rc-slider-track'));
443 | test('touch', (container) => doTouchMove(container, 0, 20, 'rc-slider-track'));
444 | });
445 |
446 | it('sets aria-orientation to default on the handle', () => {
447 | const { container } = render( );
448 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
449 | 'aria-orientation',
450 | 'horizontal',
451 | );
452 | });
453 |
454 | it('sets aria-orientation to vertical on the handles of vertical Slider', () => {
455 | const { container } = render( );
456 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
457 | 'aria-orientation',
458 | 'vertical',
459 | );
460 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
461 | 'aria-orientation',
462 | 'vertical',
463 | );
464 | });
465 |
466 | it('sets aria-label on the handles', () => {
467 | const { container } = render(
468 | ,
469 | );
470 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
471 | 'aria-label',
472 | 'Some Label',
473 | );
474 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
475 | 'aria-label',
476 | 'Some other Label',
477 | );
478 | });
479 |
480 | it('sets aria-labelledby on the handles', () => {
481 | const { container } = render(
482 | ,
483 | );
484 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
485 | 'aria-labelledby',
486 | 'some_id',
487 | );
488 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
489 | 'aria-labelledby',
490 | 'some_other_id',
491 | );
492 | });
493 |
494 | it('sets aria-valuetext on the handles', () => {
495 | const { container } = render(
496 | `${value} of something`,
503 | (value) => `${value} of something else`,
504 | ]}
505 | />,
506 | );
507 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
508 | 'aria-valuetext',
509 | '1 of something',
510 | );
511 | expect(container.getElementsByClassName('rc-slider-handle')[1]).toHaveAttribute(
512 | 'aria-valuetext',
513 | '3 of something else',
514 | );
515 | });
516 |
517 | // Corresponds to the issue described in https://github.com/react-component/slider/issues/690.
518 | it('should correctly display a dynamically changed number of handles', () => {
519 | const props = {
520 | range: true,
521 | allowCross: false,
522 | marks: {
523 | 0: { label: '0', style: {} },
524 | 25: { label: '25', style: {} },
525 | 50: { label: '50', style: {} },
526 | 75: { label: '75', style: {} },
527 | 100: { label: '100', style: {} },
528 | },
529 | step: null,
530 | };
531 |
532 | const { container, rerender } = render( );
533 |
534 | const verifyHandles = (values) => {
535 | // Has the number of handles that we set.
536 | expect(container.getElementsByClassName('rc-slider-handle')).toHaveLength(values.length);
537 |
538 | // Handles have the values that we set.
539 | Array.from(container.getElementsByClassName('rc-slider-handle')).forEach((ele, index) => {
540 | expect(ele).toHaveAttribute('aria-valuenow', values[index].toString());
541 | });
542 | };
543 |
544 | // Assert that handles are correct initially.
545 | verifyHandles([0, 25, 50, 75, 100]);
546 |
547 | // Assert that handles are correct after decreasing their number.
548 | rerender( );
549 | verifyHandles([0, 75, 100]);
550 |
551 | // Assert that handles are correct after increasing their number.
552 | rerender( );
553 | verifyHandles([0, 25, 75, 100]);
554 | });
555 |
556 | describe('focus & blur', () => {
557 | it('focus()', () => {
558 | const handleFocus = jest.fn();
559 | const { container } = render( );
560 | container.querySelector('.rc-slider-handle').focus();
561 | expect(handleFocus).toBeCalled();
562 | });
563 |
564 | it('blur()', () => {
565 | const handleBlur = jest.fn();
566 | const { container } = render( );
567 | container.querySelector('.rc-slider-handle').focus();
568 | container.querySelector('.rc-slider-handle').blur();
569 | expect(handleBlur).toHaveBeenCalled();
570 | });
571 | });
572 |
573 | it('warning for `draggableTrack` and `mergedStep=null`', () => {
574 | const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
575 |
576 | render( );
577 |
578 | expect(errorSpy).toHaveBeenCalledWith(
579 | 'Warning: `draggableTrack` is not supported when `step` is `null`.',
580 | );
581 | errorSpy.mockRestore();
582 | });
583 |
584 | it('Track should have the correct thickness', () => {
585 | const { container } = render(
586 | ,
587 | );
588 |
589 | const { container: containerVertical } = render(
590 | ,
598 | );
599 | expect(container.querySelector('.rc-slider-track-draggable')).toBeTruthy();
600 | expect(containerVertical.querySelector('.rc-slider-track-draggable')).toBeTruthy();
601 | });
602 |
603 | it('styles', () => {
604 | const { container } = render(
605 | ,
615 | );
616 |
617 | expect(container.querySelector('.rc-slider-tracks')).toHaveStyle({
618 | backgroundColor: '#654321',
619 | });
620 | expect(container.querySelector('.rc-slider-track')).toHaveStyle({
621 | backgroundColor: '#123456',
622 | });
623 | expect(container.querySelector('.rc-slider-handle')).toHaveStyle({
624 | backgroundColor: '#112233',
625 | });
626 | expect(container.querySelector('.rc-slider-rail')).toHaveStyle({
627 | backgroundColor: '#332211',
628 | });
629 | });
630 |
631 | it('classNames', () => {
632 | const { container } = render(
633 | ,
643 | );
644 |
645 | expect(container.querySelector('.rc-slider-tracks')).toHaveClass('my-tracks');
646 | expect(container.querySelector('.rc-slider-track')).toHaveClass('my-track');
647 | expect(container.querySelector('.rc-slider-handle')).toHaveClass('my-handle');
648 | expect(container.querySelector('.rc-slider-rail')).toHaveClass('my-rail');
649 | });
650 |
651 | describe('editable', () => {
652 | it('click to create', () => {
653 | const onChange = jest.fn();
654 | const { container } = render(
655 | ,
662 | );
663 |
664 | doMouseDown(container, 50, 'rc-slider', true);
665 |
666 | expect(onChange).toHaveBeenCalledWith([0, 50, 100]);
667 | });
668 |
669 | it('can not editable with draggableTrack at same time', () => {
670 | const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
671 | render( );
672 |
673 | expect(errorSpy).toHaveBeenCalledWith(
674 | 'Warning: `editable` can not work with `draggableTrack`.',
675 | );
676 | errorSpy.mockRestore();
677 | });
678 |
679 | describe('drag out to remove', () => {
680 | it('uncontrolled', () => {
681 | const onChange = jest.fn();
682 | const onChangeComplete = jest.fn();
683 | const { container } = render(
684 | ,
692 | );
693 |
694 | doMouseMove(container, 0, 1000);
695 | expect(onChange).toHaveBeenCalledWith([50, 100]);
696 |
697 | expect(container.querySelectorAll('.rc-slider-track')).toHaveLength(1);
698 |
699 | // Fire mouse up
700 | fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
701 | expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
702 | });
703 |
704 | it('out and back', () => {
705 | const onChange = jest.fn();
706 | const onChangeComplete = jest.fn();
707 | const { container } = render(
708 | ,
716 | );
717 |
718 | doMouseMove(container, 0, 1000);
719 | expect(onChange).toHaveBeenCalledWith([50]);
720 |
721 | doMouseDrag(0);
722 | expect(onChange).toHaveBeenCalledWith([0, 50]);
723 |
724 | // Fire mouse up
725 | fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
726 | expect(onChangeComplete).toHaveBeenCalledWith([0, 50]);
727 | });
728 |
729 | it('controlled', () => {
730 | const onChange = jest.fn();
731 | const onChangeComplete = jest.fn();
732 |
733 | const Demo = () => {
734 | const [value, setValue] = React.useState([0, 50, 100]);
735 | return (
736 | {
738 | onChange(nextValue);
739 | setValue(nextValue);
740 | }}
741 | onChangeComplete={onChangeComplete}
742 | min={0}
743 | max={100}
744 | value={value}
745 | range={{ editable: true }}
746 | />
747 | );
748 | };
749 |
750 | const { container } = render( );
751 |
752 | doMouseMove(container, 0, 1000);
753 | expect(onChange).toHaveBeenCalledWith([50, 100]);
754 |
755 | // Fire mouse up
756 | fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
757 | expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
758 | });
759 | });
760 |
761 | it('key to delete', () => {
762 | const onChange = jest.fn();
763 |
764 | const { container } = render(
765 | ori}
773 | />,
774 | );
775 |
776 | const handle = container.querySelectorAll('.rc-slider-handle')[1];
777 |
778 | fireEvent.mouseEnter(handle);
779 | fireEvent.keyDown(handle, {
780 | keyCode: keyCode.DELETE,
781 | });
782 |
783 | expect(onChange).toHaveBeenCalledWith([0, 100]);
784 |
785 | // Clear all
786 | fireEvent.keyDown(container.querySelector('.rc-slider-handle'), {
787 | keyCode: keyCode.DELETE,
788 | });
789 | fireEvent.keyDown(container.querySelector('.rc-slider-handle'), {
790 | keyCode: keyCode.DELETE,
791 | });
792 | expect(onChange).toHaveBeenCalledWith([]);
793 |
794 | // 2 handle
795 | expect(container.querySelectorAll('.rc-slider-handle')).toHaveLength(0);
796 | });
797 |
798 | it('not remove when minCount', () => {
799 | const onChange = jest.fn();
800 |
801 | const { container } = render(
802 | ori}
809 | />,
810 | );
811 |
812 | const handle = container.querySelector('.rc-slider-handle');
813 |
814 | // Key
815 | fireEvent.mouseEnter(handle);
816 | fireEvent.keyDown(handle, {
817 | keyCode: keyCode.DELETE,
818 | });
819 | expect(onChange).not.toHaveBeenCalled();
820 |
821 | // Mouse
822 | doMouseMove(container, 0, 1000);
823 | expect(onChange).toHaveBeenCalledWith([100]);
824 | });
825 |
826 | it('maxCount not add', () => {
827 | const onChange = jest.fn();
828 | const { container } = render(
829 | ,
836 | );
837 |
838 | doMouseDown(container, 50, 'rc-slider', true);
839 | expect(onChange).toHaveBeenCalledWith([0, 50]);
840 | });
841 | });
842 | });
843 |
--------------------------------------------------------------------------------
/tests/Slider.test.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { createEvent, fireEvent, render } from '@testing-library/react';
3 | import classNames from 'classnames';
4 | import keyCode from 'rc-util/lib/KeyCode';
5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
6 | import React from 'react';
7 | import Slider from '../src/Slider';
8 |
9 | describe('Slider', () => {
10 | beforeAll(() => {
11 | spyElementPrototypes(HTMLElement, {
12 | getBoundingClientRect: () => ({
13 | top: 0,
14 | bottom: 100,
15 | left: 0,
16 | right: 100,
17 | width: 100,
18 | height: 100,
19 | }),
20 | });
21 | });
22 |
23 | it('should render Slider with correct DOM structure', () => {
24 | const { asFragment } = render( );
25 | expect(asFragment().firstChild).toMatchSnapshot();
26 | });
27 |
28 | it('should render Slider with value correctly', () => {
29 | const { container } = render( );
30 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '50%' });
31 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
32 | left: '0%',
33 | width: '50%',
34 | });
35 | });
36 |
37 | it('should render Slider correctly where value > startPoint', () => {
38 | const { container } = render( );
39 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '50%' });
40 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
41 | left: '20%',
42 | width: '30%',
43 | });
44 | });
45 |
46 | it('should render Slider correctly where value < startPoint', () => {
47 | const { container } = render( );
48 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ left: '40%' });
49 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
50 | left: '40%',
51 | width: '20%',
52 | });
53 | });
54 |
55 | it('should render reverse Slider with value correctly', () => {
56 | const { container } = render( );
57 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '50%' });
58 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
59 | right: '0%',
60 | width: '50%',
61 | });
62 | });
63 |
64 | it('should render reverse Slider correctly where value > startPoint', () => {
65 | const { container } = render( );
66 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '50%' });
67 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
68 | right: '20%',
69 | width: '30%',
70 | });
71 | });
72 |
73 | it('should render reverse Slider correctly where value < startPoint', () => {
74 | const { container } = render( );
75 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveStyle({ right: '30%' });
76 | expect(container.getElementsByClassName('rc-slider-track')[0]).toHaveStyle({
77 | right: '30%',
78 | width: '20%',
79 | });
80 | });
81 |
82 | it('should render reverse Slider with marks correctly', () => {
83 | const marks = { 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10' };
84 | const { container } = render( );
85 | expect(container.getElementsByClassName('rc-slider-mark-text')[0]).toHaveStyle({ right: '0%' });
86 | });
87 |
88 | it('should render Slider without handle if value is null', () => {
89 | const { asFragment } = render( );
90 | expect(asFragment().firstChild).toMatchSnapshot();
91 | });
92 |
93 | it('should allow tabIndex to be set on Handle via Slider', () => {
94 | const { container } = render( );
95 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
96 | 'tabIndex',
97 | '1',
98 | );
99 | });
100 |
101 | it('should allow tabIndex to be set on Handle via Slider and be equal null', () => {
102 | const { container } = render( );
103 | expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
104 | });
105 |
106 | it('increases the value when key "up" is pressed', () => {
107 | const onChange = jest.fn();
108 | const { container } = render( );
109 |
110 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
111 | keyCode: keyCode.UP,
112 | });
113 |
114 | expect(onChange).toHaveBeenCalledWith(51);
115 | });
116 |
117 | it('decreases the value for reverse-vertical when key "up" is pressed', () => {
118 | const onChange = jest.fn();
119 | const { container } = render( );
120 |
121 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
122 | keyCode: keyCode.UP,
123 | });
124 |
125 | expect(onChange).toHaveBeenCalledWith(49);
126 | });
127 |
128 | it('increases the value when key "right" is pressed', () => {
129 | const onChange = jest.fn();
130 | const { container } = render( );
131 |
132 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
133 | keyCode: keyCode.RIGHT,
134 | });
135 |
136 | expect(onChange).toHaveBeenCalledWith(51);
137 | });
138 |
139 | it('it should trigger onAfterChange when key pressed', () => {
140 | const onAfterChange = jest.fn();
141 | const { container } = render( );
142 |
143 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
144 | keyCode: keyCode.RIGHT,
145 | });
146 |
147 | expect(onAfterChange).not.toHaveBeenCalled();
148 |
149 | fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
150 | keyCode: keyCode.RIGHT,
151 | });
152 |
153 | expect(onAfterChange).toHaveBeenCalled();
154 | });
155 |
156 | it('decreases the value for reverse-horizontal when key "right" is pressed', () => {
157 | const onChange = jest.fn();
158 | const { container } = render( );
159 |
160 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
161 | keyCode: keyCode.RIGHT,
162 | });
163 |
164 | expect(onChange).toHaveBeenCalledWith(49);
165 | });
166 |
167 | it('increases the value when key "page up" is pressed, by a factor 2', () => {
168 | const onChange = jest.fn();
169 | const { container } = render( );
170 |
171 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
172 | keyCode: keyCode.PAGE_UP,
173 | });
174 |
175 | expect(onChange).toHaveBeenCalledWith(52);
176 | });
177 |
178 | it('decreases the value when key "down" is pressed', () => {
179 | const onChange = jest.fn();
180 | const { container } = render( );
181 |
182 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
183 | keyCode: keyCode.DOWN,
184 | });
185 |
186 | expect(onChange).toHaveBeenCalledWith(49);
187 | });
188 |
189 | it('decreases the value when key "left" is pressed', () => {
190 | const onChange = jest.fn();
191 | const onChangeComplete = jest.fn();
192 | const { container } = render(
193 | ,
194 | );
195 |
196 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
197 | keyCode: keyCode.LEFT,
198 | });
199 |
200 | expect(onChange).toHaveBeenCalledWith(49);
201 | expect(onChangeComplete).not.toHaveBeenCalled();
202 |
203 | fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
204 | keyCode: keyCode.LEFT,
205 | });
206 |
207 | expect(onChangeComplete).toHaveBeenCalled();
208 | });
209 |
210 | it('it should work fine when arrow key is pressed', () => {
211 | const onChange = jest.fn();
212 | const { container } = render( );
213 |
214 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
215 | keyCode: keyCode.LEFT,
216 | });
217 | expect(onChange).toHaveBeenCalledWith([20, 49]);
218 |
219 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
220 | keyCode: keyCode.RIGHT,
221 | });
222 | expect(onChange).toHaveBeenCalledWith([20, 50]);
223 |
224 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
225 | keyCode: keyCode.UP,
226 | });
227 | expect(onChange).toHaveBeenCalledWith([20, 51]);
228 |
229 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], {
230 | keyCode: keyCode.DOWN,
231 | });
232 | expect(onChange).toHaveBeenCalledWith([20, 50]);
233 | });
234 |
235 | it('decreases the value when key "page down" is pressed, by a factor 2', () => {
236 | const onChange = jest.fn();
237 | const { container } = render( );
238 |
239 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
240 | keyCode: keyCode.PAGE_DOWN,
241 | });
242 |
243 | expect(onChange).toHaveBeenCalledWith(48);
244 | });
245 |
246 | it('sets the value to minimum when key "home" is pressed', () => {
247 | const onChange = jest.fn();
248 | const { container } = render( );
249 |
250 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
251 | keyCode: keyCode.HOME,
252 | });
253 |
254 | expect(onChange).toHaveBeenCalledWith(0);
255 | });
256 |
257 | it('sets the value to maximum when the key "end" is pressed', () => {
258 | const onChange = jest.fn();
259 | const { container } = render( );
260 |
261 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
262 | keyCode: keyCode.END,
263 | });
264 |
265 | expect(onChange).toHaveBeenCalledWith(100);
266 | });
267 |
268 | describe('when component has fixed values', () => {
269 | it('increases the value when key "up" is pressed', () => {
270 | const onChange = jest.fn();
271 | const { container } = render(
272 | ,
279 | );
280 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
281 | keyCode: keyCode.UP,
282 | });
283 | expect(onChange).toHaveBeenCalledWith(100);
284 | });
285 |
286 | it('increases the value when key "right" is pressed', () => {
287 | const onChange = jest.fn();
288 | const { container } = render(
289 | ,
296 | );
297 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
298 | keyCode: keyCode.RIGHT,
299 | });
300 | expect(onChange).toHaveBeenCalledWith(100);
301 | });
302 |
303 | it('decreases the value when key "down" is pressed', () => {
304 | const onChange = jest.fn();
305 | const { container } = render(
306 | ,
313 | );
314 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
315 | keyCode: keyCode.DOWN,
316 | });
317 | expect(onChange).toHaveBeenCalledWith(20);
318 | });
319 |
320 | it('decreases the value when key "left" is pressed', () => {
321 | const onChange = jest.fn();
322 | const { container } = render(
323 | ,
330 | );
331 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
332 | keyCode: keyCode.LEFT,
333 | });
334 | expect(onChange).toHaveBeenCalledWith(20);
335 | });
336 |
337 | it('sets the value to minimum when key "home" is pressed', () => {
338 | const onChange = jest.fn();
339 | const { container } = render(
340 | ,
347 | );
348 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
349 | keyCode: keyCode.HOME,
350 | });
351 | expect(onChange).toHaveBeenCalledWith(20);
352 | });
353 |
354 | it('sets the value to maximum when the key "end" is pressed', () => {
355 | const onChange = jest.fn();
356 | const { container } = render(
357 | ,
364 | );
365 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
366 | keyCode: keyCode.END,
367 | });
368 | expect(onChange).toHaveBeenCalledWith(100);
369 | });
370 | });
371 |
372 | it('keyboard mix with step & marks', () => {
373 | const onChange = jest.fn();
374 |
375 | // [0], 3, 7, 10
376 | const { container } = render(
377 | ,
385 | );
386 | const handler = container.getElementsByClassName('rc-slider-handle')[0];
387 |
388 | // 0, [3], 7, 10
389 | fireEvent.keyDown(handler, { keyCode: keyCode.UP });
390 | expect(onChange).toHaveBeenCalledWith(3);
391 |
392 | // 0, 3, [7], 10
393 | onChange.mockReset();
394 | fireEvent.keyDown(handler, { keyCode: keyCode.UP });
395 | expect(onChange).toHaveBeenCalledWith(7);
396 |
397 | // 0, 3, 7, [10]
398 | onChange.mockReset();
399 | fireEvent.keyDown(handler, { keyCode: keyCode.UP });
400 | expect(onChange).toHaveBeenCalledWith(10);
401 |
402 | // 0, 3, 7, [10]
403 | onChange.mockReset();
404 | fireEvent.keyDown(handler, { keyCode: keyCode.UP });
405 | expect(onChange).not.toHaveBeenCalled();
406 |
407 | // 0, 3, [7], 10
408 | onChange.mockReset();
409 | fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
410 | expect(onChange).toHaveBeenCalledWith(7);
411 |
412 | // 0, [3], 7, 10
413 | onChange.mockReset();
414 | fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
415 | expect(onChange).toHaveBeenCalledWith(3);
416 |
417 | // [0], 3, 7, 10
418 | onChange.mockReset();
419 | fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
420 | expect(onChange).toHaveBeenCalledWith(0);
421 |
422 | // [0], 3, 7, 10
423 | onChange.mockReset();
424 | fireEvent.keyDown(handler, { keyCode: keyCode.DOWN });
425 | expect(onChange).not.toHaveBeenCalled();
426 | });
427 |
428 | it('sets aria-label on the handle', () => {
429 | const { container } = render( );
430 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
431 | 'aria-label',
432 | 'Some Label',
433 | );
434 | });
435 |
436 | it('sets aria-labelledby on the handle', () => {
437 | const { container } = render( );
438 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
439 | 'aria-labelledby',
440 | 'some_id',
441 | );
442 | });
443 |
444 | it('sets aria-required on the handle', () => {
445 | const { container } = render( );
446 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
447 | 'aria-required',
448 | 'true',
449 | );
450 | });
451 |
452 | it('sets aria-valuetext on the handle', () => {
453 | const { container } = render(
454 | `${value} of something`}
459 | />,
460 | );
461 | const handle = container.getElementsByClassName('rc-slider-handle')[0];
462 | expect(handle).toHaveAttribute('aria-valuetext', '3 of something');
463 |
464 | fireEvent.keyDown(handle, { keyCode: keyCode.RIGHT });
465 | expect(handle).toHaveAttribute('aria-valuetext', '4 of something');
466 | });
467 |
468 | describe('focus & blur', () => {
469 | it('focus', () => {
470 | const handleFocus = jest.fn();
471 | const { container, unmount } = render(
472 | ,
473 | );
474 | container.getElementsByClassName('rc-slider-handle')[0].focus();
475 | expect(handleFocus).toBeCalled();
476 |
477 | unmount();
478 | });
479 |
480 | it('blur', () => {
481 | const handleBlur = jest.fn();
482 | const { container, unmount } = render(
483 | ,
484 | );
485 | container.getElementsByClassName('rc-slider-handle')[0].focus();
486 | container.getElementsByClassName('rc-slider-handle')[0].blur();
487 | expect(handleBlur).toBeCalled();
488 |
489 | unmount();
490 | });
491 |
492 | it('ref focus & blur', () => {
493 | const onFocus = jest.fn();
494 | const onBlur = jest.fn();
495 | const ref = React.createRef();
496 | render( );
497 |
498 | ref.current.focus();
499 | expect(onFocus).toBeCalled();
500 |
501 | ref.current.blur();
502 | expect(onBlur).toBeCalled();
503 | });
504 | });
505 |
506 | it('should not be out of range when value is null', () => {
507 | const { container, rerender } = render( );
508 | expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(0);
509 |
510 | rerender( );
511 | expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(1);
512 | });
513 |
514 | describe('click slider to change value', () => {
515 | it('ltr', () => {
516 | const onChange = jest.fn();
517 | const { container } = render( );
518 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
519 | clientX: 20,
520 | });
521 |
522 | expect(onChange).toHaveBeenCalledWith(20);
523 | });
524 |
525 | it('rtl', () => {
526 | const onChange = jest.fn();
527 | const { container } = render( );
528 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
529 | clientX: 20,
530 | });
531 |
532 | expect(onChange).toHaveBeenCalledWith(80);
533 | });
534 |
535 | it('btt', () => {
536 | const onChange = jest.fn();
537 | const { container } = render( );
538 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
539 | clientY: 93,
540 | });
541 |
542 | expect(onChange).toHaveBeenCalledWith(7);
543 | });
544 |
545 | it('ttb', () => {
546 | const onChange = jest.fn();
547 | const { container } = render( );
548 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
549 | clientY: 93,
550 | });
551 |
552 | expect(onChange).toHaveBeenCalledWith(93);
553 | });
554 |
555 | it('null value click to become 2 values', () => {
556 | const onChange = jest.fn();
557 | const { container } = render( );
558 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
559 | clientX: 20,
560 | });
561 |
562 | expect(onChange).toHaveBeenCalledWith([20, 20]);
563 | });
564 |
565 | it('should call onBeforeChange, onChange, and onAfterChange', () => {
566 | const onBeforeChange = jest.fn();
567 | const onChange = jest.fn();
568 | const onAfterChange = jest.fn();
569 | const { container } = render(
570 | ,
575 | );
576 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
577 | clientX: 20,
578 | });
579 |
580 | expect(onBeforeChange).toHaveBeenCalledWith(20);
581 | expect(onChange).toHaveBeenCalledWith(20);
582 | expect(onAfterChange).not.toHaveBeenCalled();
583 | fireEvent.mouseUp(container.querySelector('.rc-slider'), {
584 | clientX: 20,
585 | });
586 | expect(onAfterChange).toHaveBeenCalledWith(20);
587 | });
588 | });
589 |
590 | it('autoFocus', () => {
591 | const onFocus = jest.fn();
592 | render( );
593 |
594 | expect(onFocus).toHaveBeenCalled();
595 | });
596 |
597 | it('custom handle', () => {
598 | const { container } = render(
599 |
601 | React.cloneElement(node, {
602 | className: classNames(node.props.className, 'custom-handle'),
603 | })
604 | }
605 | />,
606 | );
607 |
608 | expect(container.querySelector('.custom-handle')).toBeTruthy();
609 | });
610 |
611 | // https://github.com/ant-design/ant-design/issues/34020
612 | it('max value not align with step', () => {
613 | const onChange = jest.fn();
614 | const { container } = render(
615 | ,
616 | );
617 | fireEvent.keyDown(container.querySelector('.rc-slider-handle'), { keyCode: keyCode.RIGHT });
618 |
619 | expect(onChange).toHaveBeenCalledWith(2);
620 | expect(container.querySelector('.rc-slider-handle').style.left).toBe('100%');
621 | });
622 |
623 | it('not show decimal', () => {
624 | const onChange = jest.fn();
625 | const { container } = render(
626 | ,
627 | );
628 | fireEvent.keyDown(container.querySelector('.rc-slider-handle'), { keyCode: keyCode.RIGHT });
629 | expect(onChange).toHaveBeenCalledWith(0.82);
630 | });
631 |
632 | it('onAfterChange should return number', () => {
633 | const onAfterChange = jest.fn();
634 | const { container } = render( );
635 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
636 | clientX: 20,
637 | });
638 | expect(onAfterChange).not.toHaveBeenCalled();
639 | fireEvent.mouseUp(container.querySelector('.rc-slider'), {
640 | clientX: 20,
641 | });
642 | expect(onAfterChange).toHaveBeenCalledWith(20);
643 | });
644 |
645 | // https://github.com/react-component/slider/pull/948
646 | it('could drag handler after click tracker', () => {
647 | const onChange = jest.fn();
648 | const { container } = render( );
649 | fireEvent.mouseDown(container.querySelector('.rc-slider'), {
650 | clientX: 20,
651 | });
652 | expect(onChange).toHaveBeenLastCalledWith(20);
653 |
654 | // Drag
655 | const mouseMove = createEvent.mouseMove(document);
656 | mouseMove.pageX = 100;
657 | fireEvent(document, mouseMove);
658 | expect(onChange).toHaveBeenLastCalledWith(100);
659 | });
660 |
661 | it('should render Slider with included=false', () => {
662 | const { asFragment } = render( );
663 | expect(asFragment().firstChild).toMatchSnapshot();
664 | });
665 |
666 | it('tipFormatter should not crash with undefined value', () => {
667 | [undefined, null].forEach((value) => {
668 | render( );
669 | });
670 | });
671 | });
672 |
--------------------------------------------------------------------------------
/tests/Tooltip.test.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { fireEvent, render } from '@testing-library/react';
3 | import React from 'react';
4 | import Slider from '../src/Slider';
5 |
6 | describe('Slider.Tooltip', () => {
7 | it('internal activeHandleRender support', () => {
8 | const { container } = render(
9 |
13 | React.cloneElement(node, {
14 | 'data-test': 'bamboo',
15 | 'data-value': info.value,
16 | })
17 | }
18 | />,
19 | );
20 |
21 | // Click second
22 | fireEvent.mouseEnter(container.querySelectorAll('.rc-slider-handle')[1]);
23 | expect(container.querySelector('.rc-slider-handle[data-test]')).toBeTruthy();
24 | expect(
25 | container.querySelector('.rc-slider-handle[data-value]').getAttribute('data-value'),
26 | ).toBe('50');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/tests/__mocks__/rc-trigger.js:
--------------------------------------------------------------------------------
1 | import Trigger from 'rc-trigger/lib/mock';
2 |
3 | export default Trigger;
4 |
--------------------------------------------------------------------------------
/tests/__snapshots__/Range.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Range should render Multi-Range with correct DOM structure 1`] = `
4 | );
38 | expect(container1.getElementsByClassName('rc-slider-vertical')).toHaveLength(1);
39 |
40 | const { container: container2 } = render( );
41 | expect(container2.getElementsByClassName('rc-slider-vertical')).toHaveLength(1);
42 | });
43 |
44 | it('should render dots correctly when `dots=true`', () => {
45 | const { container: container1 } = render( );
46 | expect(container1.getElementsByClassName('rc-slider-dot')).toHaveLength(11);
47 | expect(container1.getElementsByClassName('rc-slider-dot-active')).toHaveLength(6);
48 |
49 | const { container: container2 } = render( );
50 | expect(container2.getElementsByClassName('rc-slider-dot')).toHaveLength(11);
51 | expect(container2.getElementsByClassName('rc-slider-dot-active')).toHaveLength(4);
52 | });
53 |
54 | it('should render normally when `dots=true` and `step=null`', () => {
55 | const { container } = render( );
56 | expect(() => container).not.toThrowError();
57 | });
58 |
59 | it('should render dots correctly when dotStyle is dynamic`', () => {
60 | const { container: container1 } = render(
61 | ({ width: `${dotValue}px` })} />,
62 | );
63 | expect(container1.getElementsByClassName('rc-slider-dot')[1]).toHaveStyle(
64 | 'left: 10%; transform: translateX(-50%); width: 10px',
65 | );
66 | expect(container1.getElementsByClassName('rc-slider-dot')[2]).toHaveStyle(
67 | 'left: 20%; transform: translateX(-50%); width: 20px',
68 | );
69 |
70 | const { container: container2 } = render(
71 | ({ width: `${dotValue}px` })}
77 | />,
78 | );
79 | expect(container2.getElementsByClassName('rc-slider-dot-active')[1]).toHaveStyle(
80 | 'left: 30%; transform: translateX(-50%); width: 30px',
81 | );
82 | expect(container2.getElementsByClassName('rc-slider-dot-active')[2]).toHaveStyle(
83 | 'left: 40%; transform: translateX(-50%); width: 40px',
84 | );
85 | });
86 |
87 | it('should not set value greater than `max` or smaller `min`', () => {
88 | const { container: container1 } = render( );
89 | expect(
90 | container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
91 | ).toBe('10');
92 |
93 | const { container: container2 } = render( );
94 | expect(
95 | container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
96 | ).toBe('90');
97 |
98 | const { container: container3 } = render( );
99 | expect(
100 | container3.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
101 | ).toBe('10');
102 | expect(
103 | container3.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
104 | ).toBe('90');
105 | });
106 |
107 | it('should not set values when sending invalid numbers', () => {
108 | const { container: container1 } = render( );
109 | expect(
110 | container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
111 | ).toBe('0');
112 |
113 | const { container: container2 } = render( );
114 | expect(
115 | container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
116 | ).toBe('100');
117 |
118 | const { container: container3 } = render(
119 | ,
120 | );
121 | expect(
122 | container3.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
123 | ).toBe('0');
124 | expect(
125 | container3.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
126 | ).toBe('100');
127 | });
128 |
129 | it('should update value when it is out of range', () => {
130 | const sliderOnChange = jest.fn();
131 | const { container: container1, rerender: rerender1 } = render(
132 | ,
133 | );
134 | rerender1( );
135 | expect(
136 | container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
137 | ).toBe('10');
138 |
139 | const rangeOnChange = jest.fn();
140 | const { container: container2, rerender: rerender2 } = render(
141 | ,
142 | );
143 | rerender2( );
144 | expect(
145 | container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
146 | ).toBe('10');
147 | });
148 |
149 | it('should not trigger onChange when no min and max', () => {
150 | const sliderOnChange = jest.fn();
151 | const { container: container1, rerender: rerender1 } = render(
152 | ,
153 | );
154 | rerender1( );
155 | expect(
156 | container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
157 | ).toBe('100');
158 | expect(sliderOnChange).not.toHaveBeenCalled();
159 |
160 | const rangeOnChange = jest.fn();
161 | const { container: container2, rerender: rerender2 } = render(
162 | ,
163 | );
164 | rerender2( );
165 | expect(
166 | container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
167 | ).toBe('0');
168 | expect(
169 | container2.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
170 | ).toBe('100');
171 | expect(rangeOnChange).not.toHaveBeenCalled();
172 | });
173 |
174 | it('should not trigger onChange when value is out of range', () => {
175 | const sliderOnChange = jest.fn();
176 | const { container: container1, rerender: rerender1 } = render(
177 | ,
178 | );
179 | rerender1( );
180 | expect(
181 | container1.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
182 | ).toBe('10');
183 | expect(sliderOnChange).not.toHaveBeenCalled();
184 |
185 | const rangeOnChange = jest.fn();
186 | const { container: container2, rerender: rerender2 } = render(
187 | ,
188 | );
189 | rerender2( );
190 | expect(
191 | container2.getElementsByClassName('rc-slider-handle')[0].getAttribute('aria-valuenow'),
192 | ).toBe('0');
193 | expect(
194 | container2.getElementsByClassName('rc-slider-handle')[1].getAttribute('aria-valuenow'),
195 | ).toBe('10');
196 | expect(rangeOnChange).not.toHaveBeenCalled();
197 | });
198 |
199 | it('should not call onChange when value is the same', () => {
200 | const handler = jest.fn();
201 |
202 | const { container: container1 } = render( );
203 | const handle1 = container1.getElementsByClassName('rc-slider-handle')[0];
204 | fireEvent.mouseDown(handle1);
205 | fireEvent.mouseMove(handle1);
206 | fireEvent.mouseUp(handle1);
207 |
208 | const { container: container2 } = render( );
209 | const handle2 = container2.getElementsByClassName('rc-slider-handle')[1];
210 | fireEvent.mouseDown(handle2);
211 | fireEvent.mouseMove(handle2);
212 | fireEvent.mouseUp(handle2);
213 |
214 | expect(handler).not.toHaveBeenCalled();
215 | });
216 |
217 | // TODO: should update the following test cases for it should test API instead implementation
218 | // it('should set `dragOffset` to correct value when the left handle is clicked off-center', () => {
219 | // const { container } = render( );
220 | // setWidth(wrapper.instance().sliderRef, 100);
221 | // const leftHandle = wrapper
222 | // .find('.rc-slider-handle')
223 | // .at(1)
224 | // .instance();
225 | // wrapper.simulate('mousedown', {
226 | // type: 'mousedown',
227 | // target: leftHandle,
228 | // pageX: 5,
229 | // button: 0,
230 | // stopPropagation() {},
231 | // preventDefault() {},
232 | // });
233 | // expect(wrapper.instance().dragOffset).toBe(5);
234 | // });
235 |
236 | // it('should respect `dragOffset` while dragging the handle via MouseEvents', () => {
237 | // const { container } = render( );
238 | // setWidth(wrapper.instance().sliderRef, 100);
239 | // const leftHandle = wrapper
240 | // .find('.rc-slider-handle')
241 | // .at(1)
242 | // .instance();
243 | // wrapper.simulate('mousedown', {
244 | // type: 'mousedown',
245 | // target: leftHandle,
246 | // pageX: 5,
247 | // button: 0,
248 | // stopPropagation() {},
249 | // preventDefault() {},
250 | // });
251 | // expect(wrapper.instance().dragOffset).toBe(5);
252 | // wrapper.instance().onMouseMove({
253 | // // to propagation
254 | // type: 'mousemove',
255 | // target: leftHandle,
256 | // pageX: 14,
257 | // button: 0,
258 | // stopPropagation() {},
259 | // preventDefault() {},
260 | // });
261 | // expect(wrapper.instance().getValue()).toBe(9);
262 | // });
263 |
264 | it('should not go to right direction when mouse go to the left', () => {
265 | const { container } = render( );
266 | const leftHandle = container.getElementsByClassName('rc-slider-handle')[0];
267 |
268 | const mouseDown = createEvent.mouseDown(leftHandle);
269 | mouseDown.pageX = 5;
270 |
271 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
272 | 'aria-valuenow',
273 | '0',
274 | );
275 |
276 | const mouseMove = createEvent.mouseMove(leftHandle);
277 | mouseMove.pageX = 0;
278 |
279 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
280 | 'aria-valuenow',
281 | '0',
282 | );
283 | });
284 |
285 | it('should call onAfterChange when clicked on mark label', () => {
286 | const labelId = 'to-be-clicked';
287 | const marks = {
288 | 0: 'some other label',
289 | 100: some label,
290 | };
291 |
292 | const sliderOnChange = jest.fn();
293 | const sliderOnAfterChange = jest.fn();
294 | const { container } = render(
295 | ,
301 | );
302 | const sliderHandleWrapper = container.querySelector(`#${labelId}`);
303 | fireEvent.mouseDown(sliderHandleWrapper);
304 | // Simulate propagation
305 | fireEvent.mouseDown(container.querySelector('.rc-slider'));
306 | fireEvent.mouseUp(container.querySelector('.rc-slider'));
307 |
308 | fireEvent.click(sliderHandleWrapper);
309 | expect(sliderOnChange).toHaveBeenCalled();
310 | expect(sliderOnAfterChange).toHaveBeenCalled();
311 |
312 | const rangeOnAfterChange = jest.fn();
313 | const { container: container2 } = render(
314 | ,
315 | );
316 | const rangeHandleWrapper = container2.querySelector(`#${labelId}`);
317 | fireEvent.click(rangeHandleWrapper);
318 | // Simulate propagation
319 | fireEvent.mouseDown(container2.querySelector('.rc-slider'));
320 | fireEvent.mouseUp(container2.querySelector('.rc-slider'));
321 | expect(rangeOnAfterChange).toHaveBeenCalled();
322 | });
323 |
324 | it('only call onAfterChange once', () => {
325 | const sliderOnChange = jest.fn();
326 | const sliderOnAfterChange = jest.fn();
327 | const { container } = render(
328 | ,
329 | );
330 |
331 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
332 | keyCode: KeyCode.UP,
333 | });
334 |
335 | expect(sliderOnChange).toHaveBeenCalled();
336 | expect(sliderOnAfterChange).not.toHaveBeenCalled();
337 |
338 | fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
339 | keyCode: KeyCode.UP,
340 | });
341 | expect(sliderOnAfterChange).toHaveBeenCalled();
342 | expect(sliderOnAfterChange).toHaveBeenCalledTimes(1);
343 | });
344 |
345 | it('deprecate onAfterChange', () => {
346 | const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
347 | const onChangeComplete = jest.fn();
348 | const onAfterChange = jest.fn();
349 | const { container } = render(
350 | ,
351 | );
352 |
353 | fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[0], {
354 | keyCode: KeyCode.UP,
355 | });
356 |
357 | expect(onChangeComplete).not.toHaveBeenCalled();
358 | expect(onAfterChange).not.toHaveBeenCalled();
359 |
360 | fireEvent.keyUp(container.getElementsByClassName('rc-slider-handle')[0], {
361 | keyCode: KeyCode.UP,
362 | });
363 | expect(onChangeComplete).toHaveBeenCalledTimes(1);
364 | expect(onAfterChange).toHaveBeenCalledTimes(1);
365 | expect(errSpy).toHaveBeenCalledWith(
366 | 'Warning: [rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
367 | );
368 | errSpy.mockRestore();
369 | });
370 |
371 | // Move to antd instead
372 | // it('the tooltip should be attach to the container with the id tooltip', () => {
373 | // const SliderWithTooltip = createSliderWithTooltip(Slider);
374 | // const tooltipPrefixer = {
375 | // prefixCls: 'slider-tooltip',
376 | // };
377 | // const tooltipParent = document.createElement('div');
378 | // tooltipParent.setAttribute('id', 'tooltip');
379 | // const { container } = render(
380 | // document.getElementById('tooltip')}
383 | // />,
384 | // );
385 | // expect(wrapper.instance().props.getTooltipContainer).toBeTruthy();
386 | // });
387 | });
388 |
--------------------------------------------------------------------------------
/tests/marks.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-undef */
2 | import '@testing-library/jest-dom';
3 | import { fireEvent, render } from '@testing-library/react';
4 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
5 | import React from 'react';
6 | import Slider from '../src';
7 |
8 | describe('marks', () => {
9 | beforeAll(() => {
10 | spyElementPrototypes(HTMLElement, {
11 | getBoundingClientRect: () => ({
12 | width: 100,
13 | height: 100,
14 | }),
15 | });
16 | });
17 |
18 | it('should render marks correctly when `marks` is not an empty object', () => {
19 | const marks = { 0: 0, 30: '30', 99: '', 100: '100' };
20 |
21 | const { container } = render( );
22 | expect(container.getElementsByClassName('rc-slider-mark-text')).toHaveLength(3);
23 | expect(container.getElementsByClassName('rc-slider-mark-text')[0].innerHTML).toBe('0');
24 | expect(container.getElementsByClassName('rc-slider-mark-text')[1].innerHTML).toBe('30');
25 | expect(container.getElementsByClassName('rc-slider-mark-text')[2].innerHTML).toBe('100');
26 |
27 | const { container: container2 } = render( );
28 | expect(container2.getElementsByClassName('rc-slider-mark-text')).toHaveLength(3);
29 | expect(container2.getElementsByClassName('rc-slider-mark-text')[0].innerHTML).toBe('0');
30 | expect(container2.getElementsByClassName('rc-slider-mark-text')[1].innerHTML).toBe('30');
31 | expect(container2.getElementsByClassName('rc-slider-mark-text')[2].innerHTML).toBe('100');
32 |
33 | expect(container.querySelector('.rc-slider-with-marks')).toBeTruthy();
34 | });
35 |
36 | it('should select correct value while click on marks', () => {
37 | const marks = { 0: '0', 30: '30', 100: '100' };
38 | const onChange = jest.fn();
39 | const onChangeComplete = jest.fn();
40 | const { container } = render( );
41 | fireEvent.click(container.getElementsByClassName('rc-slider-mark-text')[1]);
42 | expect(container.getElementsByClassName('rc-slider-handle')[0]).toHaveAttribute(
43 | 'aria-valuenow',
44 | '30',
45 | );
46 | expect(onChange).toHaveBeenCalledTimes(1);
47 | expect(onChange).toHaveBeenCalledWith(30);
48 | expect(onChangeComplete).toHaveBeenCalledTimes(1);
49 | expect(onChangeComplete).toHaveBeenCalledWith(30);
50 | });
51 |
52 | // TODO: not implement yet
53 | // zombieJ: since this test leave years but not implement. Could we remove this?
54 | // xit('should select correct value while click on marks in Ranger', () => {
55 | // const rangeWrapper = render( );
56 | // const rangeMark = rangeWrapper.find('.rc-slider-mark-text').at(1);
57 | // rangeMark.simulate('mousedown', {
58 | // type: 'mousedown',
59 | // target: rangeMark,
60 | // pageX: 25,
61 | // button: 0,
62 | // stopPropagation() {},
63 | // preventDefault() {},
64 | // });
65 | // expect(rangeWrapper.state('bounds')).toBe([0, 30]);
66 | // });
67 | });
68 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/slider/874875a809e6d7423449ba8a8277e3f4cb2277cb/tests/setup.js
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "moduleResolution": "node",
5 | "baseUrl": "./",
6 | "jsx": "react",
7 | "declaration": true,
8 | "skipLibCheck": true,
9 | "esModuleInterop": true,
10 | "paths": {
11 | "@/*": ["src/*"],
12 | "@@/*": ["src/.umi/*"],
13 | "rc-slider": ["src/index.tsx"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 |
--------------------------------------------------------------------------------
70 | `;
71 |
72 | exports[`Range should render Range with correct DOM structure 1`] = `
73 |
109 | `;
110 |
--------------------------------------------------------------------------------
/tests/__snapshots__/Slider.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Slider should render Slider with correct DOM structure 1`] = `
4 |
29 | `;
30 |
31 | exports[`Slider should render Slider with included=false 1`] = `
32 |
53 | `;
54 |
55 | exports[`Slider should render Slider without handle if value is null 1`] = `
56 |
66 | `;
67 |
--------------------------------------------------------------------------------
/tests/common.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len, no-undef */
2 | import '@testing-library/jest-dom';
3 | import { createEvent, fireEvent, render } from '@testing-library/react';
4 | import KeyCode from 'rc-util/lib/KeyCode';
5 | import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
6 | import React from 'react';
7 | import Slider from '../src';
8 |
9 | // const setWidth = (object, width) => {
10 | // // https://github.com/tmpvar/jsdom/commit/0cdb2efcc69b6672dc2928644fc0172df5521176
11 | // Object.defineProperty(object, 'getBoundingClientRect', {
12 | // value: () => ({
13 | // width,
14 | // // Let all other values retain the JSDom default of `0`.
15 | // bottom: 0,
16 | // height: 0,
17 | // left: 0,
18 | // right: 0,
19 | // top: 0,
20 | // }),
21 | // enumerable: true,
22 | // configurable: true,
23 | // });
24 | // };
25 |
26 | describe('Common', () => {
27 | beforeAll(() => {
28 | spyElementPrototypes(HTMLElement, {
29 | getBoundingClientRect: () => ({
30 | width: 100,
31 | height: 100,
32 | }),
33 | });
34 | });
35 |
36 | it('should render vertical Slider/Range, when `vertical` is true', () => {
37 | const { container: container1 } = render(