Skip to content

feat: Range 组件支持 disabled 数组形式#1068

Open
EmilyyyLiu wants to merge 4 commits intoreact-component:masterfrom
EmilyyyLiu:feat/disabled-handle-array
Open

feat: Range 组件支持 disabled 数组形式#1068
EmilyyyLiu wants to merge 4 commits intoreact-component:masterfrom
EmilyyyLiu:feat/disabled-handle-array

Conversation

@EmilyyyLiu
Copy link
Copy Markdown
Contributor

@EmilyyyLiu EmilyyyLiu commented Apr 14, 2026

功能概述

Range 组件支持 disabled 属性传入数组,用于单独禁用特定滑块手柄。

关联需求:ant-design/ant-design#10143

API 变更

// 禁用第一个和第三个手柄,第二个保持可用
<Slider range disabled={[true, false, true]} />

功能特性

  • 支持 disabled={[boolean, boolean, ...]} 数组形式
  • 被禁用的手柄无法拖拽
  • 被禁用的手柄无法通过键盘移动
  • 启用的手柄无法跨越被禁用的手柄(禁用手柄作为边界)
  • 点击滑块时自动移动最近的非禁用手柄
  • editable 模式下无法删除被禁用的手柄
  • 当存在禁用手柄时,轨道拖拽功能禁用
  • 完全兼容布尔值 disabled 的原有用法

测试覆盖

  • 新增 8 个测试用例,覆盖所有功能场景
  • 代码覆盖率 99%+
  • 全部 120 个测试通过

变更文件

  • src/Slider.tsx - 核心逻辑
  • src/Handles/Handle.tsx - 手柄禁用状态
  • src/Tracks/index.tsx - 轨道拖拽限制
  • src/hooks/useDrag.ts - 拖拽逻辑
  • src/hooks/useOffset.ts - 位置计算边界
  • src/context.ts - 上下文类型
  • tests/Range.test.tsx - 测试用例
  • docs/examples/disabled-handle.tsx - 示例
  • docs/demo/disabled-handle.md - 文档
  • assets/index.less - 禁用样式

检查清单

  • 功能测试已添加
  • 示例代码已添加
  • 向后兼容

Summary by CodeRabbit

发布说明

  • 新功能

    • 支持按索引独立禁用单个滑杆句柄:disabled 可为 boolean 或 boolean[],可全局或按句柄数组禁用;禁用句柄将阻止拖拽、键盘、删除等交互并更新聚焦与无障碍属性。
  • 文档

    • 新增示例与演示页面,展示单个、边界、可编辑与全禁用等场景及交互说明。
  • 样式

    • 为禁用句柄添加专属视觉与交互样式(白色背景、禁用边框、移除阴影、不可点击光标,悬停/激活状态保持一致)。
  • 测试

    • 增补覆盖禁用数组、轨道拖拽、编辑/删除、键盘交互与点击轨道时跳过禁用句柄等单元测试。

- 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>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

Someone is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

此变更为滑块组件引入按手柄禁用能力:扩展 disabledboolean | boolean[],注入 isHandleDisabled(index) 到上下文,更新 Handle/Tracks、相关 hooks、样式、示例与测试以支持手柄级禁用和相应交互约束。

Changes

