diff --git a/README.md b/README.md
index 76577985f..6beb5a60f 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ The following APIs are shared by Slider and Range.
| handle | (props) => React.ReactNode | | A handle generator which could be used to customized handle. |
| included | boolean | `true` | If the value is `true`, it means a continuous value interval, otherwise, it is a independent value. |
| reverse | boolean | `false` | If the value is `true`, it means the component is rendered reverse. |
-| disabled | boolean | `false` | If `true`, handles can't be moved. |
+| disabled | boolean \| boolean[] | `false` | If `true`, handles can't be moved. This prop can also be an array to disable specific handles in range mode, e.g. `[true, false, true]` disables first and third handles. When any rendered handle is disabled, `editable` mode will be disabled. |
| keyboard | boolean | `true` | Support using keyboard to move handlers. |
| dots | boolean | `false` | When the `step` value is greater than 1, you can set the `dots` to `true` if you want to render the slider with dots. |
| onBeforeChange | Function | NOOP | `onBeforeChange` will be triggered when `ontouchstart` or `onmousedown` is triggered. |
diff --git a/assets/index.less b/assets/index.less
index 64daded8e..e85c9ab8b 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -105,6 +105,20 @@
cursor: -webkit-grabbing;
cursor: grabbing;
}
+
+ &-disabled {
+ background-color: #fff;
+ border-color: @disabledColor;
+ box-shadow: none;
+ cursor: not-allowed;
+
+ &:hover,
+ &:active {
+ border-color: @disabledColor;
+ box-shadow: none;
+ cursor: not-allowed;
+ }
+ }
}
&-mark {
diff --git a/docs/demo/disabled-handle.md b/docs/demo/disabled-handle.md
new file mode 100644
index 000000000..10c4d9c1f
--- /dev/null
+++ b/docs/demo/disabled-handle.md
@@ -0,0 +1,9 @@
+---
+title: Disabled Handle
+title.zh-CN: 禁用特定滑块
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/examples/disabled-handle.tsx b/docs/examples/disabled-handle.tsx
new file mode 100644
index 000000000..9967ad1ea
--- /dev/null
+++ b/docs/examples/disabled-handle.tsx
@@ -0,0 +1,170 @@
+/* eslint react/no-multi-comp: 0, no-console: 0 */
+import Slider from '@rc-component/slider';
+import React, { useState } from 'react';
+import '../../assets/index.less';
+
+const style: React.CSSProperties = {
+ width: 400,
+ margin: 50,
+};
+
+const defaultValue = [0, 30, 60, 100];
+const BasicDisabledHandle = () => {
+ const [disabled, setDisabled] = useState([true, false, false, false]);
+
+ return (
+
+
+ Slider disabled {JSON.stringify(disabled)}
+
+ {defaultValue.map((_, index) => (
+
+ {
+ const newDisabled = [...disabled];
+ newDisabled[index] = !newDisabled[index];
+ setDisabled(newDisabled);
+ }}
+ />
+ Handle {index + 1} {disabled[index] ? 'Disabled' : 'Enabled'}
+
+ ))}
+
+
+ );
+};
+
+const DisabledHandleAsBoundary = () => {
+ const [value, setValue] = useState([10, 50, 90]);
+
+ return (
+
+
Array.isArray(v) && setValue(v)}
+ disabled={[false, true, false]}
+ />
+
+ Middle handle (50) is disabled and acts as a boundary. First handle cannot go beyond 50,
+ third handle cannot go below 50. Disabled handle has gray border and not-allowed cursor.
+
+
+ );
+};
+
+const PushableWithDisabledHandle = () => {
+ const [value, setValue] = useState([20, 40, 60, 80]);
+
+ return (
+
+
setValue(v as number[])}
+ disabled={[false, true, false, false]}
+ pushable={10}
+ />
+
+ Second handle (40) is disabled. Drag the first handle toward it or push the last two handles
+ together: enabled handles keep at least 10 apart without crossing the disabled handle.
+
+
+ );
+};
+
+const SingleSlider = () => {
+ const [value1, setValue1] = useState(30);
+ const [value2, setValue2] = useState(30);
+
+ return (
+
+ setValue1(v as number)} disabled />
+
+ setValue2(v as number)} disabled={false} />
+
+ );
+};
+
+// Editable mode with disabled handles - editable is disabled when any handle is disabled
+const EditableWithDisabled = () => {
+ const [value, setValue] = useState([0, 30, 100]);
+ const [disabled, setDisabled] = useState([true, false, false]);
+
+ const hasDisabled = disabled.some((d) => d);
+
+ return (
+
+
setValue(v as number[])}
+ disabled={disabled}
+ />
+
+ {hasDisabled
+ ? 'Editable mode is DISABLED because at least one handle is disabled. Clicking track will move nearest enabled handle.'
+ : 'Editable mode is ENABLED. Click track to add handles, drag to edge to delete.'}
+
+
+ {value.map((val, index) => (
+
+ {
+ const newDisabled = [...disabled];
+ newDisabled[index] = !newDisabled[index];
+ setDisabled(newDisabled);
+ }}
+ />
+ Handle {index + 1} ({val}) {disabled[index] ? 'Disabled' : 'Enabled'}
+
+ ))}
+
+
+ Try: Toggle checkboxes to enable/disable handles. When any handle is disabled, you cannot
+ add or remove handles. When all handles are enabled, editable mode works normally.
+
+
+ );
+};
+
+export default () => (
+
+
+ single handle disabled
+
+
+
+
Disabled Handle
+
+ Toggle checkboxes to disable/enable specific handles. Track dragging is disabled when any
+ handle is disabled.
+
+
+
+
+
+
Disabled Handle as Boundary
+
+
+
+
+
Disabled Handle + Pushable
+
+
+
+
+
Editable + Disabled (Editable Disabled When Any Handle Disabled)
+
+
+
+);
diff --git a/src/Handles/Handle.tsx b/src/Handles/Handle.tsx
index edd7f1149..ae902f4b6 100644
--- a/src/Handles/Handle.tsx
+++ b/src/Handles/Handle.tsx
@@ -55,7 +55,6 @@ const Handle = React.forwardRef((props, ref) => {
min,
max,
direction,
- disabled,
keyboard,
range,
tabIndex,
@@ -65,15 +64,20 @@ const Handle = React.forwardRef((props, ref) => {
ariaValueTextFormatterForHandle,
styles,
classNames,
+ isHandleDisabled,
} = React.useContext(SliderContext);
+ const mergedDisabled = isHandleDisabled(valueIndex);
+
const handlePrefixCls = `${prefixCls}-handle`;
// ============================ Events ============================
const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
- if (!disabled) {
- onStartMove(e, valueIndex);
+ if (mergedDisabled) {
+ e.stopPropagation();
+ return;
}
+ onStartMove(e, valueIndex);
};
const onInternalFocus = (e: React.FocusEvent) => {
@@ -86,7 +90,7 @@ const Handle = React.forwardRef((props, ref) => {
// =========================== Keyboard ===========================
const onKeyDown: React.KeyboardEventHandler = (e) => {
- if (!disabled && keyboard) {
+ if (!mergedDisabled && keyboard) {
let offset: number | 'min' | 'max' | undefined;
// Change the value
@@ -161,12 +165,12 @@ const Handle = React.forwardRef((props, ref) => {
if (valueIndex !== null) {
divProps = {
- tabIndex: disabled ? undefined : getIndex(tabIndex, valueIndex) ?? undefined,
+ tabIndex: mergedDisabled ? undefined : getIndex(tabIndex, valueIndex) ?? undefined,
role: 'slider',
'aria-valuemin': min,
'aria-valuemax': max,
'aria-valuenow': value,
- 'aria-disabled': disabled,
+ 'aria-disabled': mergedDisabled,
'aria-label': getIndex(ariaLabelForHandle, valueIndex),
'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex),
'aria-required': getIndex(ariaRequired, valueIndex),
@@ -190,6 +194,7 @@ const Handle = React.forwardRef((props, ref) => {
[`${handlePrefixCls}-${valueIndex + 1}`]: valueIndex !== null && range,
[`${handlePrefixCls}-dragging`]: dragging,
[`${handlePrefixCls}-dragging-delete`]: draggingDelete,
+ [`${handlePrefixCls}-disabled`]: mergedDisabled,
},
classNames.handle,
)}
diff --git a/src/Slider.tsx b/src/Slider.tsx
index 21b846084..2734924e8 100644
--- a/src/Slider.tsx
+++ b/src/Slider.tsx
@@ -12,8 +12,9 @@ import Steps from './Steps';
import Tracks from './Tracks';
import type { SliderContextProps } from './context';
import SliderContext from './context';
+import useDisabled from './hooks/useDisabled';
import useDrag from './hooks/useDrag';
-import useOffset from './hooks/useOffset';
+import useOffset, { getClosestEnabledHandleIndex } from './hooks/useOffset';
import useRange from './hooks/useRange';
import type {
AriaValueFormat,
@@ -57,7 +58,7 @@ export interface SliderProps {
id?: string;
// Status
- disabled?: boolean;
+ disabled?: boolean | boolean[];
keyboard?: boolean;
autoFocus?: boolean;
onFocus?: (e: React.FocusEvent) => void;
@@ -131,8 +132,7 @@ const Slider = React.forwardRef>((prop
id,
- // Status
- disabled = false,
+ disabled: rawDisabled = false,
keyboard = true,
autoFocus,
onFocus,
@@ -187,6 +187,7 @@ const Slider = React.forwardRef>((prop
const handlesRef = React.useRef(null);
const containerRef = React.useRef(null);
+ const [mergedValue, setValue] = useControlledState(defaultValue, value);
const direction = React.useMemo(() => {
if (vertical) {
@@ -241,6 +242,9 @@ const Slider = React.forwardRef>((prop
.sort((a, b) => a.value - b.value);
}, [marks]);
+ // ============================ Disabled ============================
+ const [isHandleDisabled, getDisabledState] = useDisabled(rawDisabled);
+
// ============================ Format ============================
const [formatValue, offsetValues] = useOffset(
mergedMin,
@@ -249,18 +253,17 @@ const Slider = React.forwardRef>((prop
markList,
allowCross,
mergedPush as false | number,
+ isHandleDisabled,
);
// ============================ Values ============================
- const [mergedValue, setValue] = useControlledState(defaultValue, value);
-
const rawValues = React.useMemo(() => {
const valueList =
mergedValue === null || mergedValue === undefined
? []
: Array.isArray(mergedValue)
- ? mergedValue
- : [mergedValue];
+ ? mergedValue
+ : [mergedValue];
const [val0 = mergedMin] = valueList;
let returnValues = mergedValue === null ? [] : [val0];
@@ -290,6 +293,13 @@ const Slider = React.forwardRef>((prop
return returnValues;
}, [mergedValue, rangeEnabled, mergedMin, count, formatValue]);
+ const [disabled, hasDisabledHandle] = React.useMemo(
+ () => getDisabledState(rawValues),
+ [getDisabledState, rawValues],
+ );
+
+ const effectiveRangeEditable = rangeEditable && !hasDisabledHandle;
+
// =========================== onChange ===========================
const getTriggerValue = (triggerValues: number[]) =>
rangeEnabled ? triggerValues : triggerValues[0];
@@ -323,7 +333,7 @@ const Slider = React.forwardRef>((prop
});
const onDelete = (index: number) => {
- if (disabled || !rangeEditable || rawValues.length <= minCount) {
+ if (disabled || !effectiveRangeEditable || rawValues.length <= minCount) {
return;
}
@@ -348,8 +358,9 @@ const Slider = React.forwardRef>((prop
triggerChange,
finishChange,
offsetValues,
- rangeEditable,
+ effectiveRangeEditable,
minCount,
+ isHandleDisabled,
);
/**
@@ -358,20 +369,30 @@ const Slider = React.forwardRef>((prop
*/
const changeToCloseValue = (newValue: number, e?: React.MouseEvent) => {
if (!disabled) {
+ const valueIndex = rawValues.length
+ ? getClosestEnabledHandleIndex(
+ rawValues,
+ newValue,
+ mergedMin,
+ mergedMax,
+ mergedPush as false | number,
+ isHandleDisabled,
+ )
+ : 0;
+
+ if (valueIndex === -1) {
+ return;
+ }
+
// Create new values
const cloneNextValues = [...rawValues];
- let valueIndex = 0;
let valueBeforeIndex = 0; // Record the index which value < newValue
- let valueDist = mergedMax - mergedMin;
+ const valueDist = rawValues.length
+ ? Math.abs(newValue - rawValues[valueIndex])
+ : mergedMax - mergedMin;
rawValues.forEach((val, index) => {
- const dist = Math.abs(newValue - val);
- if (dist <= valueDist) {
- valueDist = dist;
- valueIndex = index;
- }
-
if (val < newValue) {
valueBeforeIndex = index;
}
@@ -379,11 +400,12 @@ const Slider = React.forwardRef>((prop
let focusIndex = valueIndex;
- if (rangeEditable && valueDist !== 0 && (!maxCount || rawValues.length < maxCount)) {
+ if (effectiveRangeEditable && valueDist !== 0 && (!maxCount || rawValues.length < maxCount)) {
cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue);
focusIndex = valueBeforeIndex + 1;
} else {
cloneNextValues[valueIndex] = newValue;
+ focusIndex = valueIndex;
}
// Fill value to match default 2 (only when `rawValues` is empty)
@@ -442,22 +464,24 @@ const Slider = React.forwardRef>((prop
};
// =========================== Keyboard ===========================
- const [keyboardValue, setKeyboardValue] = React.useState(null!);
+ const [keyboardValue, setKeyboardValue] = React.useState<{ value: number; index: number }>(null!);
const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
- if (!disabled) {
+ if (!disabled && !isHandleDisabled(valueIndex)) {
const next = offsetValues(rawValues, offset, valueIndex);
onBeforeChange?.(getTriggerValue(rawValues));
triggerChange(next.values);
- setKeyboardValue(next.value);
+ setKeyboardValue({ value: next.value, index: valueIndex });
}
};
React.useEffect(() => {
if (keyboardValue !== null) {
- const valueIndex = rawValues.indexOf(keyboardValue);
+ const { value: nextKeyboardValue, index } = keyboardValue;
+ const valueIndex =
+ rawValues[index] === nextKeyboardValue ? index : rawValues.indexOf(nextKeyboardValue);
if (valueIndex >= 0) {
handlesRef.current!.focus(valueIndex);
}
@@ -551,6 +575,7 @@ const Slider = React.forwardRef>((prop
ariaValueTextFormatterForHandle,
styles: styles || {},
classNames: classNames || {},
+ isHandleDisabled,
}),
[
mergedMin,
@@ -570,6 +595,7 @@ const Slider = React.forwardRef>((prop
ariaValueTextFormatterForHandle,
styles,
classNames,
+ isHandleDisabled,
],
);
@@ -625,7 +651,7 @@ const Slider = React.forwardRef>((prop
handleRender={handleRender}
activeHandleRender={activeHandleRender}
onChangeComplete={finishChange}
- onDelete={rangeEditable ? onDelete : undefined}
+ onDelete={effectiveRangeEditable ? onDelete : undefined}
/>
diff --git a/src/Tracks/index.tsx b/src/Tracks/index.tsx
index 1b11e3301..6a9c2ee1f 100644
--- a/src/Tracks/index.tsx
+++ b/src/Tracks/index.tsx
@@ -14,8 +14,15 @@ export interface TrackProps {
}
const Tracks: React.FC = (props) => {
- const { prefixCls, style, values, startPoint, onStartMove } = props;
- const { included, range, min, styles, classNames } = React.useContext(SliderContext);
+ const { prefixCls, style, values, startPoint, onStartMove: propsOnStartMove } = props;
+ const { included, range, min, styles, classNames, isHandleDisabled } = React.useContext(SliderContext);
+
+ const hasDisabledHandle = React.useMemo(
+ () => values.some((_, index) => isHandleDisabled(index)),
+ [isHandleDisabled, values],
+ );
+
+ const onStartMove = hasDisabledHandle ? undefined : propsOnStartMove;
// =========================== List ===========================
const trackList = React.useMemo(() => {
diff --git a/src/context.ts b/src/context.ts
index 9a0901c7a..1b13b6fda 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -19,6 +19,7 @@ export interface SliderContextProps {
ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
classNames: SliderClassNames;
styles: SliderStyles;
+ isHandleDisabled: (index: number) => boolean;
}
const SliderContext = React.createContext({
@@ -32,6 +33,7 @@ const SliderContext = React.createContext({
keyboard: true,
styles: {},
classNames: {},
+ isHandleDisabled: () => false,
});
export default SliderContext;
diff --git a/src/hooks/useDisabled.ts b/src/hooks/useDisabled.ts
new file mode 100644
index 000000000..a25081a0b
--- /dev/null
+++ b/src/hooks/useDisabled.ts
@@ -0,0 +1,40 @@
+import * as React from 'react';
+
+const useDisabled = (
+ rawDisabled: boolean | boolean[],
+): [
+ isHandleDisabled: (index: number) => boolean,
+ getDisabledState: (rawValues: number[]) => [disabled: boolean, hasDisabledHandle: boolean],
+] => {
+ const isHandleDisabled = React.useCallback(
+ (index: number) => {
+ if (typeof rawDisabled === 'boolean') {
+ return rawDisabled;
+ }
+
+ return rawDisabled[index] ?? false;
+ },
+ [rawDisabled],
+ );
+
+ const getDisabledState = React.useCallback(
+ (rawValues: number[]): [disabled: boolean, hasDisabledHandle: boolean] => {
+ if (typeof rawDisabled === 'boolean') {
+ return [rawDisabled, rawDisabled && rawValues.length > 0];
+ }
+
+ return [
+ rawValues.length > 0 && rawValues.every((_, index) => isHandleDisabled(index)),
+ rawValues.some((_, index) => isHandleDisabled(index)),
+ ];
+ },
+ [rawDisabled, isHandleDisabled],
+ );
+
+ return React.useMemo(() => [isHandleDisabled, getDisabledState], [
+ isHandleDisabled,
+ getDisabledState,
+ ]);
+};
+
+export default useDisabled;
diff --git a/src/hooks/useDrag.ts b/src/hooks/useDrag.ts
index dcdcf61d1..99455d735 100644
--- a/src/hooks/useDrag.ts
+++ b/src/hooks/useDrag.ts
@@ -26,6 +26,7 @@ function useDrag(
offsetValues: OffsetValues,
editable: boolean,
minCount: number,
+ isHandleDisabled: (index: number) => boolean,
): [
draggingIndex: number,
draggingValue: number,
@@ -85,6 +86,7 @@ function useDrag(
}
triggerChange(changeValues);
+ // Optional callback for drag change (not used in current implementation)
if (onDragChange) {
onDragChange({
rawValues: nextValues,
@@ -99,6 +101,11 @@ function useDrag(
(valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
if (valueIndex === -1) {
// >>>> Dragging on the track
+ // Defensive: should not happen as Tracks/index.tsx blocks this when any handle is disabled
+ if (originValues.some((_, index) => isHandleDisabled(index))) {
+ return;
+ }
+
const startValue = originValues[0];
const endValue = originValues[originValues.length - 1];
const maxStartOffset = min - startValue;
@@ -132,8 +139,12 @@ function useDrag(
const onStartMove: OnStartMove = (e, valueIndex, startValues?: number[]) => {
e.stopPropagation();
- // 如果是点击 track 触发的,需要传入变化后的初始值,而不能直接用 rawValues
const initialValues = startValues || rawValues;
+ // Defensive: should not happen as Handle.tsx blocks this when handle is disabled
+ if (isHandleDisabled(valueIndex)) {
+ return;
+ }
+
const originValue = initialValues[valueIndex];
setDraggingIndex(valueIndex);
@@ -147,7 +158,7 @@ function useDrag(
// We declare it here since closure can't get outer latest value
let deleteMark = false;
- // Internal trigger event
+ // Optional callback for drag start (not used in current implementation)
if (onDragStart) {
onDragStart({
rawValues: initialValues,
diff --git a/src/hooks/useOffset.ts b/src/hooks/useOffset.ts
index 83fa3c57e..75a729268 100644
--- a/src/hooks/useOffset.ts
+++ b/src/hooks/useOffset.ts
@@ -11,6 +11,7 @@ type FormatStepValue = (value: number) => number;
type FormatValue = (value: number) => number;
type OffsetMode = 'unit' | 'dist';
+type IsHandleDisabled = (index: number) => boolean;
type OffsetValue = (
values: number[],
@@ -29,6 +30,82 @@ export type OffsetValues = (
values: number[];
};
+/**
+ * Get the effective moving range for a handle.
+ * Disabled handles are treated as fixed anchors, and `pushable` is applied as the gap
+ * that enabled handles must keep away from those anchors.
+ */
+export const getDisabledBoundaryValues = (
+ values: number[],
+ valueIndex: number,
+ min: number,
+ max: number,
+ pushable: false | number,
+ isHandleDisabled: IsHandleDisabled,
+): [number, number] => {
+ const pushGap = typeof pushable === 'number' ? pushable : 0;
+ let minBound = min;
+ let maxBound = max;
+
+ for (let i = valueIndex - 1; i >= 0; i -= 1) {
+ if (isHandleDisabled(i)) {
+ minBound = values[i] + pushGap;
+ break;
+ }
+ }
+
+ for (let i = valueIndex + 1; i < values.length; i += 1) {
+ if (isHandleDisabled(i)) {
+ maxBound = values[i] - pushGap;
+ break;
+ }
+ }
+
+ return [minBound, maxBound];
+};
+
+/**
+ * Find the nearest enabled handle that can accept the target value.
+ * A handle is only considered when the target value falls inside its disabled-anchor
+ * boundaries, so clicking outside an enabled segment becomes a no-op.
+ */
+export const getClosestEnabledHandleIndex = (
+ values: number[],
+ targetValue: number,
+ min: number,
+ max: number,
+ pushable: false | number,
+ isHandleDisabled: IsHandleDisabled,
+) => {
+ let closestIndex = -1;
+ let closestDist = max - min;
+
+ values.forEach((value, index) => {
+ if (isHandleDisabled(index)) {
+ return;
+ }
+
+ const [minBound, maxBound] = getDisabledBoundaryValues(
+ values,
+ index,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+
+ if (minBound <= targetValue && targetValue <= maxBound) {
+ const dist = Math.abs(targetValue - value);
+ if (dist <= closestDist) {
+ closestDist = dist;
+ closestIndex = index;
+ }
+ }
+ });
+
+ return closestIndex;
+};
+
export default function useOffset(
min: number,
max: number,
@@ -36,6 +113,7 @@ export default function useOffset(
markList: InternalMarkObj[],
allowCross: boolean,
pushable: false | number,
+ isHandleDisabled: IsHandleDisabled,
): [FormatValue, OffsetValues] {
const formatRangeValue: FormatRangeValue = React.useCallback(
(val) => Math.max(min, Math.min(max, val)),
@@ -196,9 +274,25 @@ export default function useOffset(
const offsetValues: OffsetValues = (values, offset, valueIndex, mode = 'unit') => {
const nextValues = values.map(formatValue);
const originValue = nextValues[valueIndex];
+
+ const [minBound, maxBound] = getDisabledBoundaryValues(
+ nextValues,
+ valueIndex,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+
const nextValue = offsetValue(nextValues, offset, valueIndex, mode);
nextValues[valueIndex] = nextValue;
+ if (minBound <= maxBound) {
+ nextValues[valueIndex] = Math.max(minBound, Math.min(maxBound, nextValues[valueIndex]));
+ } else {
+ nextValues[valueIndex] = originValue;
+ }
+
if (allowCross === false) {
// >>>>> Allow Cross
const pushNum = pushable || 0;
@@ -224,35 +318,83 @@ export default function useOffset(
// >>>>>> Basic push
// End values
for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
+ if (isHandleDisabled(i)) {
+ break;
+ }
let changed = true;
while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
({ value: nextValues[i], changed } = offsetChangedValue(nextValues, 1, i));
}
+ const [, itemMaxBound] = getDisabledBoundaryValues(
+ nextValues,
+ i,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+ nextValues[i] = Math.min(nextValues[i], itemMaxBound);
}
// Start values
for (let i = valueIndex; i > 0; i -= 1) {
+ if (isHandleDisabled(i - 1)) {
+ break;
+ }
let changed = true;
while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
}
+ const [itemMinBound] = getDisabledBoundaryValues(
+ nextValues,
+ i - 1,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+ nextValues[i - 1] = Math.max(nextValues[i - 1], itemMinBound);
}
// >>>>> Revert back to safe push range
// End to Start
for (let i = nextValues.length - 1; i > 0; i -= 1) {
+ if (isHandleDisabled(i) || isHandleDisabled(i - 1)) {
+ continue;
+ }
let changed = true;
while (needPush(nextValues[i] - nextValues[i - 1]) && changed) {
({ value: nextValues[i - 1], changed } = offsetChangedValue(nextValues, -1, i - 1));
}
+ const [itemMinBound] = getDisabledBoundaryValues(
+ nextValues,
+ i - 1,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+ nextValues[i - 1] = Math.max(nextValues[i - 1], itemMinBound);
}
// Start to End
for (let i = 0; i < nextValues.length - 1; i += 1) {
+ if (isHandleDisabled(i) || isHandleDisabled(i + 1)) {
+ continue;
+ }
let changed = true;
while (needPush(nextValues[i + 1] - nextValues[i]) && changed) {
({ value: nextValues[i + 1], changed } = offsetChangedValue(nextValues, 1, i + 1));
}
+ const [, itemMaxBound] = getDisabledBoundaryValues(
+ nextValues,
+ i + 1,
+ min,
+ max,
+ pushable,
+ isHandleDisabled,
+ );
+ nextValues[i + 1] = Math.min(nextValues[i + 1], itemMaxBound);
}
}
diff --git a/tests/Range.test.tsx b/tests/Range.test.tsx
index 9b767162f..1f655af90 100644
--- a/tests/Range.test.tsx
+++ b/tests/Range.test.tsx
@@ -30,8 +30,9 @@ describe('Range', () => {
start: number,
element = 'rc-slider-handle',
skipEventCheck = false,
+ index = 0,
) {
- const ele = container.getElementsByClassName(element)[0];
+ const ele = container.getElementsByClassName(element)[index];
const mouseDown = createEvent.mouseDown(ele);
(mouseDown as any).pageX = start;
(mouseDown as any).pageY = start;
@@ -65,8 +66,9 @@ describe('Range', () => {
start: number,
end: number,
element = 'rc-slider-handle',
+ index = 0,
) {
- doMouseDown(container, start, element);
+ doMouseDown(container, start, element, false, index);
// Drag
doMouseDrag(end);
@@ -841,4 +843,212 @@ describe('Range', () => {
expect(onChange).toHaveBeenCalledWith([0, 50]);
});
});
+
+ describe('disabled as array', () => {
+ const getHandle = (container: HTMLElement, index = 0) =>
+ container.getElementsByClassName('rc-slider-handle')[index] as HTMLElement;
+
+ const repeatKeyDown = (element: Element, code: number, count: number) => {
+ for (let i = 0; i < count; i += 1) {
+ fireEvent.keyDown(element, { keyCode: code });
+ }
+ };
+
+ const getLastChange = (onChange: jest.Mock) =>
+ onChange.mock.calls[onChange.mock.calls.length - 1][0];
+
+ it('respects handle disabled state and boolean disabled fallback', () => {
+ const onChange = jest.fn();
+ const { container, rerender } = render(
+ ,
+ );
+
+ const disabledHandle = getHandle(container, 0);
+ const enabledHandle = getHandle(container, 1);
+
+ expect(disabledHandle).not.toHaveAttribute('tabIndex');
+ expect(disabledHandle).toHaveAttribute('aria-disabled', 'true');
+ expect(enabledHandle).toHaveAttribute('tabIndex');
+ expect(enabledHandle).toHaveAttribute('aria-disabled', 'false');
+
+ fireEvent.keyDown(disabledHandle, { keyCode: keyCode.RIGHT });
+ doMouseMove(container, 0, 80, 'rc-slider-handle');
+ fireEvent.mouseUp(document);
+ expect(onChange).not.toHaveBeenCalled();
+
+ fireEvent.keyDown(enabledHandle, { keyCode: keyCode.RIGHT });
+ expect(onChange).toHaveBeenCalledWith([0, 51, 100]);
+
+ rerender( );
+ expect(getHandle(container, 0)).not.toHaveAttribute('tabIndex');
+ doMouseDown(container, 30, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).toHaveBeenCalledTimes(1);
+ });
+
+ it('moves only eligible handles when clicking the track', () => {
+ const onChange = jest.fn();
+ const { container, rerender } = render(
+ ,
+ );
+
+ doMouseDown(container, 10, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).toHaveBeenCalledWith([0, 10, 100]);
+
+ onChange.mockClear();
+ rerender( );
+ doMouseDown(container, 10, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).not.toHaveBeenCalled();
+
+ rerender( );
+ doMouseDown(container, 90, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).not.toHaveBeenCalled();
+
+ rerender( );
+ doMouseDown(container, 10, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it('disables editable operations when any handle is disabled', () => {
+ const onChange = jest.fn();
+ const { container, rerender } = render(
+ ,
+ );
+
+ const handle = getHandle(container, 0);
+ fireEvent.mouseEnter(handle);
+ fireEvent.keyDown(handle, { keyCode: keyCode.DELETE });
+ expect(onChange).not.toHaveBeenCalled();
+
+ doMouseDown(container, 25, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).toHaveBeenCalledWith([25, 50, 100]);
+
+ onChange.mockClear();
+ rerender(
+ ,
+ );
+
+ doMouseDown(container, 40, 'rc-slider', true);
+ fireEvent.mouseUp(document);
+ expect(onChange).toHaveBeenCalledWith([20, 40]);
+ });
+
+ it('disables draggableTrack only when rendered handles are disabled', () => {
+ const onChange = jest.fn();
+ const { container, unmount } = render(
+ ,
+ );
+
+ const track = container.getElementsByClassName('rc-slider-track')[0];
+ const mouseDown = createEvent.mouseDown(track);
+ Object.defineProperties(mouseDown, {
+ clientX: { get: () => 0 },
+ clientY: { get: () => 0 },
+ });
+ fireEvent(track, mouseDown);
+
+ const mouseMove = createEvent.mouseMove(document);
+ (mouseMove as any).pageX = 20;
+ (mouseMove as any).pageY = 20;
+ fireEvent(document, mouseMove);
+ fireEvent.mouseUp(document);
+
+ expect(onChange).not.toHaveBeenCalled();
+
+ unmount();
+
+ const onMove = jest.fn();
+ const { container: enabledContainer } = render(
+ ,
+ );
+
+ doMouseMove(enabledContainer, 20, 30, 'rc-slider-track');
+ fireEvent.mouseUp(document);
+ expect(onMove).toHaveBeenCalledWith([30, 70]);
+ });
+
+ it('keeps keyboard movement inside disabled handle boundaries', () => {
+ const onChange = jest.fn();
+ const { container, unmount } = render(
+ ,
+ );
+
+ repeatKeyDown(getHandle(container, 0), keyCode.RIGHT, 50);
+ expect(getLastChange(onChange)[0]).toBeLessThanOrEqual(50);
+
+ unmount();
+ onChange.mockClear();
+
+ const { container: boundaryContainer } = render(
+ ,
+ );
+ const middleHandle = getHandle(boundaryContainer, 1);
+
+ middleHandle.focus();
+ fireEvent.keyDown(middleHandle, { keyCode: keyCode.HOME });
+ expect(onChange).toHaveBeenCalledWith([20, 20, 80]);
+ expect(getHandle(boundaryContainer, 1)).toHaveFocus();
+
+ onChange.mockClear();
+ fireEvent.keyDown(getHandle(boundaryContainer, 1), { keyCode: keyCode.END });
+ expect(onChange).toHaveBeenCalledWith([20, 80, 80]);
+ });
+
+ it('respects pushable boundaries around disabled handles', () => {
+ const onChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ repeatKeyDown(getHandle(container, 0), keyCode.UP, 30);
+ expect(getLastChange(onChange)[0]).toBe(30);
+ expect(getLastChange(onChange)[1]).toBe(40);
+
+ onChange.mockClear();
+
+ repeatKeyDown(getHandle(container, 3), keyCode.LEFT, 50);
+ expect(getLastChange(onChange)[2]).toBe(50);
+ expect(getLastChange(onChange)[2] - getLastChange(onChange)[1]).toBe(10);
+ });
+
+ it('respects disabled boundaries with allowCross=false and step=null', () => {
+ const onChange = jest.fn();
+ const { container, unmount } = render(
+ ,
+ );
+
+ repeatKeyDown(getHandle(container, 0), keyCode.RIGHT, 50);
+ expect(getLastChange(onChange)[0]).toBeLessThanOrEqual(50);
+
+ unmount();
+ onChange.mockClear();
+
+ const { container: stepContainer } = render(
+ ,
+ );
+
+ fireEvent.keyDown(getHandle(stepContainer, 0), { keyCode: keyCode.RIGHT });
+ expect(getLastChange(onChange)[0]).toBeLessThanOrEqual(50);
+ });
+ });
});
diff --git a/tests/Slider.test.js b/tests/Slider.test.js
index c7b6f0376..290a30768 100644
--- a/tests/Slider.test.js
+++ b/tests/Slider.test.js
@@ -511,6 +511,18 @@ describe('Slider', () => {
expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(1);
});
+ it('should not change null value when disabled', () => {
+ const onChange = jest.fn();
+ const { container } = render( );
+
+ fireEvent.mouseDown(container.querySelector('.rc-slider'), {
+ clientX: 20,
+ });
+
+ expect(onChange).not.toHaveBeenCalled();
+ expect(container.getElementsByClassName('rc-slider-track')).toHaveLength(0);
+ });
+
describe('click slider to change value', () => {
it('ltr', () => {
const onChange = jest.fn();