Skip to content

fix: honor offset in non-virtual scrollTo, export scroll config types#57

Merged
zombieJ merged 13 commits into
react-component:masterfrom
aojunhao123:fix/non-virtual-scroll-offset
Jul 16, 2026
Merged

fix: honor offset in non-virtual scrollTo, export scroll config types#57
zombieJ merged 13 commits into
react-component:masterfrom
aojunhao123:fix/non-virtual-scroll-offset

Conversation

@aojunhao123

@aojunhao123 aojunhao123 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

scrollTo({ key, align, offset }) already applied offset in the virtual list, but the non-virtual (virtual={false}) RawList path ignored it. It set a --rc-listy-item-scroll-margin-top CSS variable that only accounted for the sticky-header height, never the user's offset, and it never set a bottom margin — so align: 'bottom' offsets did nothing at all in non-virtual mode.

This PR brings the non-virtual path to parity and tidies up the approach:

  • RawList now honors offset — it sets scrollMarginTop / scrollMarginBottom directly on the scroll target at scroll time (sticky-header height + offset for items with align: 'top', offset otherwise), matching the virtual list.
  • Drops the CSS-variable indirection (--rc-listy-item-scroll-margin-top) and the per-render inline scrollMarginTop on every item. The margin is now applied only on the actual target, and only while scrolling.
  • Exports ScrollAlign and ListyScrollToConfig from the package entry so consumers can type the config they pass to scrollTo (previously declared but not exported).
  • Adds a scroll-to demo that exercises both virtual and raw modes with live align / offset / key / group controls.

No changes to the public config shape — offset was already a typed option; it simply didn't work in non-virtual mode.

Test plan

  • tests/listy.behavior.test.tsx and tests/semantic.test.tsx updated and passing (25/25) — assert scrollMarginTop/scrollMarginBottom reflect header height + offset on the target, and that a user-supplied styles.item.scrollMarginTop is present at rest but the internal offset wins on the scroll target.
  • tsc --noEmit clean
  • eslint clean on changed source and the new demo
  • Manually verify the scroll-to demo in both virtual and non-virtual modes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 新功能
    • 新增“滚动到指定位置”演示页面与示例入口,支持虚拟滚动、top/bottom/auto 对齐、offset,并支持按 item key 与分组标题滚动展示。
    • 对外补充滚动对齐与配置类型(ScrollAlign、ListyScrollToConfig)。
  • 问题修复/改进
    • 优化粘性分组的滚动定位间距计算与精度,提升 sticky 与 auto 对齐的一致性。
    • 修复 item 与分组同键时的滚动目标区分,避免误跳转。
  • 测试
    • 更新并扩展 scrollMarginTop/scrollMarginBottom、sticky/virtual 相关用例与边界场景断言。

@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

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

新增 scrollTo 交互式演示,统一 item/group 类型化滚动键,调整 RawList 与 VirtualList 的滚动边距和 sticky 对齐处理,并补充分组、virtual 与原生滚动测试。

Changes

滚动定位与分组滚动

