Skip to content

Commit 204809d

Browse files
author
刘欢
committed
feat: implement boundary constraints for disabled slider handles
1 parent 15e473f commit 204809d

2 files changed

Lines changed: 83 additions & 141 deletions

File tree

src/Slider.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,25 @@ const Slider = React.forwardRef<SliderRef, SliderProps<number | number[]>>((prop
455455

456456
valueIndex = nearestIndex;
457457
}
458-
cloneNextValues[valueIndex] = newValue;
458+
459+
// Apply boundary constraints from disabled handles
460+
let minBound = mergedMin;
461+
let maxBound = mergedMax;
462+
463+
for (let i = valueIndex - 1; i >= 0; i -= 1) {
464+
if (isHandleDisabled(i)) {
465+
minBound = rawValues[i];
466+
break;
467+
}
468+
}
469+
for (let i = valueIndex + 1; i < rawValues.length; i += 1) {
470+
if (isHandleDisabled(i)) {
471+
maxBound = rawValues[i];
472+
break;
473+
}
474+
}
475+
476+
cloneNextValues[valueIndex] = Math.max(minBound, Math.min(maxBound, newValue));
459477
focusIndex = valueIndex;
460478
}
461479

tests/Range.test.tsx

Lines changed: 64 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -843,9 +843,9 @@ describe('Range', () => {
843843
});
844844

