Skip to content

Commit bedc645

Browse files
刘欢claude
andcommitted
feat: support disabled as array for range slider
- Add support for disabled={[boolean, boolean, ...]} to disable specific handles - Disabled handles cannot be dragged or moved via keyboard - Non-disabled handles cannot cross disabled handles - Clicking slider moves nearest non-disabled handle - In editable mode, disabled handles cannot be deleted - Track draggable is disabled when any handle is disabled - Backward compatible with boolean disabled Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0e4e031 commit bedc645

File tree

10 files changed

+368
-15
lines changed

10 files changed

+368
-15
lines changed

assets/index.less

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@
105105
cursor: -webkit-grabbing;
106106
cursor: grabbing;
107107
}
108+
109+
&-disabled {
110+
background-color: #fff;
111+
border-color: @disabledColor;
112+
box-shadow: none;
113+
cursor: not-allowed;
114+
115+
&:hover,
116+
&:active {
117+
border-color: @disabledColor;
118+
box-shadow: none;
119+
cursor: not-allowed;
120+
}
121+
}
108122
}
109123

110124
&-mark {

docs/demo/disabled-handle.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Disabled Handle
3+
title.zh-CN: 禁用特定滑块
4+
nav:
5+
title: Demo
6+
path: /demo
7+
---
8+
9+
<code src="../examples/disabled-handle.tsx"></code>

docs/examples/disabled-handle.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint react/no-multi-comp: 0, no-console: 0 */
2+
import Slider from '@rc-component/slider';
3+
import React, { useState } from 'react';
4+
import '../../assets/index.less';
5+
6+
const style: React.CSSProperties = {
7+
width: 400,
8+
margin: 50,
9+
};
10+
11+
const BasicDisabledHandle = () => {
12+
const [value, setValue] = useState([0, 30, 60, 100]);
13+
const [disabled, setDisabled] = useState([true, false, false, true]);
14+
15+
return (
16+
<div>
17+
<Slider range value={value} onChange={setValue} disabled={disabled} />
18+
<div style={{ marginTop: 16 }}>
19+
{value.map((val, index) => (
20+
<label key={index} style={{ marginRight: 16 }}>
21+
<input
22+
type="checkbox"
23+
checked={disabled[index]}
24+
onChange={() => {
25+
const newDisabled = [...disabled];
26+
newDisabled[index] = !newDisabled[index];
27+
setDisabled(newDisabled);
28+
}}
29+
/>
30+
Handle {index + 1} ({val}) {disabled[index] ? 'Disabled' : 'Enabled'}
31+
</label>
32+
))}
33+
</div>
34+
</div>
35+
);
36+
};
37+
38+
const DisabledHandleAsBoundary = () => {
39+
const [value, setValue] = useState([10, 50, 90]);
40+
41+
return (
42+
<div>
43+
<Slider range value={value} onChange={setValue} disabled={[false, true, false]} />
44+
<p style={{ marginTop: 8, color: '#999' }}>
45+
Middle handle (50) is disabled and acts as a boundary.
46+
First handle cannot go beyond 50, third handle cannot go below 50.
47+
Disabled handle has gray border and not-allowed cursor.
48+
</p>
49+
</div>
50+
);
51+
};
52+
53+
const MultipleDisabledBoundaries = () => {
54+
const [value, setValue] = useState([10, 30, 50, 70, 90]);
55+
56+
return (
57+
<div>
58+
<Slider range value={value} onChange={setValue} disabled={[true, false, true, false, true]} />
59+
<p style={{ marginTop: 8, color: '#999' }}>
60+
Handles at 10, 50, 90 are disabled.
61+
Handle at 30 can only move between 10-50, handle at 70 can only move between 50-90.
62+
</p>
63+
</div>
64+
);
65+
};
66+
67+
export default () => (
68+
<div>
69+
<div style={style}>
70+
<h3>Basic Disabled Handle</h3>
71+
<p>Toggle checkboxes to disable/enable specific handles</p>
72+
<BasicDisabledHandle />
73+
</div>
74+
75+
<div style={style}>
76+
<h3>Disabled Handle as Boundary</h3>
77+
<DisabledHandleAsBoundary />
78+
</div>
79+
80+
<div style={style}>
81+
<h3>Multiple Disabled Boundaries</h3>
82+
<MultipleDisabledBoundaries />
83+
</div>
84+
</div>
85+
);

src/Handles/Handle.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
5555
min,
5656
max,
5757
direction,
58-
disabled,
58+
disabled: globalDisabled,
5959
keyboard,
6060
range,
6161
tabIndex,
@@ -65,15 +65,21 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
6565
ariaValueTextFormatterForHandle,
6666
styles,
6767
classNames,
68+
isHandleDisabled,
6869
} = React.useContext(SliderContext);
6970