Cohort / File(s) Summary
样式与文档
assets/index.less, docs/demo/disabled-handle.md, docs/examples/disabled-handle.tsx
新增手柄禁用样式(.rc-slider-handle-disabled)及示例/文档页面,展示如何按手柄禁用并包含示例代码片段。
核心滑块逻辑
src/Slider.tsx
disabled 类型扩展为 `boolean
手柄与轨道组件
src/Handles/Handle.tsx, src/Tracks/index.tsx
Handle 计算 per-handle 禁用状态并据此调整 aria-disabled/tabIndex/class 与交互早退;Tracks 在任一手柄被禁用时阻止向子 Track 传递开始移动回调(禁用轨道拖动启动)。
上下文与 Hooks
src/context.ts, src/hooks/useDrag.ts, src/hooks/useOffset.ts
Context 增加 isHandleDisabled?: (index)=>booleanuseDraguseOffset 签名扩展以接收该谓词,并在拖动/偏移计算中将禁用手柄作为早退或边界条件。
测试覆盖
tests/Range.test.tsx
测试助手改为按索引定位元素,新增“disabled as array” 测试套件,覆盖键盘、鼠标、轨道点击、跨越限制、编辑/删除约束、全部禁用与向后兼容场景。

Sequence Diagram

sequenceDiagram
    actor User
    participant Slider
    participant Context as SliderContext
    participant Handle
    participant Hooks as useDrag/useOffset
    participant Tracks

    User->>Slider: 设置/更新 disabled(boolean|boolean[])
    Slider->>Slider: 归一化并计算 isHandleDisabled(index)
    Slider->>Context: 注入 isHandleDisabled
    Slider->>Tracks: 传递 isHandleDisabled

    User->>Handle: 交互(点击/拖动/键盘)
    Handle->>Context: 读取 isHandleDisabled(index)
    alt 手柄已禁用
        Handle->>User: 标记为 aria-disabled/tabIndex/class 并拒绝交互(早退)
    else 手柄可用
        Handle->>Hooks: 发起拖动/偏移请求(包含 index)
        Hooks->>Context: 查询 isHandleDisabled 以计算边界/允许性
        Hooks->>Slider: 提交值变更
        Slider->>User: 更新 UI/值
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • zombieJ

Poem

🐰 我是代码里的小兔儿,
越不过那被禁的手柄,
边界明晰步步稳,
拖拽点击我能辨,
新样式闪亮心欢腾。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清楚地总结了 PR 的核心变更:为 Range 组件添加了对 disabled 数组形式的支持,这是整个 PR 的主要功能。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ability to disable specific handles in a range slider by passing an array of booleans to the disabled prop. The changes include styling for disabled handles, event suppression for keyboard and mouse interactions, and logic to prevent enabled handles from crossing disabled ones. The review feedback highlights a few edge cases: the click-to-move logic needs to respect disabled handle boundaries, the pushable distance should be accounted for when calculating boundaries against disabled handles, and the global disabled state derivation should be refined to avoid incorrectly applying disabled styles to the entire component when only specific handles are restricted.

Comment on lines +201 to +214
if (isHandleDisabled) {
for (let i = valueIndex - 1; i >= 0; i -= 1) {
if (isHandleDisabled(i)) {
minBound = nextValues[i];
break;
}
}
for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
if (isHandleDisabled(i)) {
maxBound = nextValues[i];
break;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The boundary calculation for the active handle should account for the pushable distance when neighboring handles are disabled. If a neighbor is disabled, the current handle must stop at a distance that respects the pushable requirement, otherwise the subsequent push logic will attempt to move the disabled handle.

Suggested change
if (isHandleDisabled) {
for (let i = valueIndex - 1; i >= 0; i -= 1) {
if (isHandleDisabled(i)) {
minBound = nextValues[i];
break;
}
}
for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
if (isHandleDisabled(i)) {
maxBound = nextValues[i];
break;
}
}
}
if (isHandleDisabled) {
const pushNum = typeof pushable === 'number' ? pushable : 0;
for (let i = valueIndex - 1; i >= 0; i -= 1) {
if (isHandleDisabled(i)) {
minBound = nextValues[i] + (valueIndex - i) * pushNum;
break;
}
}
for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
if (isHandleDisabled(i)) {
maxBound = nextValues[i] - (i - valueIndex) * pushNum;
break;
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果被禁用手柄,那不应该能被被移动吧

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

它说的是 pushable,指的是拉其他的把被禁用的被动推走。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不会的,我拉过,没有变化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 96.82540% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.15%. Comparing base (0e4e031) to head (218f061).

Files with missing lines Patch % Lines
src/hooks/useDrag.ts 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1068      +/-   ##
==========================================
- Coverage   99.39%   99.15%   -0.24%     
==========================================
  Files          14       14              
  Lines         661      713      +52     
  Branches      191      218      +27     
==========================================
+ Hits          657      707      +50     
- Misses          4        6       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from 7b60ace to a7db356 Compare April 14, 2026 03:23
@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from 341c634 to 4f28cc4 Compare April 14, 2026 03:40
@EmilyyyLiu EmilyyyLiu marked this pull request as ready for review April 14, 2026 05:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Slider.tsx (1)

341-355: ⚠️ Potential issue | 🟠 Major

editable 模式下 disabled[] 会在增删后错位。

尤其是 uncontrolled 用法下,这里只对 rawValues 做了 splicedisabled 仍按原始 index 解释。比如 [false, true, false] 删除第 0 个 handle 后,原来禁用的中间 handle 会变成 index 0 并重新可操作;插入新 handle 也会把后续禁用位整体右移。现在只拦住了“当前 index 上的 disabled handle”,但没保证“同一个 handle 持续 disabled”。建议为 editable 模式维护和 values 同步的 handle 元数据,或者明确限制这组组合必须由外部受控并同步更新。

Also applies to: 399-408

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 341 - 355, The onDelete handler (and
corresponding insert logic around the insert function at lines ~399-408) mutates
rawValues but leaves the parallel disabled[] array indexed to original handles,
causing disabled flags to shift; update the implementation so editable mode
keeps per-handle metadata in sync with values—either splice the disabled array
the same way you splice rawValues (e.g., cloneDisabled.splice(index,1) on delete
and cloneDisabled.splice(index,0,newFlag) on insert) and pass the updated
disabled array/state through the same change flow, or introduce a single array
of handle metadata (e.g., handleMeta[]) that is kept in lockstep with rawValues
and is updated in onDelete, onInsert, and wherever values change; ensure
isHandleDisabled and any callers read from this synchronized source rather than
the original static disabled[] prop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Slider.tsx`:
- Around line 191-206: The aggregated disabled computation is inconsistent with
per-handle checks: update the computed "disabled" (currently using
rawDisabled.every) to consider the current number of handles (use
rawValues.length) so it only returns true when rawDisabled is an array of length
equal to rawValues.length and every entry is truthy; keep isHandleDisabled
(function name) as-is but ensure it still falls back to false for missing
indices. In short, change the logic for disabled to: if rawDisabled is boolean
return it; if it's an array, return rawDisabled.length === rawValues.length &&
rawDisabled.every(Boolean); leave isHandleDisabled(index) unchanged.

