Skip to content

feat: 支持 disabled 数组和 onDisabledChange 回调#1069

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

feat: 支持 disabled 数组和 onDisabledChange 回调#1069
EmilyyyLiu wants to merge 13 commits intoreact-component:masterfrom
EmilyyyLiu:feat/disabled-handle-array

Conversation

@EmilyyyLiu
Copy link
Copy Markdown
Contributor

@EmilyyyLiu EmilyyyLiu commented Apr 15, 2026

功能说明

本 PR 新增了两个功能:

  1. disabled 支持数组类型:可以单独禁用特定的滑块手柄
  2. onDisabledChange 回调:在 editable 模式下增删点位时,自动同步 disabled 数组状态

关联issue

ant-design/ant-design#10143

使用示例

const [value, setValue] = useState([20, 50, 60]);
const [disabled, setDisabled] = useState([true, false, true]);

<Slider
  range={{ editable: true }}
  value={value}
  disabled={disabled}
  onChange={setValue}
  onDisabledChange={setDisabled}  // 增删点位时同步 disabled 状态
/>

解决的问题

在 editable 模式下,当添加或删除点位时:

  • 之前:value 数组长度变化,但 disabled 数组未同步,导致索引错位
  • 现在:通过 onDisabledChange 回调,组件自动通知外部更新 disabled 数组

测试覆盖

  • ✅ 所有现有测试通过(124 个)
  • ✅ 新增 onDisabledChange 回调测试
  • ✅ 新增增删点位时 disabled 数组同步测试
  • ✅ 更新 README 文档
  • ✅ 更新示例代码

Summary by CodeRabbit

  • 新功能

    • 支持按句柄禁用:disabled 可为 boolean 或 boolean[]
    • 新增 onDisabledChange 回调,用于在句柄增删时同步禁用数组
  • 行为改进

    • 禁用句柄阻止拖动、点击、键盘操作,作为移动边界并影响插入/替换目标
    • 编辑模式下新增/删除句柄会调整禁用数组并触发回调
  • 文档

    • 添加示例页和示例代码,展示按句柄禁用与交互同步
  • 样式

    • 禁用句柄视觉样式与不可交互光标
  • 测试

    • 新增覆盖禁用数组行为的单元与交互测试

刘欢 and others added 3 commits April 15, 2026 13:02
- Update README.md with disabled array and onDisabledChange documentation
- Fix BasicDisabledHandle example to handle sparse disabled array

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

vercel bot commented Apr 15, 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 15, 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

将 Slider 的 disabled 从单一 boolean 扩展为 boolean | boolean[],新增 onDisabledChange 回调;按手柄禁用逻辑贯穿 Handle、Slider、useDrag、useOffset、Tracks,并更新样式、示例文档与测试以覆盖新行为。

Changes

Cohort / File(s) Summary
文档与示例
README.md, docs/demo/disabled-handle.md, docs/examples/disabled-handle.tsx
disabled 文档更新为 `boolean
样式
assets/index.less
新增 .@{prefixClass}-handle-disabled 样式,调整禁用手柄的背景、边框、阴影与光标,并将伪类应用到 :hover/:active
核心实现:Slider 与 Context
src/Slider.tsx, src/context.ts
SliderProps.disabled 扩展为 `boolean
手柄实现
src/Handles/Handle.tsx
基于 isHandleDisabled 计算 handleDisabled,在禁用时阻断指针/键盘交互、移除 tabIndex、添加 aria-disabled*-disabled 类名。
交互钩子
src/hooks/useDrag.ts, src/hooks/useOffset.ts
useDrag/useOffset 接收可选 isHandleDisabled 回调;useDrag 在启动/轨道点击处对禁用手柄早退,useOffset 将禁用手柄作为移动边界并在推挤循环中跳过或中止遇到的禁用手柄,同时对更新值进行边界夹紧。
轨道行为
src/Tracks/index.tsx
从 context 检查是否存在禁用手柄,若存在则将 onStartMove 置空以禁止轨道拖动启动。
测试
tests/Range.test.tsx
调整测试助手以支持索引,新增“disabled as array” 测试覆盖 DOM 属性、键盘/鼠标/拖拽行为、边界与可编辑模式下增删同步以及 onDisabledChange 调用与兼容性测试。

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Handle as Handle
    participant Slider as Slider
    participant useDrag as useDrag Hook
    participant useOffset as useOffset Hook

    User->>Handle: 交互(点击/拖动/键盘),携带 valueIndex
    Handle->>Slider: 转发交互请求(valueIndex)
    Slider->>Slider: 调用 isHandleDisabled(valueIndex)
    alt 手柄已禁用
        Slider->>Handle: 阻断交互,返回 aria/tabIndex 状态
    else 手柄可用
        Slider->>useDrag: 初始化拖动(含 valueIndex)
        useDrag->>useOffset: 请求位移计算(含 isHandleDisabled)
        useOffset->>useOffset: 计算左右禁用边界并约束/推送值
        useOffset-->>useDrag: 返回受限的新值
        useDrag->>Slider: 触发 onChange/onDrag 回调
        Slider-->>Handle: 更新渲染与聚焦
    end