71+
const handleDisabled =
72+
globalDisabled || (isHandleDisabled ? isHandleDisabled(valueIndex) : false);
73+
7074
const handlePrefixCls = `${prefixCls}-handle`;
7175

7276
// ============================ Events ============================
7377
const onInternalStartMove = (e: React.MouseEvent | React.TouchEvent) => {
74-
if (!disabled) {
75-
onStartMove(e, valueIndex);
78+
if (handleDisabled) {
79+
e.stopPropagation();
80+
return;
7681
}
82+
onStartMove(e, valueIndex);
7783
};
7884

7985
const onInternalFocus = (e: React.FocusEvent<HTMLDivElement>) => {
@@ -86,7 +92,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
8692

8793
// =========================== Keyboard ===========================
8894
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
89-
if (!disabled && keyboard) {
95+
if (!handleDisabled && keyboard) {
9096
let offset: number | 'min' | 'max' = null;
9197

9298
// Change the value
@@ -161,12 +167,12 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
161167

162168
if (valueIndex !== null) {
163169
divProps = {
164-
tabIndex: disabled ? null : getIndex(tabIndex, valueIndex),
170+
tabIndex: handleDisabled ? null : getIndex(tabIndex, valueIndex),
165171
role: 'slider',
166172
'aria-valuemin': min,
167173
'aria-valuemax': max,
168174
'aria-valuenow': value,
169-
'aria-disabled': disabled,
175+
'aria-disabled': handleDisabled,
170176
'aria-label': getIndex(ariaLabelForHandle, valueIndex),
171177
'aria-labelledby': getIndex(ariaLabelledByForHandle, valueIndex),
172178
'aria-required': getIndex(ariaRequired, valueIndex),
@@ -190,6 +196,7 @@ const Handle = React.forwardRef<HTMLDivElement, HandleProps>((props, ref) => {
190196
[`${handlePrefixCls}-${valueIndex + 1}`]: valueIndex !== null && range,
191197
[`${handlePrefixCls}-dragging`]: dragging,
192198
[`${handlePrefixCls}-dragging-delete`]: draggingDelete,
199+
[`${handlePrefixCls}-disabled`]: handleDisabled,
193200
},
194201
classNames.handle,
195202
)}

src/Slider.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface SliderProps<ValueType = number | number[]> {
5757
id?: string;
5858

5959
// Status
60-
disabled?: boolean;
60+
disabled?: boolean | boolean[];
6161
keyboard?: boolean;
6262
autoFocus?: boolean;
6363
onFocus?: (e: React.FocusEvent<HTMLDivElement>) => void;
@@ -131,8 +131,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
131131

132132
id,
133133

134-
// Status
135-
disabled = false,
134+
disabled: rawDisabled = false,
136135
keyboard = true,
137136
autoFocus,
138137
onFocus,
@@ -188,6 +187,24 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
188187
const handlesRef = React.useRef<HandlesRef>(null);
189188
const containerRef = React.useRef<HTMLDivElement>(null);
190189

190+
// ============================ Disabled ============================
191+
const disabled = React.useMemo(() => {
192+
if (typeof rawDisabled === 'boolean') {
193+
return rawDisabled;
194+
}
195+
return rawDisabled.every((d) => d);
196+
}, [rawDisabled]);
197+
198+
const isHandleDisabled = React.useCallback(
199+
(index: number) => {
200+
if (typeof rawDisabled === 'boolean') {
201+
return rawDisabled;
202+
}
203+
return rawDisabled[index] || false;
204+
},
205+
[rawDisabled],
206+
);
207+
191208
const direction = React.useMemo<Direction>(() => {
192209
if (vertical) {
193210
return reverse ? 'ttb' : 'btt';
@@ -247,6 +264,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
247264
markList,
248265
allowCross,
249266
mergedPush,
267+
isHandleDisabled,
250268
);
251269

252270
// ============================ Values ============================
@@ -321,7 +339,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
321339
});
322340

323341
const onDelete = (index: number) => {
324-
if (disabled || !rangeEditable || rawValues.length <= minCount) {
342+
if (disabled || !rangeEditable || rawValues.length <= minCount || isHandleDisabled(index)) {
325343
return;
326344
}
327345

@@ -348,6 +366,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
348366
offsetValues,
349367
rangeEditable,
350368
minCount,
369+
isHandleDisabled,
351370
);
352371

353372
/**
@@ -378,10 +397,39 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
378397
let focusIndex = valueIndex;
379398

380399
if (rangeEditable && valueDist !== 0 && (!maxCount || rawValues.length < maxCount)) {
400+
const leftDisabled = isHandleDisabled(valueBeforeIndex);
401+
const rightDisabled = isHandleDisabled(valueBeforeIndex + 1);
402+
403+
if (leftDisabled && rightDisabled) {
404+
return;
405+
}
406+
381407
cloneNextValues.splice(valueBeforeIndex + 1, 0, newValue);
382408
focusIndex = valueBeforeIndex + 1;
383409
} else {
410+
if (isHandleDisabled(valueIndex)) {
411+
let nearestIndex = -1;
412+
let nearestDist = mergedMax - mergedMin;
413+
414+
rawValues.forEach((val, index) => {
415+
if (!isHandleDisabled(index)) {
416+
const dist = Math.abs(newValue - val);
417+
if (dist < nearestDist) {
418+
nearestDist = dist;
419+
nearestIndex = index;
420+
}
421+
}
422+
});
423+
424+
// If all handles are disabled, do nothing
425+
if (nearestIndex === -1) {
426+
return;
427+
}
428+
429+
valueIndex = nearestIndex;
430+
}
384431
cloneNextValues[valueIndex] = newValue;
432+
focusIndex = valueIndex;
385433
}
386434

387435
// Fill value to match default 2 (only when `rawValues` is empty)
@@ -443,7 +491,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
443491
const [keyboardValue, setKeyboardValue] = React.useState<number>(null);
444492

445493
const onHandleOffsetChange = (offset: number | 'min' | 'max', valueIndex: number) => {
446-
if (!disabled) {
494+
if (!disabled && !isHandleDisabled(valueIndex)) {
447495
const next = offsetValues(rawValues, offset, valueIndex);
448496

449497
onBeforeChange?.(getTriggerValue(rawValues));
@@ -546,6 +594,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
546594
ariaValueTextFormatterForHandle,
547595
styles: styles || {},
548596
classNames: classNames || {},
597+
isHandleDisabled,
549598
}),
550599
[
551600
mergedMin,
@@ -565,6 +614,7 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
565614
ariaValueTextFormatterForHandle,
566615
styles,
567616
classNames,
617+
isHandleDisabled,
568618
],
569619
);
570620

src/Tracks/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ export interface TrackProps {
1414
}
1515

1616
const Tracks: React.FC<TrackProps> = (props) => {
17-
const { prefixCls, style, values, startPoint, onStartMove } = props;
18-
const { included, range, min, styles, classNames } = React.useContext(SliderContext);
17+
const { prefixCls, style, values, startPoint, onStartMove: propsOnStartMove } = props;
18+
const { included, range, min, styles, classNames, isHandleDisabled } = React.useContext(SliderContext);
19+
20+
const hasDisabledHandle = React.useMemo(() => {
21+
if (!isHandleDisabled) return false;
22+
for (let i = 0; i < values.length; i++) {
23+
if (isHandleDisabled(i)) return true;
24+
}
25+
return false;
26+
}, [isHandleDisabled, values.length]);
27+
28+
const onStartMove = hasDisabledHandle ? undefined : propsOnStartMove;
1929

2030
// =========================== List ===========================
2131
const trackList = React.useMemo(() => {

src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface SliderContextProps {
1919
ariaValueTextFormatterForHandle?: AriaValueFormat | AriaValueFormat[];
2020
classNames: SliderClassNames;
2121
styles: SliderStyles;
22+
isHandleDisabled?: (index: number) => boolean;
2223
}
2324

2425
const SliderContext = React.createContext<SliderContextProps>({

src/hooks/useDrag.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function useDrag(
2626
offsetValues: OffsetValues,
2727
editable: boolean,
2828
minCount: number,
29+
isHandleDisabled?: (index: number) => boolean,
2930
): [
3031
draggingIndex: number,
3132
draggingValue: number,
@@ -91,6 +92,10 @@ function useDrag(
9192
(valueIndex: number, offsetPercent: number, deleteMark: boolean) => {
9293
if (valueIndex === -1) {
9394
// >>>> Dragging on the track
95+
if (isHandleDisabled && originValues.some((_, index) => isHandleDisabled(index))) {
96+
return;
97+
}
98+
9499
const startValue = originValues[0];
95100
const endValue = originValues[originValues.length - 1];
96101
const maxStartOffset = min - startValue;
@@ -126,6 +131,10 @@ function useDrag(
126131

127132
// 如果是点击 track 触发的,需要传入变化后的初始值,而不能直接用 rawValues
128133
const initialValues = startValues || rawValues;
134+
if (isHandleDisabled && isHandleDisabled(valueIndex)) {
135+
return;
136+
}
137+
129138
const originValue = initialValues[valueIndex];
130139

131140
setDraggingIndex(valueIndex);

0 commit comments

Comments
 (0)