In `@tests/Range.test.tsx`:
- Around line 985-1005: The test's claim that it covers the nearestIndex === -1
path is incorrect because with disabled={[true,true,true]} the
changeToCloseValue early-returns at if (!disabled), so the nearestIndex branch
is never reached; either remove or update the misleading comment/description in
the it(...) block, or replace the test with one that actually executes the
nearestIndex === -1 branch by ensuring changeToCloseValue runs (e.g., make at
least one handle enabled and construct a click position that yields nearestIndex
=== -1 so the code path in Slider.tsx referencing nearestIndex is exercised);
refer to the test case name in Range.test.tsx and the changeToCloseValue /
nearestIndex symbols to locate where to change the test and its assertion.
- Around line 961-983: The test for "draggableTrack disabled when any handle is
disabled" is likely a false positive because it builds a custom
mouseDown/mouseMove sequence that doesn't match the track drag startup used
elsewhere; reuse the existing doMouseMove helper so the test exercises the same
drag path and ensure the mouseDown event includes pageX/pageY getters and a
preventDefault implementation (and set pageX/pageY on the mouseMove) so the
draggableTrack logic is triggered; update the spec to call doMouseMove (or
mirror its behavior) when interacting with the rc-slider-track and assert
onChange remains uncalled when one handle is disabled.

---

Outside diff comments:
In `@src/Slider.tsx`:
- Around line 341-355: The onDelete handler (and corresponding insert logic
around the insert function at lines ~399-408) mutates rawValues but leaves the
parallel disabled[] array indexed to original handles, causing disabled flags to
shift; update the implementation so editable mode keeps per-handle metadata in
sync with values—either splice the disabled array the same way you splice
rawValues (e.g., cloneDisabled.splice(index,1) on delete and
cloneDisabled.splice(index,0,newFlag) on insert) and pass the updated disabled
array/state through the same change flow, or introduce a single array of handle
metadata (e.g., handleMeta[]) that is kept in lockstep with rawValues and is
updated in onDelete, onInsert, and wherever values change; ensure
isHandleDisabled and any callers read from this synchronized source rather than
the original static disabled[] prop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 462e9270-165c-464f-a6ac-41f3c062772f

📥 Commits

Reviewing files that changed from the base of the PR and between 0e4e031 and 4f28cc4.

📒 Files selected for processing (10)
  • assets/index.less
  • docs/demo/disabled-handle.md
  • docs/examples/disabled-handle.tsx
  • src/Handles/Handle.tsx
  • src/Slider.tsx
  • src/Tracks/index.tsx
  • src/context.ts
  • src/hooks/useDrag.ts
  • src/hooks/useOffset.ts
  • tests/Range.test.tsx