Loading

Estimated code review effort

🎯 4 (复杂) | ⏱️ ~45 分钟

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标题准确概括了主要变更:添加了disabled数组支持和onDisabledChange回调,这正是raw_summary中所有文件变更的核心。

✏️ 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.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 94.73684% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.78%. Comparing base (0e4e031) to head (7fae517).

Files with missing lines Patch % Lines
src/Slider.tsx 96.07% 2 Missing ⚠️
src/hooks/useDrag.ts 50.00% 2 Missing ⚠️
src/context.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1069      +/-   ##
==========================================
- Coverage   99.39%   98.78%   -0.61%     
==========================================
  Files          14       14              
  Lines         661      741      +80     
  Branches      191      218      +27     
==========================================
+ Hits          657      732      +75     
- Misses          4        9       +5     

☔ 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.

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 support for disabling specific handles in range sliders by allowing the disabled prop to accept an array of booleans. Key changes include updates to handle interaction logic (mouse, keyboard, and track dragging), ARIA accessibility attributes, and the addition of an onDisabledChange callback to maintain state synchronization in editable mode. Review feedback highlighted a potential bug in the array synchronization logic when duplicate values are present and suggested a more consistent approach for determining the slider's overall disabled state in single-handle mode.

Comment thread src/Slider.tsx Outdated
Comment thread src/Slider.tsx Outdated
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

🧹 Nitpick comments (3)
README.md (1)

111-111: 建议微调说明文案可读性。

Line 111 的第二句可补充主语让语义更自然,例如改为 “This prop can also be an array...”。

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

In `@README.md` at line 111, The README description for the disabled prop is
slightly awkward; update the second sentence for clarity by adding an explicit
subject, e.g. change the clause "Can also be an array to disable specific
handles..." to "This prop can also be an array to disable specific handles..."
so the property description for disabled reads naturally and clearly (reference:
property name "disabled" in the table).
docs/examples/disabled-handle.tsx (2)

53-79: 稀疏数组可能导致示例行为不够清晰。

useState([true]) 创建的数组只有索引 0 有值,但滑块有 4 个手柄(值为 [0, 30, 60, 100])。虽然 !!disabled[index] 能正确处理 undefined(转为 false),但这种稀疏数组的初始化方式可能让用户困惑。

建议初始化与 value 数组长度一致的 disabled 数组:

