Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions assets/index.less
Original file line number Diff line number Diff line change
@@ -73,6 +73,10 @@
&-dragging&-dragging&-dragging {
border-color: tint(@primary-color, 20%);
box-shadow: 0 0 0 5px tint(@primary-color, 50%);

&-delete {
opacity: 0;
}
}

&:focus {
@@ -186,11 +190,11 @@
left: 5px;
width: 4px;
}

&-track-draggable {
border-top:0;
border-bottom: 0;
border-top: 0;
border-right: 5px solid rgba(0, 0, 0, 0);
border-bottom: 0;
border-left: 5px solid rgba(0, 0, 0, 0);
transform: translateX(-5px);
}
8 changes: 8 additions & 0 deletions docs/demo/editable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Multiple
nav:
title: Demo
path: /demo
---

<code src="../examples/editable.tsx"></code>
43 changes: 43 additions & 0 deletions docs/examples/editable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint react/no-multi-comp: 0, no-console: 0 */
import Slider from 'rc-slider';
import React from 'react';
import '../../assets/index.less';

const style: React.CSSProperties = {
width: 400,
margin: 50,
};

export default () => {
const [value, setValue] = React.useState([0, 50, 80]);

Check notice

Code scanning / CodeQL

Unused variable, import, function or class

Unused variable value.

return (
<div>
<div style={style}>
<Slider
// range
range={{
editable: true,
}}
track={false}
min={0}
max={100}
value={value}
// defaultValue={null}
onChange={(nextValue) => {
console.error('Change:', nextValue);
setValue(nextValue as any);
}}
onChangeComplete={(nextValue) => {
console.log('Complete', nextValue);
}}
styles={{
rail: {
background: `linear-gradient(to right, blue, red)`,
},
}}
/>
</div>
</div>
);
};
4 changes: 2 additions & 2 deletions docs/examples/multiple.tsx
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ const NodeWrapper = ({ children }: { children: React.ReactElement }) => {
};

export default () => {
const [value, setValue] = React.useState([0, 5, 8]);
const [value, setValue] = React.useState([0, 50, 80]);

return (
<div>
@@ -27,7 +27,7 @@ export default () => {
// defaultValue={[0, 10, 30]}
// onChange={log}
min={0}
max={10}
max={100}
value={value}
onChange={(nextValue) => {
// console.log('>>>', nextValue);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@
"@testing-library/react": "^12.1.3",
"@types/classnames": "^2.2.9",
"@types/jest": "^29.5.1",
"@types/node": "^20.14.10",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.0.11",
"@umijs/fabric": "^4.0.1",
12 changes: 12 additions & 0 deletions src/Handles/Handle.tsx
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ interface RenderProps {
prefixCls: string;
value: number;
dragging: boolean;
draggingDelete: boolean;
}

export interface HandleProps
@@ -19,7 +20,9 @@ export interface HandleProps
value: number;
valueIndex: number;
dragging: boolean;
draggingDelete: boolean;
onStartMove: OnStartMove;
onDelete: (index: number) => void;
onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
onFocus: (e: React.FocusEvent<HTMLDivElement>, index: number) => void;
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>, index: number) => void;
@@ -37,9 +40,11 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
value,
valueIndex,
onStartMove,
onDelete,
style,
render,
dragging,
draggingDelete,
onOffsetChange,
onChangeComplete,
onFocus,
@@ -118,6 +123,11 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
case KeyCode.PAGE_DOWN:
offset = -2;
break;

case KeyCode.BACKSPACE:
case KeyCode.DELETE:
onDelete(valueIndex);
break;
}

if (offset !== null) {
@@ -177,6 +187,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
{
[`${handlePrefixCls}-${valueIndex + 1}`]: valueIndex !== null && range,
[`${handlePrefixCls}-dragging`]: dragging,
[`${handlePrefixCls}-dragging-delete`]: draggingDelete,
},
classNames.handle,
)}
@@ -197,6 +208,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
prefixCls,
value,
dragging,
draggingDelete,
});
}

