diff --git a/assets/index.less b/assets/index.less index aa10b3d3a..e2cb4e411 100644 --- a/assets/index.less +++ b/assets/index.less @@ -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); } diff --git a/docs/demo/editable.md b/docs/demo/editable.md new file mode 100644 index 000000000..3c31ad9b4 --- /dev/null +++ b/docs/demo/editable.md @@ -0,0 +1,8 @@ +--- +title: Multiple +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/editable.tsx b/docs/examples/editable.tsx new file mode 100644 index 000000000..051ef4e8f --- /dev/null +++ b/docs/examples/editable.tsx @@ -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]); + + return ( +
+
+ { + console.error('Change:', nextValue); + setValue(nextValue as any); + }} + onChangeComplete={(nextValue) => { + console.log('Complete', nextValue); + }} + styles={{ + rail: { + background: `linear-gradient(to right, blue, red)`, + }, + }} + /> +
+
+ ); +}; diff --git a/docs/examples/multiple.tsx b/docs/examples/multiple.tsx index 55bc91d25..5f5e96301 100644 --- a/docs/examples/multiple.tsx +++ b/docs/examples/multiple.tsx @@ -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 (
@@ -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); diff --git a/package.json b/package.json index 08efda1f8..a33a27dcc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Handles/Handle.tsx b/src/Handles/Handle.tsx index bf802145e..1a54f6ccd 100644 --- a/src/Handles/Handle.tsx +++ b/src/Handles/Handle.tsx @@ -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, index: number) => void; onMouseEnter: (e: React.MouseEvent, index: number) => void; @@ -37,9 +40,11 @@ const Handle = React.forwardRef((props, ref) => { value, valueIndex, onStartMove, + onDelete, style, render, dragging, + draggingDelete, onOffsetChange, onChangeComplete, onFocus, @@ -118,6 +123,11 @@ const Handle = React.forwardRef((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((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((props, ref) => { prefixCls, value, dragging, + draggingDelete, }); } diff --git a/src/Handles/index.tsx b/src/Handles/index.tsx index c412c9c96..c7a4f97bd 100644 --- a/src/Handles/index.tsx +++ b/src/Handles/index.tsx @@ -12,6 +12,7 @@ export interface HandlesProps { onOffsetChange: (value: number | 'min' | 'max', valueIndex: number) => void; onFocus?: (e: React.FocusEvent) => void; onBlur?: (e: React.FocusEvent) => 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((props, ref) => { handleRender, activeHandleRender, draggingIndex, + draggingDelete, onFocus, ...restProps } = props; @@ -74,24 +77,30 @@ const Handles = React.forwardRef((props, ref) => { return ( <> - {values.map((value, index) => ( - { - 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((value, index) => { + const dragging = draggingIndex === index; + + return ( + { + 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 && ( ((props, ref) => { value={values[activeIndex]} valueIndex={null} dragging={draggingIndex !== -1} + draggingDelete={draggingDelete} render={activeHandleRender} style={{ pointerEvents: 'none' }} tabIndex={null} diff --git a/src/Slider.tsx b/src/Slider.tsx index 1168e4b38..78af41525 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -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 { prefixCls?: string; className?: string; @@ -51,7 +58,7 @@ export interface SliderProps { onBlur?: (e: React.FocusEvent) => void; // Value - range?: boolean; + range?: boolean | RangeConfig; count?: number; min?: number; max?: number; @@ -68,8 +75,6 @@ export interface SliderProps { // Cross allowCross?: boolean; pushable?: boolean | number; - /** range only */ - draggableTrack?: boolean; // Direction reverse?: boolean; @@ -94,6 +99,7 @@ export interface SliderProps { // Components handleRender?: HandlesProps['handleRender']; activeHandleRender?: HandlesProps['handleRender']; + track?: boolean; // Accessibility tabIndex?: number | number[]; @@ -138,7 +144,6 @@ const Slider = React.forwardRef>((prop // Cross allowCross = true, pushable = false, - draggableTrack, // Direction reverse, @@ -160,6 +165,7 @@ const Slider = React.forwardRef>((prop // Components handleRender, activeHandleRender, + track, // Accessibility tabIndex = 0, @@ -179,6 +185,8 @@ const Slider = React.forwardRef>((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>((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>((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>((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,15 +351,23 @@ const Slider = React.forwardRef>((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); } @@ -339,8 +375,8 @@ const Slider = React.forwardRef>((prop 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>((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>((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>((prop included, includedStart, includedEnd, - range, + range: rangeEnabled, tabIndex, ariaLabelForHandle, ariaLabelledByForHandle, @@ -492,7 +528,7 @@ const Slider = React.forwardRef>((prop included, includedStart, includedEnd, - range, + rangeEnabled, tabIndex, ariaLabelForHandle, ariaLabelledByForHandle, @@ -521,13 +557,15 @@ const Slider = React.forwardRef>((prop style={{ ...railStyle, ...styles?.rail }} /> - + {track !== false && ( + + )} >((prop style={handleStyle} values={cacheValues} draggingIndex={draggingIndex} + draggingDelete={draggingDelete} onStartMove={onStartMove} onOffsetChange={onHandleOffsetChange} onFocus={onFocus} @@ -550,6 +589,7 @@ const Slider = React.forwardRef>((prop handleRender={handleRender} activeHandleRender={activeHandleRender} onChangeComplete={finishChange} + onDelete={rangeEditable ? onDelete : undefined} /> diff --git a/src/hooks/useDrag.ts b/src/hooks/useDrag.ts index 49fb8fc80..37de73a42 100644 --- a/src/hooks/useDrag.ts +++ b/src/hooks/useDrag.ts @@ -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((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((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 = {}; + 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; diff --git a/src/hooks/useRange.ts b/src/hooks/useRange.ts new file mode 100644 index 000000000..ff109f239 --- /dev/null +++ b/src/hooks/useRange.ts @@ -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]); +} diff --git a/tests/Range.test.js b/tests/Range.test.tsx similarity index 81% rename from tests/Range.test.js rename to tests/Range.test.tsx index 638e22d93..63010ee4d 100644 --- a/tests/Range.test.js +++ b/tests/Range.test.tsx @@ -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 ( { + onChange={(values: number[]) => { setValue(values); onChange(values); }} @@ -377,7 +393,7 @@ describe('Range', () => { const onChange = jest.fn(); const { container, unmount } = render( - , + , ); // Do move @@ -507,15 +523,15 @@ describe('Range', () => { it('focus()', () => { const handleFocus = jest.fn(); const { container } = render(); - container.getElementsByClassName('rc-slider-handle')[0].focus(); + container.querySelector('.rc-slider-handle').focus(); expect(handleFocus).toBeCalled(); }); it('blur()', () => { const handleBlur = jest.fn(); const { container } = render(); - container.getElementsByClassName('rc-slider-handle')[0].focus(); - container.getElementsByClassName('rc-slider-handle')[0].blur(); + container.querySelector('.rc-slider-handle').focus(); + container.querySelector('.rc-slider-handle').blur(); expect(handleBlur).toHaveBeenCalled(); }); }); @@ -523,8 +539,7 @@ describe('Range', () => { it('warning for `draggableTrack` and `mergedStep=null`', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - resetWarned(); - render(); + render(); 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( - , + , ); const { container: containerVertical } = render( , @@ -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( + , + ); + + 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(); + + 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( + , + ); + + 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 ( + { + onChange(nextValue); + setValue(nextValue); + }} + onChangeComplete={onChangeComplete} + min={0} + max={100} + value={value} + range={{ editable: true }} + /> + ); + }; + + const { container } = render(); + + 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( + , + ); + + fireEvent.keyDown(container.querySelectorAll('.rc-slider-handle')[1], { + keyCode: keyCode.DELETE, + }); + + expect(onChange).toHaveBeenCalledWith([0, 100]); + }); + }); }); diff --git a/tests/__snapshots__/Range.test.js.snap b/tests/__snapshots__/Range.test.tsx.snap similarity index 100% rename from tests/__snapshots__/Range.test.js.snap rename to tests/__snapshots__/Range.test.tsx.snap