♻️ 建议的改进
 const BasicDisabledHandle = () => {
   const [value, setValue] = useState<number[]>([0, 30, 60, 100]);
-  const [disabled, setDisabled] = useState([true]);
+  const [disabled, setDisabled] = useState([true, false, false, false]);
🤖 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 53 - 79, The disabled array
is initialized as a sparse array via useState([true]) while
BasicDisabledHandle’s value has four handles, which can confuse readers; update
the disabled state initialization to create an array matching value.length
(e.g., using useState(() => value.map(() => true/false) or
Array(value.length).fill(false)) so each handle index is explicitly defined, and
ensure the existing onChange handler that uses setDisabled and !!disabled[index]
continues to work with the fully populated array.

115-117: 移除空的 div 元素。

这个空的 <div> 元素没有任何内容或样式,建议移除。

♻️ 建议的改进
     <div>
       single handle disabled
       <SingleSlider />
     </div>
-    <div>
-
-    </div>
     <div style={style}>
🤖 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 115 - 117, Remove the empty
JSX element: delete the empty <div></div> fragment found in the component's
render/return so there is no unused/blank wrapper element left; ensure any
necessary layout or spacing is preserved by keeping surrounding elements intact
and not altering component logic.
🤖 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/hooks/useOffset.ts`:
- Around line 198-222: The current adjustment logic around minBound/maxBound and
the subsequent push flow allows disabled handles to be moved under allowCross +
pushable because only the active handle is clamped; update the logic in
useOffset (symbols: isHandleDisabled, valueIndex, nextValues, offsetValue,
minBound, maxBound) so that when computing minBound/maxBound you skip disabled
handles (as you already try) and also ensure the push loop(s) that propagate
offsets explicitly ignore disabled handles (do not push into or from an index
where isHandleDisabled(index) is true); in practice, filter or short-circuit any
push iteration to treat disabled handles as immutable boundaries and clamp
nextValues[valueIndex] accordingly before and after calling offsetValue and
before performing any neighbor adjustments.

In `@src/Slider.tsx`:
- Around line 324-341: The current logic that finds the insert/remove index
using includes can fail when values repeat; instead compute the index by
scanning the arrays for the first position where the sequences diverge: when
cloneNextValues is longer, walk both arrays i and j and return the index in
cloneNextValues where an extra item appears; when cloneNextValues is shorter,
walk both arrays and return the index in rawValues where an item was removed.
Update the block that mutates rawDisabled (references: rawValues,
cloneNextValues, rawDisabled, newDisabled, onDisabledChange) to use this
deterministic sequence-compare approach (or a small helper like
getDiffIndex(existing, next)) and then splice at that returned index rather than
using findIndex/includes.

---

Nitpick comments:
In `@docs/examples/disabled-handle.tsx`:
- Around line 53-79: The disabled array is initialized as a sparse array via
useState([true]) while BasicDisabledHandle’s value has four handles, which can
confuse readers; update the disabled state initialization to create an array
matching value.length (e.g., using useState(() => value.map(() => true/false) or
Array(value.length).fill(false)) so each handle index is explicitly defined, and
ensure the existing onChange handler that uses setDisabled and !!disabled[index]
continues to work with the fully populated array.
- Around line 115-117: Remove the empty JSX element: delete the empty
<div></div> fragment found in the component's render/return so there is no
unused/blank wrapper element left; ensure any necessary layout or spacing is
preserved by keeping surrounding elements intact and not altering component
logic.

In `@README.md`:
- Line 111: The README description for the disabled prop is slightly awkward;
update the second sentence for clarity by adding an explicit subject, e.g.
change the clause "Can also be an array to disable specific handles..." to "This
prop can also be an array to disable specific handles..." so the property
description for disabled reads naturally and clearly (reference: property name
"disabled" in the table).
🪄 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: df159bb5-73f9-4e53-a895-b3b282545dd3

📥 Commits

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

📒 Files selected for processing (11)
  • README.md
  • 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 thread src/hooks/useOffset.ts Outdated
Comment thread src/Slider.tsx Outdated
@EmilyyyLiu EmilyyyLiu changed the title feat: 支持 disabled 数组和 onDisabledChange 回调 [WIP]feat: 支持 disabled 数组和 onDisabledChange 回调 Apr 15, 2026
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.

🧹 Nitpick comments (4)
src/Slider.tsx (4)

193-203: valueundefined 时的边界处理。

valueundefined(未受控或初始状态)时,Array.isArray(value)false,此时若 rawDisabled 为数组,disabled 会返回 false。这可能不符合预期——例如用户传入 disabled={[true]} 但未传 value 时,全局 disabled 仍为 false

建议考虑是否应使用 rawValues(已处理后的值数组)来判断:

💡 可选改进
 const disabled = React.useMemo(() => {
   if (typeof rawDisabled === 'boolean') {
     return rawDisabled;
   }
-  if (Array.isArray(value)) {
-    return value.every((_, index) => rawDisabled[index]);
+  // 使用 rawValues 保证与内部状态一致
+  if (rawValues.length > 0) {
+    return rawValues.every((_, index) => rawDisabled[index]);
   }

   return false;
-}, [rawDisabled, value]);
+}, [rawDisabled, rawValues]);

注意:此改动需要调整变量声明顺序,将 rawValues 的计算移到 disabled 之前。

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

In `@src/Slider.tsx` around lines 193 - 203, The disabled computation currently
checks Array.isArray(value) which returns false when value is undefined, causing
cases like rawDisabled=[true] with no value to incorrectly yield disabled=false;
update the logic in the disabled React.useMemo (symbols: disabled, rawDisabled,
value) to use the processed array of values (rawValues) instead of raw value so
array-shaped rawDisabled is honored even when value is undefined, and move the
rawValues calculation above the disabled useMemo so rawValues is available when
computing disabled.

205-213: 建议在开发环境添加长度不匹配警告。

rawDisabled 为数组但长度与 rawValues 不匹配时,越界索引默默返回 false。这可能掩盖配置错误。

💡 可选:添加开发环境警告
 const isHandleDisabled = React.useCallback(
   (index: number) => {
     if (typeof rawDisabled === 'boolean') {
       return rawDisabled;
     }
+    if (process.env.NODE_ENV !== 'production' && index >= rawDisabled.length) {
+      warning(false, `[rc-slider] \`disabled\` array length (${rawDisabled.length}) is shorter than handle count.`);
+    }
     return rawDisabled[index] || false;
   },
   [rawDisabled],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 205 - 213, The isHandleDisabled callback should