845845
describe('disabled as array', () => {
846-
it('basic', () => {
846+
it('basic functionality with boolean and array', () => {
847847
const onChange = jest.fn();
848-
const { container } = render(
848+
const { container, rerender } = render(
849849
<Slider range defaultValue={[0, 50, 100]} disabled={[true, false, true]} onChange={onChange} />,
850850
);
851851

@@ -864,31 +864,30 @@ describe('Range', () => {
864864
// Keyboard: enabled handle should respond
865865
fireEvent.keyDown(container.getElementsByClassName('rc-slider-handle')[1], { keyCode: keyCode.RIGHT });
866866
expect(onChange).toHaveBeenCalledWith([0, 51, 100]);
867-
});
868867

869-
it('drag disabled handle', () => {
870-
const onChange = jest.fn();
871-
const { container } = render(
872-
<Slider range defaultValue={[20, 50]} disabled={[true, false]} onChange={onChange} />,
873-
);
874-
875-
// Try to drag disabled first handle
876-
doMouseMove(container, 20, 80, 'rc-slider-handle');
877-
expect(onChange).not.toHaveBeenCalled();
868+
// Boolean disabled backward compatibility
869+
rerender(<Slider range defaultValue={[20, 50]} disabled={true} onChange={onChange} />);
870+
expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
871+
doMouseDown(container, 30, 'rc-slider', true);
872+
expect(onChange).toHaveBeenCalledTimes(1); // Still 1, not triggered
878873
});
879874

880-
it('click slider to move nearest enabled handle', () => {
875+
it('drag and click respect disabled state', () => {
881876
const onChange = jest.fn();
882877
const { container } = render(
883878
<Slider range defaultValue={[0, 50, 100]} disabled={[true, false, true]} onChange={onChange} />,
884879
);
885880

886-
// Click near disabled handle at 0, should move enabled handle at 50
881+
// Drag disabled handle - no change
882+
doMouseMove(container, 0, 80, 'rc-slider-handle');
883+
expect(onChange).not.toHaveBeenCalled();
884+
885+
// Click near disabled handle - moves nearest enabled handle
887886
doMouseDown(container, 10, 'rc-slider', true);
888887
expect(onChange).toHaveBeenCalledWith([0, 10, 100]);
889888
});
890889

891-
it('cannot cross disabled handle', () => {
890+
it('cannot cross disabled handle boundary', () => {
892891
const onChange = jest.fn();
893892
const { container } = render(
894893
<Slider range defaultValue={[20, 50, 80]} disabled={[false, true, false]} onChange={onChange} />,
@@ -902,59 +901,81 @@ describe('Range', () => {
902901
expect(lastCall[0][0]).toBeLessThanOrEqual(50);
903902
});
904903

905-
it('editable: cannot delete disabled handle', () => {
904+
it('editable mode with disabled handles', () => {
906905
const onChange = jest.fn();
906+
const onDisabledChange = jest.fn();
907907
const { container } = render(
908-
<Slider range={{ editable: true }} defaultValue={[20, 50, 80]} disabled={[false, true, false]} onChange={onChange} />,
908+
<Slider
909+
range={{ editable: true }}
910+
value={[0, 50, 100]}
911+
disabled={[false, true, false]}
912+
onChange={onChange}
913+
onDisabledChange={onDisabledChange}
914+
/>,
909915
);
910916

911-
// Try to delete disabled middle handle
917+
// Cannot delete disabled handle
912918
const handle = container.getElementsByClassName('rc-slider-handle')[1];
913919
fireEvent.mouseEnter(handle);
914920
fireEvent.keyDown(handle, { keyCode: keyCode.DELETE });
915921
expect(onChange).not.toHaveBeenCalled();
916922

917-
// Try to drag out disabled handle
923+
// Cannot drag out disabled handle
918924
doMouseMove(container, 50, 1000, 'rc-slider-handle', 1);
919925
expect(onChange).not.toHaveBeenCalled();
926+
927+
// onDisabledChange called when removing enabled handle
928+
doMouseMove(container, 0, 1000);
929+
expect(onChange).toHaveBeenCalledWith([50, 100]);
930+
expect(onDisabledChange).toHaveBeenCalledWith([true, false]);
920931
});
921932

922-
it('backward compatible with boolean', () => {
933+
it('editable: add handle respects disabled boundaries', () => {
923934
const onChange = jest.fn();
924-
const { container } = render(
925-
<Slider range defaultValue={[20, 50]} disabled={true} onChange={onChange} />,
935+
const onDisabledChange = jest.fn();
936+
const { container, rerender } = render(
937+
<Slider
938+
range={{ editable: true }}
939+
value={[20, 60]}
940+
disabled={[true, true]}
941+
onChange={onChange}
942+
onDisabledChange={onDisabledChange}
943+
/>,
926944
);
927945

928-
expect(container.getElementsByClassName('rc-slider-handle')[0]).not.toHaveAttribute('tabIndex');
929-
doMouseDown(container, 30, 'rc-slider', true);
946+
// Cannot add between two disabled handles
947+
doMouseDown(container, 40, 'rc-slider', true);
930948
expect(onChange).not.toHaveBeenCalled();
931-
});
932949

933-
it('editable: cannot add handle between two disabled handles', () => {
934-
const onChange = jest.fn();
935-
const { container } = render(
936-
<Slider range={{ editable: true }} defaultValue={[20, 50, 80]} disabled={[true, true, false]} onChange={onChange} />,
950+
// Can add when only one side is disabled
951+
rerender(
952+
<Slider
953+
range={{ editable: true }}
954+
value={[0, 100]}
955+
disabled={[true, false]}
956+
onChange={onChange}
957+
onDisabledChange={onDisabledChange}
958+
/>,
937959
);
938-
939-
// Click between 20 and 50, both are disabled
940-
doMouseDown(container, 35, 'rc-slider', true);
941-
expect(onChange).not.toHaveBeenCalled();
960+
doMouseDown(container, 50, 'rc-slider', true);
961+
expect(onChange).toHaveBeenCalledWith([0, 50, 100]);
962+
expect(onDisabledChange).toHaveBeenCalledWith([true, false, false]);
942963
});
943964

944-
it('all handles disabled: click does nothing', () => {
965+
it('all handles disabled prevents interaction', () => {
945966
const onChange = jest.fn();
946967
const { container } = render(
947-
<Slider range defaultValue={[20, 50]} disabled={[true, true]} onChange={onChange} />,
968+
<Slider range defaultValue={[0, 50, 100]} disabled={[true, true, true]} onChange={onChange} />,
948969
);
949970

971+
// Click does nothing (covers findNearestEnabled returning -1)
950972
const rail = container.querySelector('.rc-slider-rail');
951973
const mouseDown = createEvent.mouseDown(rail);
952974
Object.defineProperties(mouseDown, {
953-
clientX: { get: () => 30 },
954-
clientY: { get: () => 30 },
975+
clientX: { get: () => 10 },
976+
clientY: { get: () => 10 },
955977
});
956978
fireEvent(rail, mouseDown);
957-
958979
expect(onChange).not.toHaveBeenCalled();
959980
});
960981

@@ -964,7 +985,6 @@ describe('Range', () => {
964985
<Slider range={{ draggableTrack: true }} defaultValue={[0, 50]} disabled={[false, true]} onChange={onChange} />,
965986
);
966987

967-
// Try to drag track - should not work because one handle is disabled
968988
const track = container.getElementsByClassName('rc-slider-track')[0];
969989
const mouseDown = createEvent.mouseDown(track);
970990
Object.defineProperties(mouseDown, {
@@ -973,7 +993,6 @@ describe('Range', () => {
973993
});
974994
fireEvent(track, mouseDown);
975995

976-
// Drag
977996
const mouseMove = createEvent.mouseMove(document);
978997
(mouseMove as any).pageX = 20;
979998
(mouseMove as any).pageY = 20;
@@ -982,114 +1001,19 @@ describe('Range', () => {
9821001
expect(onChange).not.toHaveBeenCalled();
9831002
});
9841003

985-
it('all handles disabled: find nearest enabled returns -1', () => {
986-
// This test specifically covers line 426 in Slider.tsx
987-
// When all handles are disabled and clicking near one,
988-
// the nearestIndex search returns -1 and returns early
1004+
it('click to move respects disabled boundary', () => {
9891005
const onChange = jest.fn();
990-
const { container } = render(
991-
<Slider range defaultValue={[0, 50, 100]} disabled={[true, true, true]} onChange={onChange} />,
992-
);
993-
994-
// Click at position 10 (near first disabled handle)
995-
const rail = container.querySelector('.rc-slider-rail');
996-
const mouseDown = createEvent.mouseDown(rail);
997-
Object.defineProperties(mouseDown, {
998-
clientX: { get: () => 10 },
999-
clientY: { get: () => 10 },
1000-
});
1001-
fireEvent(rail, mouseDown);
1002-
1003-
// Should not trigger onChange because all handles are disabled
1004-
expect(onChange).not.toHaveBeenCalled();
1005-
});
1006-
1007-
it('editable: onDisabledChange called when adding handle', () => {
1008-
const onChange = jest.fn();
1009-
const onDisabledChange = jest.fn();
1010-
const { container } = render(
1011-
<Slider
1012-
range={{ editable: true }}
1013-
value={[0, 100]}
1014-
disabled={[true, false]}
1015-
onChange={onChange}
1016-
onDisabledChange={onDisabledChange}
1017-
/>,
1018-
);
1019-
1020-
// Click to add a handle between 0 and 100
1021-
doMouseDown(container, 50, 'rc-slider', true);
1022-
1023-
expect(onChange).toHaveBeenCalledWith([0, 50, 100]);
1024-
expect(onDisabledChange).toHaveBeenCalledWith([true, false, false]);
1025-
});
1026-
1027-
it('editable: onDisabledChange called when removing handle', () => {
1028-
const onChange = jest.fn();
1029-
const onDisabledChange = jest.fn();
1030-
const { container } = render(
1031-
<Slider
1032-
range={{ editable: true }}
1033-
value={[0, 50, 100]}
1034-
disabled={[false, true, false]}
1035-
onChange={onChange}
1036-
onDisabledChange={onDisabledChange}
1037-
/>,
1038-
);
1039-
1040-
// Drag first handle (enabled) out to delete it
1041-
doMouseMove(container, 0, 1000);
1042-
1043-
expect(onChange).toHaveBeenCalledWith([50, 100]);
1044-
expect(onDisabledChange).toHaveBeenCalledWith([true, false]);
1045-
});
1046-
1047-
it('editable: disabled array stays in sync when adding between disabled handles', () => {
1048-
const onChange = jest.fn();
1049-
const onDisabledChange = jest.fn();
1050-
const { container } = render(
1051-
<Slider
1052-
range={{ editable: true }}
1053-
value={[20, 60]}
1054-
disabled={[true, true]}
1055-
onChange={onChange}
1056-
onDisabledChange={onDisabledChange}
1057-
/>,
1058-
);
1059-
1060-
// Click to add a handle between 20 and 60
1061-
doMouseDown(container, 40, 'rc-slider', true);
1062-
1063-
// Should not trigger onChange because both surrounding handles are disabled
1064-
expect(onChange).not.toHaveBeenCalled();
1065-
expect(onDisabledChange).not.toHaveBeenCalled();
1066-
});
1067-
1068-
it('click to move cannot cross disabled handle boundary', () => {
1069-
const onChange = jest.fn();
1070-
const { container } = render(
1006+
const { container, rerender } = render(
10711007
<Slider range value={[20, 50, 80]} disabled={[true, false, false]} onChange={onChange} />,
10721008
);
10731009

1074-
// Click at position 10, which is left of disabled handle at 20
1075-
// Nearest enabled handle is 50, but it cannot cross below 20
1010+
// Click left of disabled handle - clamped to boundary
10761011
doMouseDown(container, 10, 'rc-slider', true);
1077-
1078-
// Should move handle to 20 (boundary), not 10
10791012
expect(onChange).toHaveBeenCalledWith([20, 20, 80]);
1080-
});
1081-
1082-
it('click to move cannot cross disabled handle on right side', () => {
1083-
const onChange = jest.fn();
1084-
const { container } = render(
1085-
<Slider range value={[20, 50, 80]} disabled={[false, false, true]} onChange={onChange} />,
1086-
);
10871013

1088-
// Click at position 90, which is right of disabled handle at 80
1089-
// Nearest enabled handle is 50, but it cannot cross above 80
1014+
// Click right of disabled handle - clamped to boundary
1015+
rerender(<Slider range value={[20, 50, 80]} disabled={[false, false, true]} onChange={onChange} />);
10901016
doMouseDown(container, 90, 'rc-slider', true);
1091-
1092-
// Should move handle to 80 (boundary), not 90
10931017
expect(onChange).toHaveBeenCalledWith([20, 80, 80]);
10941018
});
10951019
});

0 commit comments

Comments
 (0)