44 changes: 27 additions & 17 deletions src/Handles/index.tsx
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ export interface HandlesProps {
onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void;
onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void;
onDelete: (index: number) => void;
handleRender?: HandleProps['render'];
/**
* When config `activeHandleRender`,
@@ -20,6 +21,7 @@ export interface HandlesProps {
*/
activeHandleRender?: HandleProps['render'];
draggingIndex: number;
draggingDelete: boolean;
onChangeComplete?: () => void;
}

@@ -37,6 +39,7 @@ const Handles = React.forwardRef<HandlesRef, HandlesProps>((props, ref) => {
handleRender,
activeHandleRender,
draggingIndex,
draggingDelete,
onFocus,
...restProps
} = props;
@@ -74,31 +77,38 @@ const Handles = React.forwardRef<HandlesRef, HandlesProps>((props, ref) => {

return (
<>
{values.map<React.ReactNode>((value, index) => (
<Handle
ref={(node) => {
if (!node) {
delete handlesRef.current[index];
} else {
handlesRef.current[index] = node;
}
}}
dragging={draggingIndex === index}
style={getIndex(style, index)}
key={index}
value={value}
valueIndex={index}
{...handleProps}
/>
))}
{values.map<React.ReactNode>((value, index) => {
const dragging = draggingIndex === index;

return (
<Handle
ref={(node) => {
if (!node) {
delete handlesRef.current[index];
} else {
handlesRef.current[index] = node;
}
}}
dragging={dragging}
draggingDelete={dragging && draggingDelete}
style={getIndex(style, index)}
key={index}
value={value}
valueIndex={index}
{...handleProps}
/>
);
})}

{/* Used for render tooltip, this is not a real handle */}
{activeHandleRender && (
<Handle
key="a11y"
{...handleProps}
value={values[activeIndex]}
valueIndex={null}
dragging={draggingIndex !== -1}
draggingDelete={draggingDelete}
render={activeHandleRender}
style={{ pointerEvents: 'none' }}
tabIndex={null}
120 changes: 80 additions & 40 deletions src/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cls from 'classnames';
import { useEvent } from 'rc-util';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import isEqual from 'rc-util/lib/isEqual';
import warning from 'rc-util/lib/warning';
@@ -13,6 +14,7 @@ import type { SliderContextProps } from './context';
import SliderContext from './context';
import useDrag from './hooks/useDrag';
import useOffset from './hooks/useOffset';
import useRange from './hooks/useRange';
import type {
AriaValueFormat,
Direction,
@@ -35,6 +37,11 @@ import type {
* - keyboard support pushable
*/

export type RangeConfig = {
editable?: boolean;
draggableTrack?: boolean;
};

export interface SliderProps<ValueType = number | number[]> {
prefixCls?: string;
className?: string;
@@ -51,7 +58,7 @@ export interface SliderProps<ValueType = number | number[]> {
onBlur?: (e: React.FocusEvent<HTMLDivElement>) => void;

// Value
range?: boolean;
range?: boolean | RangeConfig;
count?: number;
min?: number;
max?: number;
@@ -68,8 +75,6 @@ export interface SliderProps<ValueType = number | number[]> {
// Cross
allowCross?: boolean;
pushable?: boolean | number;
/** range only */
draggableTrack?: boolean;

// Direction
reverse?: boolean;
@@ -94,6 +99,7 @@ export interface SliderProps<ValueType = number | number[]> {
// Components
handleRender?: HandlesProps['handleRender'];
activeHandleRender?: HandlesProps['handleRender'];
track?: boolean;

// Accessibility
tabIndex?: number | number[];
@@ -138,7 +144,6 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
// Cross
allowCross = true,
pushable = false,
draggableTrack,

// Direction
reverse,
@@ -160,6 +165,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
// Components
handleRender,
activeHandleRender,
track,

// Accessibility
tabIndex = 0,
@@ -179,6 +185,8 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
}, [reverse, vertical]);

// ============================ Range =============================
const [rangeEnabled, rangeEditable, rangeDraggableTrack] = useRange(range);

const mergedMin = React.useMemo(() => (isFinite(min) ? min : 0), [min]);
const mergedMax = React.useMemo(() => (isFinite(max) ? max : 100), [max]);

@@ -247,7 +255,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
let returnValues = mergedValue === null ? [] : [val0];

// Format as range
if (range) {
if (rangeEnabled) {
returnValues = [...valueList];

// When count provided or value is `undefined`, we fill values
@@ -269,38 +277,49 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
});

return returnValues;
}, [mergedValue, range, mergedMin, count, formatValue]);
}, [mergedValue, rangeEnabled, mergedMin, count, formatValue]);

// =========================== onChange ===========================
const rawValuesRef = React.useRef(rawValues);
rawValuesRef.current = rawValues;
const getTriggerValue = (triggerValues: number[]) =>
rangeEnabled ? triggerValues : triggerValues[0];

const getTriggerValue = (triggerValues: number[]) => (range ? triggerValues : triggerValues[0]);

const triggerChange = (nextValues: number[]) => {
const triggerChange = useEvent((nextValues: number[]) => {
// Order first
const cloneNextValues = [...nextValues].sort((a, b) => a - b);

// Trigger event if needed
if (onChange && !isEqual(cloneNextValues, rawValuesRef.current, true)) {
if (onChange && !isEqual(cloneNextValues, rawValues, true)) {
onChange(getTriggerValue(cloneNextValues));
}

// We set this later since it will re-render component immediately
setValue(cloneNextValues);
};
});

const finishChange = () => {
const finishValue = getTriggerValue(rawValuesRef.current);
const finishChange = useEvent(() => {
const finishValue = getTriggerValue(rawValues);
onAfterChange?.(finishValue);
warning(
!onAfterChange,
'[rc-slider] `onAfterChange` is deprecated. Please use `onChangeComplete` instead.',
);
onChangeComplete?.(finishValue);
});

const onDelete = (index: number) => {
if (!disabled && rangeEditable) {
const cloneNextValues = [...rawValues];
cloneNextValues.splice(index, 1);

onBeforeChange?.(getTriggerValue(cloneNextValues));
triggerChange(cloneNextValues);

const nextFocusIndex = Math.max(0, index - 1);
handlesRef.current.focus(nextFocusIndex);
}
};

const [draggingIndex, draggingValue, cacheValues, onStartDrag] = useDrag(
const [draggingIndex, draggingValue, draggingDelete, cacheValues, onStartDrag] = useDrag(
containerRef,
direction,
rawValues,
@@ -310,11 +329,20 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
triggerChange,
finishChange,
offsetValues,
rangeEditable,
);

/**
* When `rangeEditable` will insert a new value in the values array.
* Else it will replace the value in the values array.
*/
const changeToCloseValue = (newValue: number, e?: React.MouseEvent) => {
if (!disabled) {
// Create new values
const cloneNextValues = [...rawValues];

let valueIndex = 0;
let valueBeforeIndex = 0; // Record the index which value < newValue
let valueDist = mergedMax - mergedMin;

rawValues.forEach((val, index) => {
@@ -323,24 +351,32 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
valueDist = dist;
valueIndex = index;
}

if (val < newValue) {
valueBeforeIndex = index;
}
});

// Create new values
const cloneNextValues = [...rawValues];
let focusIndex = valueIndex;

cloneNextValues[valueIndex] = newValue;
if (rangeEditable && valueDist !== 0) {
cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue);
focusIndex = valueBeforeIndex + 1;
} else {
cloneNextValues[valueIndex] = newValue;
}

// Fill value to match default 2
if (range && !rawValues.length && count === undefined) {
// Fill value to match default 2 (only when `rawValues` is empty)
if (rangeEnabled && !rawValues.length && count === undefined) {
cloneNextValues.push(newValue);
}

onBeforeChange?.(getTriggerValue(cloneNextValues));
triggerChange(cloneNextValues);
if (e) {
(document.activeElement as HTMLElement)?.blur?.();
handlesRef.current.focus(valueIndex);
onStartDrag(e, valueIndex, cloneNextValues);
handlesRef.current.focus(focusIndex);
onStartDrag(e, focusIndex, cloneNextValues);
}
}
};
@@ -402,20 +438,20 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop

// ============================= Drag =============================
const mergedDraggableTrack = React.useMemo(() => {
if (draggableTrack && mergedStep === null) {
if (rangeDraggableTrack && mergedStep === null) {
if (process.env.NODE_ENV !== 'production') {
warning(false, '`draggableTrack` is not supported when `step` is `null`.');
}
return false;
}
return draggableTrack;
}, [draggableTrack, mergedStep]);
return rangeDraggableTrack;
}, [rangeDraggableTrack, mergedStep]);

const onStartMove: OnStartMove = (e, valueIndex) => {
const onStartMove: OnStartMove = useEvent((e, valueIndex) => {
onStartDrag(e, valueIndex);

onBeforeChange?.(getTriggerValue(rawValuesRef.current));
};
onBeforeChange?.(getTriggerValue(rawValues));
});

// Auto focus for updated handle
const dragging = draggingIndex !== -1;
@@ -435,12 +471,12 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
// Provide a range values with included [min, max]
// Used for Track, Mark & Dot
const [includedStart, includedEnd] = React.useMemo(() => {
if (!range) {
if (!rangeEnabled) {
return [mergedMin, sortedCacheValues[0]];
}

return [sortedCacheValues[0], sortedCacheValues[sortedCacheValues.length - 1]];
}, [sortedCacheValues, range, mergedMin]);
}, [sortedCacheValues, rangeEnabled, mergedMin]);

// ============================= Refs =============================
React.useImperativeHandle(ref, () => ({
@@ -474,7 +510,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
included,
includedStart,
includedEnd,
range,
range: rangeEnabled,
tabIndex,
ariaLabelForHandle,
ariaLabelledByForHandle,
@@ -492,7 +528,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
included,
includedStart,
includedEnd,
range,
rangeEnabled,
tabIndex,
ariaLabelForHandle,
ariaLabelledByForHandle,
@@ -521,13 +557,15 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
style={{ ...railStyle, ...styles?.rail }}
/>

<Tracks
prefixCls={prefixCls}
style={trackStyle}
values={sortedCacheValues}
startPoint={startPoint}
onStartMove={mergedDraggableTrack ? onStartMove : undefined}
/>
{track !== false && (
<Tracks
prefixCls={prefixCls}
style={trackStyle}
values={sortedCacheValues}
startPoint={startPoint}
onStartMove={mergedDraggableTrack ? onStartMove : undefined}
/>
)}

<Steps
prefixCls={prefixCls}
@@ -543,13 +581,15 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
style={handleStyle}
values={cacheValues}
draggingIndex={draggingIndex}
draggingDelete={draggingDelete}
onStartMove={onStartMove}
onOffsetChange={onHandleOffsetChange}
onFocus={onFocus}
onBlur={onBlur}
handleRender={handleRender}
activeHandleRender={activeHandleRender}
onChangeComplete={finishChange}
onDelete={rangeEditable ? onDelete : undefined}
/>

<Marks prefixCls={prefixCls} marks={markList} onClick={changeToCloseValue} />
117 changes: 75 additions & 42 deletions src/hooks/useDrag.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,9 @@ import * as React from 'react';
import type { Direction, OnStartMove } from '../interface';
import type { OffsetValues } from './useOffset';

/** Drag to delete offset. It's a user experience number for dragging out */
const REMOVE_DIST = 130;

function getPosition(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) {
const obj = 'touches' in e ? e.touches[0] : e;

@@ -19,14 +22,17 @@ function useDrag(
triggerChange: (values: number[]) => void,
finishChange: () => void,
offsetValues: OffsetValues,
editable: boolean,
): [
draggingIndex: number,
draggingValue: number,
draggingDelete: boolean,
returnValues: number[],
onStartMove: OnStartMove,
] {
const [draggingValue, setDraggingValue] = React.useState(null);
const [draggingIndex, setDraggingIndex] = React.useState(-1);
const [draggingDelete, setDraggingDelete] = React.useState(false);
const [cacheValues, setCacheValues] = React.useState(rawValues);
const [originValues, setOriginValues] = React.useState(rawValues);

@@ -50,50 +56,55 @@ function useDrag(
[],
);

const flushValues = (nextValues: number[], nextValue?: number) => {
const flushValues = (nextValues: number[], nextValue?: number, deleteMark?: boolean) => {
// Perf: Only update state when value changed
if (cacheValues.some((val, i) => val !== nextValues[i])) {
if (cacheValues.some((val, i) => val !== nextValues[i]) || deleteMark) {
if (nextValue !== undefined) {
setDraggingValue(nextValue);
}
setCacheValues(nextValues);
triggerChange(nextValues);

let changeValues = nextValues;
if (deleteMark) {
changeValues = nextValues.filter((_, i) => i !== draggingIndex);
}
triggerChange(changeValues);
}
};

const updateCacheValue = useEvent((valueIndex: number, offsetPercent: number) => {
// Basic point offset

if (valueIndex === -1) {
// >>>> Dragging on the track
const startValue = originValues[0];
const endValue = originValues[originValues.length - 1];
const maxStartOffset = min - startValue;
const maxEndOffset = max - endValue;

// Get valid offset
let offset = offsetPercent * (max - min);
offset = Math.max(offset, maxStartOffset);
offset = Math.min(offset, maxEndOffset);

// Use first value to revert back of valid offset (like steps marks)
const formatStartValue = formatValue(startValue + offset);
offset = formatStartValue - startValue;
const cloneCacheValues = originValues.map<number>((val) => val + offset);
flushValues(cloneCacheValues);
} else {
// >>>> Dragging on the handle
const offsetDist = (max - min) * offsetPercent;

// Always start with the valueIndex origin value
const cloneValues = [...cacheValues];
cloneValues[valueIndex] = originValues[valueIndex];

const next = offsetValues(cloneValues, offsetDist, valueIndex, 'dist');

flushValues(next.values, next.value);
}
});
const updateCacheValue = useEvent(
(valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
if (valueIndex === -1) {
// >>>> Dragging on the track
const startValue = originValues[0];
const endValue = originValues[originValues.length - 1];
const maxStartOffset = min - startValue;
const maxEndOffset = max - endValue;

// Get valid offset
let offset = offsetPercent * (max - min);
offset = Math.max(offset, maxStartOffset);
offset = Math.min(offset, maxEndOffset);

// Use first value to revert back of valid offset (like steps marks)
const formatStartValue = formatValue(startValue + offset);
offset = formatStartValue - startValue;
const cloneCacheValues = originValues.map<number>((val) => val + offset);
flushValues(cloneCacheValues);
} else {
// >>>> Dragging on the handle
const offsetDist = (max - min) * offsetPercent;

// Always start with the valueIndex origin value
const cloneValues = [...cacheValues];
cloneValues[valueIndex] = originValues[valueIndex];

const next = offsetValues(cloneValues, offsetDist, valueIndex, 'dist');

flushValues(next.values, next.value, deleteMark);
}
},
);

const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => {
e.stopPropagation();
@@ -105,6 +116,8 @@ function useDrag(
setDraggingIndex(valueIndex);
setDraggingValue(originValue);
setOriginValues(initialValues);
setCacheValues(initialValues);
setDraggingDelete(false);

const { pageX: startX, pageY: startY } = getPosition(e);

@@ -119,23 +132,34 @@ function useDrag(
const { width, height } = containerRef.current.getBoundingClientRect();

let offSetPercent: number;
let removeDist: number;

switch (direction) {
case 'btt':
offSetPercent = -offsetY / height;
removeDist = offsetX;
break;

case 'ttb':
offSetPercent = offsetY / height;
removeDist = offsetX;
break;

case 'rtl':
offSetPercent = -offsetX / width;
removeDist = offsetY;
break;

default:
offSetPercent = offsetX / width;
removeDist = offsetY;
}
updateCacheValue(valueIndex, offSetPercent);

// Check if need mark remove
const deleteMark = editable ? Math.abs(removeDist) > REMOVE_DIST : false;
setDraggingDelete(deleteMark);

updateCacheValue(valueIndex, offSetPercent, deleteMark);
};

// End
@@ -166,12 +190,21 @@ function useDrag(
const sourceValues = [...rawValues].sort((a, b) => a - b);
const targetValues = [...cacheValues].sort((a, b) => a - b);

return sourceValues.every((val, index) => val === targetValues[index])
? cacheValues
: rawValues;
}, [rawValues, cacheValues]);
const counts: Record<number, number> = {};
targetValues.forEach((val) => {
counts[val] = (counts[val] || 0) + 1;
});
sourceValues.forEach((val) => {
counts[val] = (counts[val] || 0) - 1;
});

const maxDiffCount = editable ? 1 : 0;
const diffCount: number = Object.values(counts).reduce((prev, next) => prev + next, 0);

return diffCount <= maxDiffCount ? cacheValues : rawValues;
}, [rawValues, cacheValues, editable]);

return [draggingIndex, draggingValue, returnValues, onStartMove];
return [draggingIndex, draggingValue, draggingDelete, returnValues, onStartMove];
}

export default useDrag;
21 changes: 21 additions & 0 deletions src/hooks/useRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { warning } from 'rc-util/lib/warning';
import { useMemo } from 'react';
import type { SliderProps } from '../Slider';

export default function useRange(
range?: SliderProps['range'],
): [range: boolean, rangeEditable: boolean, rangeDraggableTrack: boolean] {
return useMemo(() => {
if (range === true || !range) {
return [!!range, false, false];
}

const { editable, draggableTrack } = range;

if (process.env.NODE_ENV !== 'production') {
warning(!editable || !draggableTrack, '`editable` can not work with `draggableTrack`.');
}

return [true, editable, !editable && draggableTrack];
}, [range]);
}
172 changes: 145 additions & 27 deletions tests/Range.test.js → tests/Range.test.tsx
Original file line number Diff line number Diff line change
@@ -5,54 +5,70 @@ import keyCode from 'rc-util/lib/KeyCode';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import { resetWarned } from 'rc-util/lib/warning';
import React from 'react';
import Slider from '../src/';
import Slider from '../src';

describe('Range', () => {
let container;

beforeAll(() => {
spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
width: 100,
height: 100,
left: 0,
top: 0,
bottom: 100,
right: 100,
}),
});
});

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
resetWarned();
});

function doMouseMove(container, start, end, element = 'rc-slider-handle') {
function doMouseDown(container: HTMLElement, start: number, element = 'rc-slider-handle') {
const mouseDown = createEvent.mouseDown(container.getElementsByClassName(element)[0]);
mouseDown.pageX = start;
mouseDown.pageY = start;
(mouseDown as any).pageX = start;
(mouseDown as any).pageY = start;
Object.defineProperties(mouseDown, {
clientX: { get: () => start },
clientY: { get: () => start },
});

fireEvent(container.getElementsByClassName(element)[0], mouseDown);
}

function doMouseMove(
container: HTMLElement,
start: number,
end: number,
element = 'rc-slider-handle',
) {
doMouseDown(container, start, element);

// Drag
const mouseMove = createEvent.mouseMove(document);
mouseMove.pageX = end;
mouseMove.pageY = end;
(mouseMove as any).pageX = end;
(mouseMove as any).pageY = end;
fireEvent(document, mouseMove);
}

function doTouchMove(container, start, end, element = 'rc-slider-handle') {
function doTouchMove(
container: HTMLElement,
start: number,
end: number,
element = 'rc-slider-handle',
) {
const touchStart = createEvent.touchStart(container.getElementsByClassName(element)[0], {
touches: [{}],
});
touchStart.touches[0].pageX = start;
(touchStart as any).touches[0].pageX = start;
fireEvent(container.getElementsByClassName(element)[0], touchStart);

// Drag
const touchMove = createEvent.touchMove(document, {
touches: [{}],
});
touchMove.touches[0].pageX = end;
(touchMove as any).touches[0].pageX = end;
fireEvent(document, touchMove);
}

@@ -344,7 +360,7 @@ describe('Range', () => {
return (
<Slider
range
onChange={(values) => {
onChange={(values: number[]) => {
setValue(values);
onChange(values);
}}
@@ -377,7 +393,7 @@ describe('Range', () => {
const onChange = jest.fn();

const { container, unmount } = render(
<Slider range defaultValue={[0, 30]} draggableTrack onChange={onChange} />,
<Slider range={{ draggableTrack: true }} defaultValue={[0, 30]} onChange={onChange} />,
);

// Do move
@@ -507,24 +523,23 @@ describe('Range', () => {
it('focus()', () => {
const handleFocus = jest.fn();
const { container } = render(<Slider range min={0} max={20} onFocus={handleFocus} />);
container.getElementsByClassName('rc-slider-handle')[0].focus();
container.querySelector<HTMLDivElement>('.rc-slider-handle').focus();
expect(handleFocus).toBeCalled();
});

it('blur()', () => {
const handleBlur = jest.fn();
const { container } = render(<Slider range min={0} max={20} onBlur={handleBlur} />);
container.getElementsByClassName('rc-slider-handle')[0].focus();
container.getElementsByClassName('rc-slider-handle')[0].blur();
container.querySelector<HTMLDivElement>('.rc-slider-handle').focus();
container.querySelector<HTMLDivElement>('.rc-slider-handle').blur();
expect(handleBlur).toHaveBeenCalled();
});
});

it('warning for `draggableTrack` and `mergedStep=null`', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

resetWarned();
render(<Slider range draggableTrack step={null} />);
render(<Slider range={{ draggableTrack: true }} step={null} />);

expect(errorSpy).toHaveBeenCalledWith(
'Warning: `draggableTrack` is not supported when `step` is `null`.',
@@ -534,16 +549,15 @@ describe('Range', () => {

it('Track should have the correct thickness', () => {
const { container } = render(
<Slider range allowCross={false} reverse defaultValue={[0, 40]} draggableTrack />,
<Slider range={{ draggableTrack: true }} allowCross={false} reverse defaultValue={[0, 40]} />,
);

const { container: containerVertical } = render(
<Slider
range
range={{ draggableTrack: true }}
allowCross={false}
reverse
defaultValue={[0, 40]}
draggableTrack
vertical
style={{ height: '300px' }}
/>,
@@ -599,4 +613,108 @@ describe('Range', () => {
expect(container.querySelector('.rc-slider-handle')).toHaveClass('my-handle');
expect(container.querySelector('.rc-slider-rail')).toHaveClass('my-rail');
});

describe('editable', () => {
it('click to create', () => {
const onChange = jest.fn();
const { container } = render(
<Slider
onChange={onChange}
min={0}
max={100}
value={[0, 100]}
range={{ editable: true }}
/>,
);

doMouseDown(container, 50, 'rc-slider');

expect(onChange).toHaveBeenCalledWith([0, 50, 100]);
});

it('can not editable with draggableTrack at same time', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Slider range={{ editable: true, draggableTrack: true }} />);

expect(errorSpy).toHaveBeenCalledWith(
'Warning: `editable` can not work with `draggableTrack`.',
);
errorSpy.mockRestore();
});

describe('drag out to remove', () => {
it('uncontrolled', () => {
const onChange = jest.fn();
const onChangeComplete = jest.fn();
const { container } = render(
<Slider
onChange={onChange}
onChangeComplete={onChangeComplete}
min={0}
max={100}
defaultValue={[0, 50, 100]}
range={{ editable: true }}
/>,
);

doMouseMove(container, 0, 1000);
expect(onChange).toHaveBeenCalledWith([50, 100]);

// Fire mouse up
fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
});

it('controlled', () => {
const onChange = jest.fn();
const onChangeComplete = jest.fn();

const Demo = () => {
const [value, setValue] = React.useState([0, 50, 100]);
return (
<Slider
onChange={(nextValue: number[]) => {
onChange(nextValue);
setValue(nextValue);
}}
onChangeComplete={onChangeComplete}
min={0}
max={100}
value={value}
range={{ editable: true }}
/>
);
};

const { container } = render(<Demo />);

doMouseMove(container, 0, 1000);
expect(onChange).toHaveBeenCalledWith([50, 100]);

// Fire mouse up
fireEvent.mouseUp(container.querySelector('.rc-slider-handle'));
expect(onChangeComplete).toHaveBeenCalledWith([50, 100]);
});
});

it('key to delete', () => {
const onChange = jest.fn();

const { container } = render(
<Slider
onChange={onChange}
min={0}
max={100}
defaultValue={[0, 50, 100]}
range={{ editable: true }}
/>,
);

fireEvent.keyDown(container.querySelectorAll('.rc-slider-handle')[1], {
keyCode: keyCode.DELETE,
});

expect(onChange).toHaveBeenCalledWith([0, 100]);
});
});
});
File renamed without changes.