Comment on lines +961 to +983
it('draggableTrack disabled when any handle is disabled', () => {
const onChange = jest.fn();
const { container } = render(
<Slider range={{ draggableTrack: true }} defaultValue={[0, 50]} disabled={[false, true]} onChange={onChange} />,
);

// Try to drag track - should not work because one handle is disabled
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);

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

expect(onChange).not.toHaveBeenCalled();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

这个拖拽用例现在可能是“假通过”。

这里没有复用 doMouseMovemouseDown 也没补 pageX/pageYpreventDefault,很可能根本没走到现有 track drag 用例相同的启动路径。这样即使 track 仍然可拖,onChange 也可能不会触发,导致用例误报通过。建议直接复用同一个 helper。

可参考的最小改法
-      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);
-
-      // Drag
-      const mouseMove = createEvent.mouseMove(document);
-      (mouseMove as any).pageX = 20;
-      (mouseMove as any).pageY = 20;
-      fireEvent(document, mouseMove);
+      doMouseMove(container, 0, 20, 'rc-slider-track');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Range.test.tsx` around lines 961 - 983, The test for "draggableTrack
disabled when any handle is disabled" is likely a false positive because it
builds a custom mouseDown/mouseMove sequence that doesn't match the track drag
startup used elsewhere; reuse the existing doMouseMove helper so the test
exercises the same drag path and ensure the mouseDown event includes pageX/pageY
getters and a preventDefault implementation (and set pageX/pageY on the
mouseMove) so the draggableTrack logic is triggered; update the spec to call
doMouseMove (or mirror its behavior) when interacting with the rc-slider-track
and assert onChange remains uncalled when one handle is disabled.

Comment on lines +985 to +1005
it('all handles disabled: find nearest enabled returns -1', () => {
// This test specifically covers line 426 in Slider.tsx
// When all handles are disabled and clicking near one,
// the nearestIndex search returns -1 and returns early
const onChange = jest.fn();
const { container } = render(
<Slider range defaultValue={[0, 50, 100]} disabled={[true, true, true]} onChange={onChange} />,
);

// Click at position 10 (near first disabled handle)
const rail = container.querySelector('.rc-slider-rail');
const mouseDown = createEvent.mouseDown(rail);
Object.defineProperties(mouseDown, {
clientX: { get: () => 10 },
clientY: { get: () => 10 },
});
fireEvent(rail, mouseDown);

// Should not trigger onChange because all handles are disabled
expect(onChange).not.toHaveBeenCalled();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

这条“覆盖 nearestIndex === -1”的说明不成立。

disabled={[true, true, true]} 时,src/Slider.tsxchangeToCloseValue 入口的 if (!disabled) 处就已经返回,根本到不了后面的 nearestIndex === -1。所以这条用例实际上只是再次验证“全部 disabled 时点击无效”,并没有覆盖注释里说的路径。建议删掉这段误导性的覆盖说明,或者改成一个真正可达的场景。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Range.test.tsx` around lines 985 - 1005, The test's claim that it
covers the nearestIndex === -1 path is incorrect because with
disabled={[true,true,true]} the changeToCloseValue early-returns at if
(!disabled), so the nearestIndex branch is never reached; either remove or
update the misleading comment/description in the it(...) block, or replace the
test with one that actually executes the nearestIndex === -1 branch by ensuring
changeToCloseValue runs (e.g., make at least one handle enabled and construct a
click position that yields nearestIndex === -1 so the code path in Slider.tsx
referencing nearestIndex is exercised); refer to the test case name in
Range.test.tsx and the changeToCloseValue / nearestIndex symbols to locate where
to change the test and its assertion.