warn in development when rawDisabled is an array whose length doesn't match
rawValues to surface configuration errors; update isHandleDisabled (or a small
helper it calls) to detect Array.isArray(rawDisabled) and when
process.env.NODE_ENV !== 'production' and rawDisabled.length !==
rawValues.length emit a console.warn (or the component's logger) describing the
mismatch and the expected length, then continue returning the current boolean
behaviour (rawDisabled[index] || false) so runtime behavior is unchanged.

437-439: 越界访问 rawValues[valueBeforeIndex + 1] 可能返回 undefined

valueBeforeIndex 为最后一个索引时,rawValues[valueBeforeIndex + 1] 返回 undefined,此时 newValue > undefined 始终为 false,虽然不会导致错误行为,但代码意图不够清晰。

💡 建议增加边界检查以提高可读性
-if (rightDisabled && newValue > rawValues[valueBeforeIndex + 1]) {
+if (rightDisabled && valueBeforeIndex + 1 < rawValues.length && newValue > rawValues[valueBeforeIndex + 1]) {
   return;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 437 - 439, The condition accesses
rawValues[valueBeforeIndex + 1] without ensuring the index exists; update the
conditional in the Slider logic so it first checks that valueBeforeIndex + 1 is
within rawValues.length before comparing (e.g., ensure valueBeforeIndex + 1 <
rawValues.length), then apply the existing check (rightDisabled && newValue >
rawValues[valueBeforeIndex + 1]); reference the variables rightDisabled,
newValue, rawValues, and valueBeforeIndex in the function handling value updates
to locate and fix the code.

466-484: 逻辑正确,但与 useOffset.ts 中存在重复代码。

查找 disabled 边界并 clamp 值的逻辑与 src/hooks/useOffset.ts 中的实现(第 199-223 行)几乎相同。当前实现可以接受,但后续可考虑提取为共享工具函数以减少维护成本。

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

In `@src/Slider.tsx` around lines 466 - 484, Duplicate logic: the code in
Slider.tsx that finds the nearest disabled handle bounds and clamps newValue
(using mergedMin/mergedMax, rawValues, valueIndex, isHandleDisabled, and writing
into cloneNextValues) is the same as the implementation in useOffset.ts; extract
this into a shared helper (e.g., clampWithinDisabledBounds or
findDisabledBoundsAndClamp) that accepts rawValues, valueIndex, isHandleDisabled
callback, newValue, mergedMin, mergedMax and returns the clamped value (or
bounds + clamped value), then replace the duplicated block in both Slider.tsx
(where cloneNextValues[valueIndex] is set and focusIndex updated) and in
useOffset.ts to call the new helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Slider.tsx`:
- Around line 193-203: The disabled computation currently checks
Array.isArray(value) which returns false when value is undefined, causing cases
like rawDisabled=[true] with no value to incorrectly yield disabled=false;
update the logic in the disabled React.useMemo (symbols: disabled, rawDisabled,
value) to use the processed array of values (rawValues) instead of raw value so
array-shaped rawDisabled is honored even when value is undefined, and move the
rawValues calculation above the disabled useMemo so rawValues is available when
computing disabled.
- Around line 205-213: The isHandleDisabled callback should warn in development
when rawDisabled is an array whose length doesn't match rawValues to surface
configuration errors; update isHandleDisabled (or a small helper it calls) to
detect Array.isArray(rawDisabled) and when process.env.NODE_ENV !== 'production'
and rawDisabled.length !== rawValues.length emit a console.warn (or the
component's logger) describing the mismatch and the expected length, then
continue returning the current boolean behaviour (rawDisabled[index] || false)
so runtime behavior is unchanged.
- Around line 437-439: The condition accesses rawValues[valueBeforeIndex + 1]
without ensuring the index exists; update the conditional in the Slider logic so
it first checks that valueBeforeIndex + 1 is within rawValues.length before
comparing (e.g., ensure valueBeforeIndex + 1 < rawValues.length), then apply the
existing check (rightDisabled && newValue > rawValues[valueBeforeIndex + 1]);
reference the variables rightDisabled, newValue, rawValues, and valueBeforeIndex
in the function handling value updates to locate and fix the code.
- Around line 466-484: Duplicate logic: the code in Slider.tsx that finds the
nearest disabled handle bounds and clamps newValue (using mergedMin/mergedMax,
rawValues, valueIndex, isHandleDisabled, and writing into cloneNextValues) is
the same as the implementation in useOffset.ts; extract this into a shared
helper (e.g., clampWithinDisabledBounds or findDisabledBoundsAndClamp) that
accepts rawValues, valueIndex, isHandleDisabled callback, newValue, mergedMin,
mergedMax and returns the clamped value (or bounds + clamped value), then
replace the duplicated block in both Slider.tsx (where
cloneNextValues[valueIndex] is set and focusIndex updated) and in useOffset.ts
to call the new helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 725017db-e67b-40f6-836b-89b000fbe4b1

📥 Commits

Reviewing files that changed from the base of the PR and between 5303584 and 4b232b7.

📒 Files selected for processing (2)
  • src/Slider.tsx
  • tests/Range.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Range.test.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.

🧹 Nitpick comments (1)
src/Slider.tsx (1)

333-344: findDiffIndex 逻辑已改进,但存在潜在边界问题。

arr1arr2 短时(如删除场景调用 findDiffIndex(rawValues, cloneNextValues)),循环中 arr1[i]i >= arr1.length 时会返回 undefined,导致后续 filter 比较可能产生非预期结果。

建议将循环边界限制为较短数组的长度加一:

♻️ 建议的优化
       const findDiffIndex = (arr1: number[], arr2: number[]) => {
-        const maxLen = Math.max(arr1.length, arr2.length);
+        // Only need to check up to the longer array's length, but stop early when diff found
+        const maxLen = arr1.length;
         for (let i = 0; i < maxLen; i++) {
           // Count occurrences up to current index
           const count1 = arr1.slice(0, i + 1).filter((v) => v === arr1[i]).length;
           const count2 = arr2.filter((v) => v === arr1[i]).length;
           if (count1 > count2) {
             return i;
           }
         }
-        return -1;
+        // If no diff found in arr1, the new element is at the end of arr2
+        return arr1.length;
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Slider.tsx` around lines 333 - 344, The findDiffIndex function can access
arr1[i] when i >= arr1.length causing undefined comparisons; fix by limiting the
loop to Math.max(0, Math.min(arr1.length, arr2.length) + 1) (i.e., iterate only
up to the shorter array length plus one) so you never read past arr1; update the
loop bound inside findDiffIndex and keep the existing counting logic using
arr1[i] safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Slider.tsx`:
- Around line 333-344: The findDiffIndex function can access arr1[i] when i >=
arr1.length causing undefined comparisons; fix by limiting the loop to
Math.max(0, Math.min(arr1.length, arr2.length) + 1) (i.e., iterate only up to
the shorter array length plus one) so you never read past arr1; update the loop
bound inside findDiffIndex and keep the existing counting logic using arr1[i]
safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 607a8c7b-bff6-43a1-bf76-20944b5c2b9c

📥 Commits

Reviewing files that changed from the base of the PR and between 4b232b7 and e9fd7e1.

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

@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from e9fd7e1 to 604d94e Compare April 15, 2026 07:50
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

♻️ Duplicate comments (2)
src/hooks/useOffset.ts (1)

198-221: ⚠️ Potential issue | 🟠 Major

pushable 间距在 disabled 边界处仍然会被破坏。

这里把 minBound/maxBound 直接钳到 disabled 句柄的值,但没有预留 pushable 距离。allowCross + pushable={10} 下,启用句柄仍然可以被推到 disabled 句柄上或其禁区内,disabled 句柄并没有真正变成“不可推动的边界”。这一点在 [20(disabled), 40, 60] 这类场景里会直接破坏最小间距约束。

建议修复
-    let minBound = min;
-    let maxBound = max;
+    let minBound = min;
+    let maxBound = max;
+    const pushGap = typeof pushable === 'number' ? pushable : 0;

     if (isHandleDisabled) {
       for (let i = valueIndex - 1; i >= 0; i -= 1) {
         if (isHandleDisabled(i)) {
-          minBound = nextValues[i];
+          minBound = nextValues[i] + pushGap;
           break;
         }
       }
       for (let i = valueIndex + 1; i < nextValues.length; i += 1) {
         if (isHandleDisabled(i)) {
-          maxBound = nextValues[i];
+          maxBound = nextValues[i] - pushGap;
           break;
         }
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useOffset.ts` around lines 198 - 221, The code clamps
nextValues[valueIndex] to disabled handle values but ignores the configured
pushable distance, allowing enabled handles to be moved into disabled handles'
push zones; inside useOffset, when you find a disabled handle (using
isHandleDisabled and nextValues), adjust minBound to disabledValue + pushable
for left-side disabled handles and adjust maxBound to disabledValue - pushable
for right-side disabled handles (use the pushable variable from the surrounding
scope), then use those adjusted bounds when clamping nextValues[valueIndex]
after calling offsetValue; update the min/max search loops (the ones referencing
minBound/maxBound and nextValues[i]) and the final clamp so disabled handles
truly enforce the pushable spacing.
src/Slider.tsx (1)

324-341: ⚠️ Potential issue | 🟠 Major

删除句柄时的 disabled 同步在重复值场景下仍然会错位。

新增分支已经改成了按位置比较,但删除分支还在用 includes 推断“哪个值被删掉了”。只要 rawValues 里有重复值,这里就会返回 -1 或删错索引,最终把错误的 disabled 状态同步给外部。更稳妥的做法是把新增/删除的句柄索引从调用点传进来,而不是在这里根据排序后的值数组反推。

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

In `@src/Slider.tsx` around lines 324 - 341, The deletion branch misidentifies the
removed handle when rawValues contains duplicates by using includes; change the
deletion logic in the block that handles cloneNextValues.length <
rawValues.length so it finds the removed handle by positional comparison (mirror
the insertion approach): compute index = rawValues.findIndex((item, i) => item
!== cloneNextValues[i]) and set removeIndex = index === -1 ? rawValues.length -
1 : index, then call newDisabled.splice(removeIndex, 1) and call
onDisabledChange?.(newDisabled); alternatively, modify the caller to pass the
explicit add/remove index into this function and use that index instead of
inferring it here (refer to rawDisabled, cloneNextValues, rawValues,
newDisabled, onDisabledChange).
🤖 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 437-459: The click-path can assign newValue directly to
cloneNextValues[valueIndex] after redirecting to the nearest enabled handle,
which skips the disabled-boundary clamping added in useOffset; update the
assignment to first clamp the candidate newValue against left/right disabled
neighbors (or call the existing offsetValues(..., 'dist') helper to compute the
adjusted value) before setting cloneNextValues[valueIndex] and focusIndex so
enabled handles cannot be moved outside disabled boundaries; adjust logic around
isHandleDisabled, rawValues, valueIndex, cloneNextValues and newValue
accordingly.
- Around line 193-203: The current disabled useMemo wrongly derives "all
disabled" from the controlled value, which breaks uncontrolled cases; update the
computation in the disabled memo (the block referencing rawDisabled and value)
to base the check on the actual rendered handles array instead of only value —
e.g., derive a valuesList that uses value when controlled but falls back to
defaultValue (or whatever internal mergedValues/handles array the component
renders) when uncontrolled, then return valuesList.every((_, i) =>
rawDisabled[i]) (and handle non-array rawDisabled/boolean cases as before).

---

Duplicate comments:
In `@src/hooks/useOffset.ts`:
- Around line 198-221: The code clamps nextValues[valueIndex] to disabled handle
values but ignores the configured pushable distance, allowing enabled handles to
be moved into disabled handles' push zones; inside useOffset, when you find a
disabled handle (using isHandleDisabled and nextValues), adjust minBound to
disabledValue + pushable for left-side disabled handles and adjust maxBound to
disabledValue - pushable for right-side disabled handles (use the pushable
variable from the surrounding scope), then use those adjusted bounds when
clamping nextValues[valueIndex] after calling offsetValue; update the min/max
search loops (the ones referencing minBound/maxBound and nextValues[i]) and the
final clamp so disabled handles truly enforce the pushable spacing.

In `@src/Slider.tsx`:
- Around line 324-341: The deletion branch misidentifies the removed handle when
rawValues contains duplicates by using includes; change the deletion logic in
the block that handles cloneNextValues.length < rawValues.length so it finds the
removed handle by positional comparison (mirror the insertion approach): compute
index = rawValues.findIndex((item, i) => item !== cloneNextValues[i]) and set
removeIndex = index === -1 ? rawValues.length - 1 : index, then call
newDisabled.splice(removeIndex, 1) and call onDisabledChange?.(newDisabled);
alternatively, modify the caller to pass the explicit add/remove index into this
function and use that index instead of inferring it here (refer to rawDisabled,
cloneNextValues, rawValues, newDisabled, onDisabledChange).
🪄 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: 18679b8c-2503-4925-975e-60d949c0d40f

📥 Commits

Reviewing files that changed from the base of the PR and between e9fd7e1 and 604d94e.

📒 Files selected for processing (5)
  • README.md
  • docs/examples/disabled-handle.tsx
  • src/Slider.tsx
  • src/hooks/useOffset.ts
  • tests/Range.test.tsx
✅ Files skipped from review due to trivial changes (1)
  • docs/examples/disabled-handle.tsx

Comment thread src/Slider.tsx Outdated
Comment thread src/Slider.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.

🧹 Nitpick comments (1)
src/Slider.tsx (1)

333-362: 同步逻辑改进良好,但初始对齐问题值得注意。

findDiffIndex 的实现比之前的 includes 方案更健壮,能正确处理重复值场景。

一个潜在问题:如果用户传入的 disabled 数组长度与 rawValues 长度本就不一致,这里的 splice 操作会在已经错位的基础上继续操作。建议考虑在回调前校验长度一致性,或在文档中明确要求 disabled 数组长度必须与 handle 数量匹配。

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

In `@src/Slider.tsx` around lines 333 - 362, 当前逻辑在根据 cloneNextValues/rawValues
差异插入或删除 disabled 元素时没有处理传入的 rawDisabled 与 rawValues 初始长度不一致的问题;请在使用
findDiffIndex 和对 newDisabled 执行 splice 前,校验并归一化 rawDisabled 长度(例如:如果 rawDisabled
长度大于 rawValues,先 slice 到 rawValues.length;如果小于,则用 false 或指定默认值填充到
rawValues.length),然后基于归一化后的数组执行插入/删除,最后再调用 onDisabledChange(newDisabled);保留变量名
rawDisabled、rawValues、cloneNextValues、findDiffIndex、newDisabled 和
onDisabledChange 以便定位修改点,并在校验失败时可选地通过 console.warn 提醒调用方。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Slider.tsx`:
- Around line 333-362: 当前逻辑在根据 cloneNextValues/rawValues 差异插入或删除 disabled
元素时没有处理传入的 rawDisabled 与 rawValues 初始长度不一致的问题;请在使用 findDiffIndex 和对 newDisabled
执行 splice 前,校验并归一化 rawDisabled 长度(例如:如果 rawDisabled 长度大于 rawValues,先 slice 到
rawValues.length;如果小于,则用 false 或指定默认值填充到
rawValues.length),然后基于归一化后的数组执行插入/删除,最后再调用 onDisabledChange(newDisabled);保留变量名
rawDisabled、rawValues、cloneNextValues、findDiffIndex、newDisabled 和
onDisabledChange 以便定位修改点,并在校验失败时可选地通过 console.warn 提醒调用方。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 554ef8cc-5adb-43ee-9483-3a98e3f57fbd

📥 Commits

Reviewing files that changed from the base of the PR and between 604d94e and 40985bc.

📒 Files selected for processing (1)
  • src/Slider.tsx

@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from 40985bc to aca26f6 Compare April 15, 2026 08:51
@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from aca26f6 to 15e473f Compare April 15, 2026 08:54
@zombieJ
Copy link
Copy Markdown
Member

zombieJ commented Apr 16, 2026

@gemini-code-assist CR again

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 support for disabling individual handles in range mode by allowing the disabled prop to accept an array of booleans. The implementation includes updates to the slider logic to enforce boundary constraints around disabled handles, preventing them from being crossed or pushed during interaction. Additionally, a new onDisabledChange callback has been added to keep the disabled state in sync when handles are added or removed in editable mode. Review feedback highlights the need for a more robust index detection method when syncing disabled states, a fix for the global disabled state logic when no handles are present, and the removal of a redundant type check.

Comment thread src/Slider.tsx Outdated
Comment thread src/Slider.tsx
}
if (Array.isArray(rawDisabled)) {
const values = Array.isArray(mergedValue) ? mergedValue : [mergedValue];
return values.every((_, index) => rawDisabled[index]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When mergedValue is an empty array (which can happen in editable mode), values.every() returns true. This causes the slider to appear visually disabled (via the rc-slider-disabled class) even if it's not. Consider ensuring there is at least one handle before concluding the slider is fully disabled.

Suggested change
return values.every((_, index) => rawDisabled[index]);
return values.length > 0 && values.every((_, index) => rawDisabled[index]);

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.

values一定是数组没必要判断长度吧?

Comment thread src/Slider.tsx Outdated
EmilyyyLiu and others added 2 commits April 16, 2026 17:37
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@EmilyyyLiu EmilyyyLiu changed the title [WIP]feat: 支持 disabled 数组和 onDisabledChange 回调 feat: 支持 disabled 数组和 onDisabledChange 回调 Apr 17, 2026
Comment thread docs/examples/disabled-handle.tsx
@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from f2bb03c to 056db7a Compare April 17, 2026 06:05
Add test cases to cover pushable behavior with disabled handles:
- Test pushable maintains distance when pushing toward disabled handle
- Test pushable with step=null and disabled handles

Remove duplicate tests:
- Remove 'pushable with boolean true' (covered by existing tests)
- Remove 'pushable without disabled handles' (covered by 'pushable & allowCross')

Consolidate similar tests to reduce duplication while maintaining coverage.

Coverage: 98.59% statements, 90.29% branches, 100% functions, 100% lines
@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from 056db7a to eee70b9 Compare April 17, 2026 06:08
Comment thread src/Slider.tsx Outdated
newDisabled.splice(index, 1);
}

onDisabledChange?.(newDisabled);
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 节点将线段切成两端,大家只能在自己的那段里处理就行了,这样比较通用。不需要 onDisabledChange

然后 editable 那里,先做成存在一个 disabled 节点就禁止全部 editable,未来 onChange 加一个 info 属性支持告知添加是哪个 index 后再放开

- Remove onDisabledChange callback and related sync logic
- Disable editable mode when any handle is disabled
- Disabled handles act as fixed boundaries that cannot be crossed
- Update examples and tests to reflect new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/hooks/useOffset.ts Outdated
({ value: nextValues[i + 1], changed } = offsetChangedValue(nextValues, 1, i + 1));
}
// Apply boundary constraint to pushed handle
if (isHandleDisabled) {
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.

isHandleDisabled 很奇怪,有的地方当 func 有的地方比如这里直接 if。你在 Slider 里既然始终会传入 isHandleDisabled,那应该 if 都是不需要的

@EmilyyyLiu EmilyyyLiu force-pushed the feat/disabled-handle-array branch from a82de9b to 9584ac3 Compare April 17, 2026 07:57
刘欢 and others added 2 commits April 17, 2026 15:58
- Change isHandleDisabled from optional to required in SliderContextProps
- Remove unnecessary null checks in Tracks and Handle components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
React.createContext default value was missing isHandleDisabled after
making it required in SliderContextProps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/Handles/Handle.tsx
isHandleDisabled,
} = React.useContext(SliderContext);

const handleDisabled = globalDisabled || isHandleDisabled(valueIndex);
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.

这边已经不需要 globalDisabled 了,只需要 isHandleDisabled 来管,上游的 isHandleDisabled 如果遇到全 disabled,它返回的也是 disabled

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.

handleDisabled 名字叫 mergedDisabled

Comment thread src/hooks/useOffset.ts
};

// Get the minimum boundary for a handle considering disabled handles
const getHandleMinBound = (values: number[], handleIndex: number): number => {
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.

参考一下,不一定代码完全一样:

更推荐的方式——将禁用 handle 统一抽象为"固定锚点",在计算边界时把禁用 handle 和 min/max 一起纳入约束集合,而不是在遍历时散落处理:

const getEffectiveMin = (values: number[], handleIndex: number): number => {
  const gap = typeof pushable === 'number' ? pushable : 0;
  // 收集所有左侧禁用 handle 的位置作为候选下界
  const candidates = [min];
  for (let i = handleIndex - 1; i >= 0; i -= 1) {
    if (isHandleDisabled(i)) {
      candidates.push(values[i] + gap);
      break; // 取最近的即可
    }
  }
  return Math.max(...candidates);
};

这样语义更清晰,也更容易扩展(比如将来支持 step 约束等)。

Comment thread src/Slider.tsx
if (typeof rawDisabled === 'boolean') {
return rawDisabled;
}
return rawDisabled[index] || false;
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 的返回单个,没有就 fallback 成全局的 disabled

Comment thread src/Slider.tsx
focusIndex = valueBeforeIndex + 1;
} else {
cloneNextValues[valueIndex] = newValue;
if (isHandleDisabled(valueIndex)) {
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.

看着有点啰嗦,优化一下?

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.

2 participants