Layer / File(s) Summary
类型化键与行模型
src/util.ts, src/hooks/useItemKey.ts, src/VirtualList/useFlattenRows.ts, src/index.ts
新增 item/group tagged key 和条目键提取 hook;展开行统一包含 tagged key,并导出滚动相关类型。
VirtualList sticky 滚动定位
src/VirtualList/index.tsx, src/VirtualList/useStickyGroupHeader.tsx
VirtualList 使用 tagged key 渲染 item/group 行,并根据 sticky 分组、对齐方式和组头尺寸调整 scrollTo offset。
RawList 目标与滚动边距
src/RawList/index.tsx, src/RawList/useRawListScroll.ts
RawList 使用类型化 data-key 定位 item/group,并在 scrollIntoView 期间临时设置和恢复滚动边距。
演示、测试与开发配置
docs/demos/scroll-to.md, docs/examples/scroll-to.tsx, tests/*, .dumirc.ts, package.json, .githooks/pre-commit
新增 scrollTo 演示,扩展滚动、sticky、键冲突和 falsy 分组键测试,并更新别名、虚拟列表版本和提交检查脚本。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ListyRef
  participant VirtualList
  participant useRawListScroll
  participant TargetElement
  ListyRef->>VirtualList: scrollTo(config)
  VirtualList->>VirtualList: 生成 tagged item/group key
  VirtualList->>ListyRef: 调用虚拟列表滚动
  ListyRef->>useRawListScroll: scrollTo(config)
  useRawListScroll->>TargetElement: 设置临时 scroll margins
  useRawListScroll->>TargetElement: 调用 scrollIntoView
Loading

Possibly related PRs

Suggested reviewers: zombiej

Poem

兔子挥爪滚到底,
item、group 不相欺。
顶部底部皆对齐,
offset 轻轻加一笔,
测试闪闪蹦满地。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了核心变更:非虚拟 scrollTo 支持 offset,并导出滚动配置类型。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6fb43b7) to head (afba0d9).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master       #57   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            8        10    +2     
  Lines          211       216    +5     
  Branches        62        55    -7     
=========================================
+ Hits           211       216    +5     

☔ View full report in Codecov by Harness.
📢 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.

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

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.

Code Review

This pull request introduces support for custom offsets when scrolling to items or group headers in RawList. It refactors the scroll margin application by replacing the CSS-variable-based approach with direct inline styles (scrollMarginTop and scrollMarginBottom) dynamically computed on the target element. It also adds a new "Scroll To" demo, exports relevant types, and updates the test suite to verify the new behavior. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/RawList/useRawListScroll.ts`:
- Around line 69-77: 在 useRawListScroll 中分离 item 与 group 的滚动定位属性:根据 isItem 选择
data-item-key 或 data-group-key 构造选择器,避免继续使用
data-key;同时确保对应的条目和分组节点分别写入匹配的数据属性,并保留现有 applyScrollMargin 的定位行为。
- Around line 39-52: Update applyScrollMargin so sticky header height is
included for item targets whenever align is not 'bottom', including 'auto';
retain zero header offset for bottom alignment and non-item targets.
🪄 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: 41b08372-c439-473b-a4a2-4559168c7b23

📥 Commits

Reviewing files that changed from the base of the PR and between 13f952a and b83e38c.

📒 Files selected for processing (7)
  • docs/demos/scroll-to.md
  • docs/examples/scroll-to.tsx
  • src/RawList/index.tsx
  • src/RawList/useRawListScroll.ts
  • src/index.ts
  • tests/listy.behavior.test.tsx
  • tests/semantic.test.tsx

Comment thread src/RawList/useRawListScroll.ts Outdated
Comment thread src/RawList/useRawListScroll.ts Outdated
@zombieJ

zombieJ commented Jul 14, 2026

Copy link
Copy Markdown
Member

来自 Code Review Skill

这个 PR 在做什么

这个 PR 为 RawList 的 item/group scrollTo 补齐 offset,处理 sticky header 和 auto 对齐,并公开滚动配置类型。它还增加了完整演示以及 RawList、VirtualList、sticky、语义样式相关测试。

结论

不适合合并

存在一个可由合法输入触发的滚动目标混淆问题:item key 与 group key 相同时,会滚动到错误节点。应先拆分两类节点的定位属性并补充回归测试;此外,当前分支与 master 存在合并冲突,需要先更新分支。

问题清单

🔴 高优先级(阻塞合并)

  • item 与 group 共用 data-key,相同 key 会命中错误的滚动目标src/RawList/useRawListScroll.ts:72
    RawList 同时把 data-key 写在 group section 和 item 上;由于 group section 位于 item 之前,scrollTo({ key: value }) 在 item key 与 group key 字符串相同时会选中 group section。这样不仅滚动位置错误,sticky header offset 也会基于错误节点计算。建议分别使用 data-item-keydata-group-key,按配置类型构造选择器,并加入 key 冲突测试。

🟡 中优先级(建议修复)

无。

🟢 低优先级(可选改进)

无。

The virtual list already applied the `offset` from `scrollTo({ key, align,
offset })`, but the non-virtual RawList path ignored it: it set a
`--rc-listy-item-scroll-margin-top` CSS variable that only accounted for the
sticky header height and never the user's offset, and it never set a bottom
margin, so `align: 'bottom'` offsets did nothing.

- RawList now applies `scrollMarginTop`/`scrollMarginBottom` directly on the
  scroll target at scroll time (header height + offset for items on `top`
  align, offset otherwise), matching the virtual path.
- Drop the CSS-variable indirection and the per-render inline scrollMarginTop
  on every item; the margin is applied only on the target, only when scrolling.
- Export `ScrollAlign` and `ListyScrollToConfig` so consumers can type the
  config they pass to `scrollTo`.
- Add a `scroll-to` demo exercising both virtual and raw modes.
`align: 'auto'` maps to `scrollIntoView({ block: 'nearest' })`, which can land
the item at the container top where the sticky header would occlude it. The
header offset was only added for `align: 'top'`, so `auto` (and the default)
were left uncorrected in both the raw and virtual paths.

Apply the header offset whenever alignment is not explicitly `bottom`, in both
RawList and VirtualList, keeping the two modes consistent.
@aojunhao123
aojunhao123 force-pushed the fix/non-virtual-scroll-offset branch from 62adb9d to 7b6970c Compare July 14, 2026 03:38
@zombieJ

zombieJ commented Jul 14, 2026

Copy link
Copy Markdown
Member

来自 Code Review Skill

这个 PR 在做什么

PR #57 已 rebase 到包含 #56 的最新 master,合并冲突已经解决。它为非虚拟列表的 scrollTo 补齐 offset 支持,同时调整 sticky 分组的自动对齐逻辑、导出滚动配置类型,并增加演示与测试。

结论

不适合合并

RawList 的 offset 实现基本正确,但 VirtualList 对 align: 'auto' 的 sticky header 补偿会错误影响最终选择 bottom 对齐的情况。核心 CI 均通过;Vercel 仍只是团队授权失败。

问题清单

🔴 高优先级(阻塞合并)

  • auto 最终按底部对齐时仍错误叠加 sticky header 高度src/VirtualList/index.tsx:106
    rc-virtual-list 会根据目标位于视口上方还是下方,将 auto 动态解析为 top 或 bottom;但这里预先把 header 高度加入统一的 offset。当目标位于视口下方时,底部对齐也会多出一个 header 高度,造成错误的底部间距。新增测试只调用了 offset 函数,没有覆盖 auto → bottom 的实际落点;建议暂时只对显式 align: 'top' 补偿,或让补偿能够根据最终对齐方向决定。

🟡 中优先级(建议修复)

  • 调用 scrollTo 会永久覆盖用户传入的 scroll margin 样式src/RawList/useRawListScroll.ts:49
    直接修改 React 管理的 element.style.scrollMarginTop/Bottom 后没有恢复。若用户通过 styles.item 设置这些属性,调用一次 scrollTo 后其值会被永久替换;普通 rerender 也不一定恢复,因为 React 认为 style props 没有变化。建议在 scrollIntoView 前保存原值,并在浏览器读取滚动参数后恢复,或继续用内部 CSS 变量隔离状态。

🟢 低优先级(可选改进)

  • item/group 同名 key 仍会选错目标src/RawList/useRawListScroll.ts:72
    上次指出的问题仍存在。不过重新对比 master 后确认,这是已有实现中的历史问题,并非本 PR 引入,因此不再作为本次合并阻塞项;仍建议后续拆分 data-item-keydata-group-key

Item keys and group keys shared one namespace in virtual row keys and
raw-mode data-key attributes, so a colliding key (an item id equal to a
group key) made scrollTo target the wrong node. Row keys are now tagged
as `item:x` / `group:x` via a single toTaggedKey helper, stamped once
per row at flatten time — which also turns the per-frame itemKey calls
in the virtual hot path into plain property reads.

Also fixes two edges in the sticky-header offset path: the group lookup
map is keyed by tagged keys so a string/number key mismatch can no
longer scroll while silently dropping the header compensation, and a
falsy group key (0, '') no longer disables the header push-up.

Note: raw-mode data-key attribute values change from the raw key to the
tagged form (`data-key="item:1"`).
The scrollTo refactor split the single fallthrough into explicit
branches; the numeric-offset and position-config paths had no
virtual-mode coverage (only raw-mode).
…d align

Non-virtual (raw list) scrollTo wrote scroll-margin directly on the target and
left it there, permanently overwriting styles.item.scrollMargin*. React diffs
the style prop against its own last-rendered value, not the live DOM, so a
same-value rerender never restores it. Apply the margin only for the
synchronous scrollIntoView call and restore the element's inline value right
after.

Virtual scrollTo now reads the resolved `align` exposed by the virtual-list
offset callback instead of reverse-engineering the direction via
getScrollInfo(), adding the sticky-header offset only when the item lands at
the top.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/listy.behavior.test.tsx (1)

213-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

请使用非空初始值验证滚动边距恢复。

这些断言只覆盖原值为空的情况;若实现滚动后始终清空样式,测试仍会通过,无法保护本 PR 修复的用户自定义样式覆盖场景。

建议补充的验证
+groupBSection.style.scrollMarginTop = '13px';
 // 执行 scrollTo
 expect(marginDuringScroll).toBe('5px');
-expect(groupBSection.style.scrollMarginTop).toBe('');
+expect(groupBSection.style.scrollMarginTop).toBe('13px');

+secondItem.style.scrollMarginBottom = '9px';
 // 执行 scrollTo
 expect(bottomMarginDuringScroll).toBe('4px');
-expect(secondItem.style.scrollMarginBottom).toBe('');
+expect(secondItem.style.scrollMarginBottom).toBe('9px');

Also applies to: 279-281, 426-427, 467-470, 497-499

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/listy.behavior.test.tsx` around lines 213 - 215, 更新相关滚动边距测试,将目标元素的
scrollMarginTop 预设为非空的用户自定义值;在滚动期间继续验证使用
5px,滚动完成后验证恢复为原始自定义值而非空字符串。覆盖当前断言及其余标注的测试场景,确保测试保护样式恢复行为。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/listy.behavior.test.tsx`:
- Around line 213-215: 更新相关滚动边距测试,将目标元素的 scrollMarginTop
预设为非空的用户自定义值;在滚动期间继续验证使用
5px,滚动完成后验证恢复为原始自定义值而非空字符串。覆盖当前断言及其余标注的测试场景,确保测试保护样式恢复行为。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b35fa98c-f690-4725-98c5-578d66e68aae

📥 Commits

Reviewing files that changed from the base of the PR and between 7b6970c and d566931.

📒 Files selected for processing (12)
  • .dumirc.ts
  • package.json
  • src/RawList/index.tsx
  • src/RawList/useRawListScroll.ts
  • src/VirtualList/index.tsx
  • src/VirtualList/useFlattenRows.ts
  • src/VirtualList/useStickyGroupHeader.tsx
  • src/hooks/useItemKey.ts
  • src/util.ts
  • tests/hooks.test.tsx
  • tests/listy.behavior.test.tsx
  • tests/semantic.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/semantic.test.tsx
  • src/RawList/useRawListScroll.ts
  • src/RawList/index.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 37: Update the package.json prepare script to tolerate missing Git or a
missing .git directory by appending a fallback that prevents git config failure
from aborting npm install.
🪄 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: 3d6c90cc-2927-4d2b-9665-c938f8a680b3

📥 Commits

Reviewing files that changed from the base of the PR and between d566931 and 4a3e5e3.

📒 Files selected for processing (10)
  • .githooks/pre-commit
  • docs/examples/scroll-to.tsx
  • package.json
  • src/RawList/index.tsx
  • src/RawList/useRawListScroll.ts
  • src/VirtualList/index.tsx
  • src/VirtualList/useFlattenRows.ts
  • tests/hooks.test.tsx
  • tests/listy.behavior.test.tsx
  • tests/semantic.test.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/semantic.test.tsx
  • src/RawList/useRawListScroll.ts
  • src/VirtualList/useFlattenRows.ts
  • src/VirtualList/index.tsx
  • src/RawList/index.tsx
  • docs/examples/scroll-to.tsx
  • tests/hooks.test.tsx

Comment thread package.json Outdated
Updated pre-commit hook to include prettier check for staged files.
Comment thread src/hooks/useItemKey.ts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

rawList 和 virtual-list 都有这段 完全一样的逻辑,单独抽个 hook

if ('key' in config || 'groupKey' in config) {
const { align = 'top' } = config;
const targetKey = 'groupKey' in config ? config.groupKey : config.key;
const { align = 'auto', offset = 0 } = config;

@aojunhao123 aojunhao123 Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

这里 rawList 不传 align 默认应该是 auto,和 virtualList 行为对齐

@aojunhao123 aojunhao123 Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

这里是 fable 5 给我的优化,在这里 flatten 顺便给 key 加上前缀,然后传给 rc 侧的 itemKey 就直接用拼好的 taggedKey ,避免了在每个滚动帧里调用 getKey,还有性能上的收益(ai 真强

@socket-security

socket-security Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​husky@​9.1.71001006280100
Addednpm/​lint-staged@​17.0.810010010096100

View full report

@zombieJ
zombieJ merged commit a3d3da4 into react-component:master Jul 16, 2026
10 of 11 checks passed
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