src/Slider.tsx Outdated
if (typeof rawDisabled === 'boolean') {
return rawDisabled;
}
return rawDisabled.every((d) => d);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里如果给的是 disabled={[true]}  ,但是有 2 个节点也会被当成全部 disabled。

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/examples/disabled-handle.tsx`:
- Around line 55-58: The description text is inconsistent with the initial state
of BasicDisabledHandle (disabled=[true, false, false, true]) which disables
track dragging; update the paragraph so it accurately reflects behavior (e.g.,
"When any handle is disabled, track dragging is disabled; toggle checkboxes to
enable/disable handles and, when all handles are enabled, drag the track to move
the range.") or alternatively change BasicDisabledHandle's initial disabled
array to match the original description; refer to the JSX around
BasicDisabledHandle and the paragraph node that currently reads "Drag the track
area to move the range" to apply the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f81268eb-5a58-4350-9bea-879d4ee5856d

📥 Commits

Reviewing files that changed from the base of the PR and between 4f28cc4 and ded7506.

📒 Files selected for processing (2)
  • docs/examples/disabled-handle.tsx
  • src/Slider.tsx

@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from ded7506 to b456606 Compare April 14, 2026 08:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Slider.tsx (2)

508-517: ⚠️ Potential issue | 🟡 Minor

缺少 rawValues 依赖项。

管道警告:React Hook React.useEffect has a missing dependency: 'rawValues'。虽然这可能是已有代码,但缺少此依赖可能导致闭包陈旧问题。

🔧 建议的修复
  React.useEffect(() => {
    if (keyboardValue !== null) {
      const valueIndex = rawValues.indexOf(keyboardValue);
      if (valueIndex >= 0) {
        handlesRef.current.focus(valueIndex);
      }
    }

    setKeyboardValue(null);
- }, [keyboardValue]);
+ }, [keyboardValue, rawValues]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 508 - 517, The effect watching keyboardValue is
missing rawValues in its dependency array which can cause stale closures; update
the React.useEffect (the effect that references keyboardValue, rawValues,
handlesRef, and calls setKeyboardValue) to include rawValues in the dependency
array (i.e., [keyboardValue, rawValues]) so the valueIndex lookup uses the
current rawValues; if rawValues is not stable, ensure it's memoized (e.g., via
useMemo) or derive a stable key instead to avoid excessive re-renders.

536-543: ⚠️ Potential issue | 🟡 Minor

缺少 draggingValuerawValues 依赖项。

管道警告:React Hook React.useEffect has missing dependencies: 'draggingValue' and 'rawValues'。当 dragging 变为 false 时,使用的 draggingValuerawValues 可能是陈旧的值。

🔧 建议的修复
  React.useEffect(() => {
    if (!dragging) {
      const valueIndex = rawValues.lastIndexOf(draggingValue);
      handlesRef.current.focus(valueIndex);
    }
- }, [dragging]);
+ }, [dragging, draggingValue, rawValues]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 536 - 543, The effect that auto-focuses the
updated handle is missing dependencies and can read stale values; update the
useEffect dependency array to include draggingValue and rawValues (in addition
to dragging) and inside the effect recompute valueIndex =
rawValues.lastIndexOf(draggingValue) when dragging is false, guarding that
handlesRef.current exists and valueIndex is valid before calling
handlesRef.current.focus(valueIndex); keep the existing dragging boolean
(draggingIndex !== -1) and use the same symbols (dragging, draggingValue,
rawValues, handlesRef) so the hook reruns with fresh values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Slider.tsx`:
- Around line 190-210: The disabled calculation references rawValues before it
is declared causing the ESLint "'rawValues' was used before it was defined"
error; move the useMemo that defines disabled and the useCallback
isHandleDisabled to after rawValues is declared (or refactor them to compute
lazily so they read rawValues at call time), update their dependency arrays to
include rawValues and rawDisabled, and then relocate or re-evaluate any callers
(notably useOffset which depends on isHandleDisabled) so useOffset is invoked
after these definitions—alternatively split isHandleDisabled into a part that
doesn't depend on rawValues (e.g., boolean rawDisabled check) and a part that
uses rawValues to avoid reordering large blocks.

---

Outside diff comments:
In `@src/Slider.tsx`:
- Around line 508-517: The effect watching keyboardValue is missing rawValues in
its dependency array which can cause stale closures; update the React.useEffect
(the effect that references keyboardValue, rawValues, handlesRef, and calls
setKeyboardValue) to include rawValues in the dependency array (i.e.,
[keyboardValue, rawValues]) so the valueIndex lookup uses the current rawValues;
if rawValues is not stable, ensure it's memoized (e.g., via useMemo) or derive a
stable key instead to avoid excessive re-renders.
- Around line 536-543: The effect that auto-focuses the updated handle is
missing dependencies and can read stale values; update the useEffect dependency
array to include draggingValue and rawValues (in addition to dragging) and
inside the effect recompute valueIndex = rawValues.lastIndexOf(draggingValue)
when dragging is false, guarding that handlesRef.current exists and valueIndex
is valid before calling handlesRef.current.focus(valueIndex); keep the existing
dragging boolean (draggingIndex !== -1) and use the same symbols (dragging,
draggingValue, rawValues, handlesRef) so the hook reruns with fresh values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a285a5ff-c125-4412-bbd9-bd2cec8996ba

📥 Commits

Reviewing files that changed from the base of the PR and between ded7506 and b456606.

📒 Files selected for processing (2)
  • docs/examples/disabled-handle.tsx
  • src/Slider.tsx
✅ Files skipped from review due to trivial changes (1)
  • docs/examples/disabled-handle.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/examples/disabled-handle.tsx`:
- Around line 112-116: The paragraph text is inaccurate: because the component
DraggableTrackWithDisabled is initialized with
disabled={[true,false,false,true], and the Tracks implementation disables track
dragging whenever any handle is disabled, track dragging is actually disabled
here; update the paragraph copy to state that track dragging is disabled when
any handle is disabled (or alternatively change the disabled prop on
DraggableTrackWithDisabled to only disable the middle handles if you want
dragging enabled) and mention the disabled prop so the example and description
match actual behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b54edc7-ee41-4327-9c1d-e2abc6449188

📥 Commits

Reviewing files that changed from the base of the PR and between b456606 and 942b218.

📒 Files selected for processing (2)
  • docs/examples/disabled-handle.tsx
  • src/Slider.tsx

Comment on lines +112 to +116
<p style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
First and last handles are disabled (boundaries).
Drag track to move middle handles together.
Click track to add/delete handles.
</p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

文档描述与实际行为不一致

根据 src/Tracks/index.tsx:20-28 的实现,当任意手柄被禁用时,轨道拖拽功能会被禁用。但此处 DraggableTrackWithDisabled 初始状态 disabled={[true, false, false, true]},第一和最后一个手柄已被禁用,因此轨道拖拽实际上是不可用的。

建议修正描述以反映实际行为:

📝 建议修改
       <p style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
         First and last handles are disabled (boundaries).
-        Drag track to move middle handles together.
-        Click track to add/delete handles.
+        Track dragging is disabled when any handle is disabled.
+        Middle handles can still be dragged individually.
       </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
First and last handles are disabled (boundaries).
Drag track to move middle handles together.
Click track to add/delete handles.
</p>
<p style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
First and last handles are disabled (boundaries).
Track dragging is disabled when any handle is disabled.
Middle handles can still be dragged individually.
</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/examples/disabled-handle.tsx` around lines 112 - 116, The paragraph text
is inaccurate: because the component DraggableTrackWithDisabled is initialized
with disabled={[true,false,false,true], and the Tracks implementation disables
track dragging whenever any handle is disabled, track dragging is actually
disabled here; update the paragraph copy to state that track dragging is
disabled when any handle is disabled (or alternatively change the disabled prop
on DraggableTrackWithDisabled to only disable the middle handles if you want
dragging enabled) and mention the disabled prop so the example and description
match actual behavior.

Use value prop to determine expected length instead of rawValues.
This ensures disabled=[true,true,true] correctly applies global disabled styling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from 942b218 to 218f061 Compare April 14, 2026 08:45
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Slider.tsx`:
- Around line 411-433: The current fallback that finds the nearest enabled
handle when isHandleDisabled(valueIndex) is true (using rawValues, newValue and
nearestIndex) can pick a handle across a disabled boundary and allow handles to
jump past disabled handles; change the search in that block so candidates are
limited to handles that can be reached without crossing any disabled handle
between the original index and the candidate: when scanning rawValues for a
nearestIndex, skip any candidate if any index between the original valueIndex
and that candidate is disabled (i.e., ensure the move path contains no disabled
handles), or alternatively restrict the search to the contiguous enabled segment
around the clicked position; update the logic that sets
cloneNextValues[valueIndex], valueIndex and focusIndex accordingly so you never
select a nearestIndex that requires crossing a disabled handle (refer to
isHandleDisabled, rawValues, valueIndex, newValue, cloneNextValues, focusIndex).
- Around line 191-197: The current disabled aggregation in the useMemo (symbols:
rawDisabled, value, disabled) only compares rawDisabled.length to value.length
and thus misses uncontrolled/default handles; change the logic to mirror how
effective handle count is computed elsewhere by using rawValues (or the same
helper that yields rawValues) instead of value when determining expected length
and in the dependency array so disabled is true when rawDisabled aligns with the
actual rendered handles (e.g., use rawValues.length and depend on rawValues,
rawDisabled).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc96bcd9-c708-4d68-816b-2ec8f5bdd931

📥 Commits

Reviewing files that changed from the base of the PR and between 942b218 and 218f061.

📒 Files selected for processing (2)
  • docs/examples/disabled-handle.tsx
  • src/Slider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/examples/disabled-handle.tsx

Comment on lines +191 to +197
const disabled = React.useMemo(() => {
if (typeof rawDisabled === 'boolean') {
return rawDisabled;
};

return Array.isArray(value) && value.length === rawDisabled.length && rawDisabled.every(Boolean);
}, [rawDisabled, value]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

聚合 disabled 仍然漏掉非受控 / 默认句柄数。

Line 196 现在只拿 value.lengthrawDisabled.length 对齐。这样 <Slider range disabled={[true, true]} /><Slider range count={2} disabled={[true, true, true]} /><Slider range defaultValue={[10, 20]} disabled={[true, true]} /> 这类初始化里,所有已渲染 handle 都是禁用的,但根节点 disabled 仍会是 false-disabled 样式和依赖聚合禁用态的分支都会失真。这里需要按和 rawValues 相同的规则推导期望长度,而不是只看 value

🔧 建议修复
  const disabled = React.useMemo(() => {
    if (typeof rawDisabled === 'boolean') {
      return rawDisabled;
-    };
+    }

-    return Array.isArray(value) && value.length === rawDisabled.length && rawDisabled.every(Boolean);
-  }, [rawDisabled, value]);
+    const sourceValues = Array.isArray(value)
+      ? value
+      : Array.isArray(defaultValue)
+        ? defaultValue
+        : undefined;
+
+    const expectedLength =
+      sourceValues?.length ?? (range ? (count >= 0 ? count + 1 : 2) : 1);
+
+    return (
+      expectedLength > 0 &&
+      rawDisabled.length === expectedLength &&
+      Array.from({ length: expectedLength }, (_, index) => rawDisabled[index] === true).every(Boolean)
+    );
+  }, [rawDisabled, value, defaultValue, range, count]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 191 - 197, The current disabled aggregation in
the useMemo (symbols: rawDisabled, value, disabled) only compares
rawDisabled.length to value.length and thus misses uncontrolled/default handles;
change the logic to mirror how effective handle count is computed elsewhere by
using rawValues (or the same helper that yields rawValues) instead of value when
determining expected length and in the dependency array so disabled is true when
rawDisabled aligns with the actual rendered handles (e.g., use rawValues.length
and depend on rawValues, rawDisabled).

Comment on lines +411 to +433
if (isHandleDisabled(valueIndex)) {
let nearestIndex = -1;
let nearestDist = mergedMax - mergedMin;

rawValues.forEach((val, index) => {
if (!isHandleDisabled(index)) {
const dist = Math.abs(newValue - val);
if (dist < nearestDist) {
nearestDist = dist;
nearestIndex = index;
}
}
});

// If all handles are disabled, do nothing
if (nearestIndex === -1) {
return;
}

valueIndex = nearestIndex;
}
cloneNextValues[valueIndex] = newValue;
focusIndex = valueIndex;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

轨道 / Mark 点击的回退选择会让 handle 穿过禁用边界。

Line 415-423 在最近的 handle 是禁用时,会从整个 rawValues 里找“最近的启用 handle”。这会选到禁用边界另一侧的候选:例如 values=[0, 20, 100]disabled=[false, true, false] 时点击 30,这里会把 0 改到 30,直接越过 20。更糟的是后面还会排序,禁用标记会跟着索引错位到新的值上。回退搜索需要限制在 newValue 所在的禁用分段内,或者显式过滤掉移动路径上存在禁用 handle 的候选。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 411 - 433, The current fallback that finds the
nearest enabled handle when isHandleDisabled(valueIndex) is true (using
rawValues, newValue and nearestIndex) can pick a handle across a disabled
boundary and allow handles to jump past disabled handles; change the search in
that block so candidates are limited to handles that can be reached without
crossing any disabled handle between the original index and the candidate: when
scanning rawValues for a nearestIndex, skip any candidate if any index between
the original valueIndex and that candidate is disabled (i.e., ensure the move
path contains no disabled handles), or alternatively restrict the search to the
contiguous enabled segment around the clicked position; update the logic that
sets cloneNextValues[valueIndex], valueIndex and focusIndex accordingly so you
never select a nearestIndex that requires crossing a disabled handle (refer to
isHandleDisabled, rawValues, valueIndex, newValue, cloneNextValues, focusIndex).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants