Skip to content

fix(UI): 在根布局建立滚轮事件边界,避免 ScriptCat 内部滚动触发浏览器 swipe 跳页#1431

Open
cyfung1031 wants to merge 5 commits intomainfrom
fix/overscroll-behavior/2
Open

fix(UI): 在根布局建立滚轮事件边界,避免 ScriptCat 内部滚动触发浏览器 swipe 跳页#1431
cyfung1031 wants to merge 5 commits intomainfrom
fix/overscroll-behavior/2

Conversation

@cyfung1031
Copy link
Copy Markdown
Collaborator

@cyfung1031 cyfung1031 commented May 10, 2026

上次的 #1413 修完后还是不满意效果。好像不稳定
这个 ScrollBoundary 是针对整个 #root
所有 wheel 跑到这个 #root 外层就会截下来, 不让 body/html 触发 swipe 跳页


Checklist / 检查清单

  • Fixes mentioned issues / 修复已提及的问题
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

#1413 已针对脚本列表横向滚动、Monaco 编辑器横向滚动导致浏览器触发「上一页 / 下一页」的问题做过局部修正,但实际使用中仍存在不稳定情况。

在 ScriptCat 的 options 页面里,脚本列表、脚本编辑器、弹窗、下拉菜单等组件都运行在同一个 React 根节点下。部分区域,尤其是 Monaco 编辑器和可横向滚动的内容区域,在触控板或鼠标滚轮产生横向滚动时,wheel 事件仍可能继续向 body/html 传播,进而触发浏览器或宿主页面的 swipe back/forward 行为,表现为用户只是在页面内滚动,却意外跳转到上一页。

因此这次不再只依赖局部 overscroll-behavior,而是在 ScriptCat 应用根节点建立统一的滚轮事件边界。

本次变更

1. 新增 ScrollBoundary 组件

新增 src/pages/components/layout/ScrollBoundary.tsx,用于在指定父节点上注册 wheel 事件监听器。

当前接入点为 #root,也就是 ScriptCat React 应用的根容器。

处理策略:

  • 当滚轮事件来自 Monaco 编辑器内部时,调用 preventDefault(),避免事件继续触发浏览器 / 页面级横向滑动行为。
  • 当滚轮事件来自其他 ScriptCat UI 区域时,调用 stopImmediatePropagation()stopPropagation(),阻止事件继续冒泡到根节点外部。
  • 不对非 Monaco 区域调用 preventDefault(),保留脚本列表、弹窗、下拉菜单等区域自身的原生滚动能力。
  • 监听器使用 passive: false,确保 Monaco 场景下的 preventDefault() 可以生效。
  • 注册前先移除同一个 handler,避免重复绑定。

2. 在 MainLayout 中包裹全局布局

将 ScriptCat options 页面内的主要 UI 都纳入同一个滚动边界中,包括顶部工具栏、脚本导入弹窗、脚本列表、脚本编辑器和子路由页面。

这样可以避免每个页面或组件单独处理滚轮穿透问题,后续新增可滚动区域时也能复用这层根节点保护。

3. 调整全局滚动样式

src/index.css 中对 #root 增加:

#root {
    overscroll-behavior: none;
    contain: content;
    overflow: auto;
}

作用是将 ScriptCat 应用根容器明确作为滚动边界,减少滚动链继续传递到 body/html 的机会。

同时将滚动条颜色改为通过 * { scrollbar-color: inherit; } 继承,继续保持 light/dark theme 下滚动条样式一致。

为什么这样改

之前 #1413 的修复更偏向局部:在脚本列表和编辑器相关容器上增加 tw-overscroll-none。这种方式可以覆盖已知区域,但对复杂嵌套组件、Monaco 内部滚动实现、弹层组件以及后续新增滚动区域并不够稳。

这次改为在 #root 上建立统一的 ScrollBoundary,把「ScriptCat 内部滚动」和「浏览器页面级 swipe 行为」隔离开:

  • ScriptCat 内部组件仍然可以正常滚动。
  • Monaco 编辑器滚动不会误触发浏览器后退 / 前进。
  • 脚本列表横向滚动不会导致页面跳转。
  • 后续新增页面或滚动容器时,不需要每个地方都重复加 overscroll 规则。

影响范围

  • 仅影响 ScriptCat React 根节点 #root 内部的 wheel 事件处理。
  • 不修改业务逻辑、脚本管理逻辑、脚本执行逻辑或导入逻辑。
  • 不改变现有组件对外接口。
  • 主要影响 options 页面中的滚轮 / 触控板滚动行为。
  • 非 Monaco 区域不阻止默认滚动,因此列表、弹窗、菜单等组件仍应保持原有滚动体验。

测试

已验证:

  • 在 Monaco 编辑器中使用触控板 / 鼠标滚轮横向滚动,不再触发浏览器上一页 / 下一页。
  • 在脚本列表中横向滚动,不再误触发页面跳转。
  • 在编辑器外的普通页面区域滚动,页面滚动行为保持正常。
  • 弹窗、下拉菜单等 Arco 组件仍可正常交互。
  • light / dark theme 下滚动条样式保持正常。

@cyfung1031 cyfung1031 changed the title fix(UI): 避免页面互动触发 body 的 swipe 跳页 fix(UI): 引入 ScrollBoundary 组件,隔离滚轮事件逻辑,避免页面互动触发 body 的 swipe 跳页 May 10, 2026
@cyfung1031 cyfung1031 added the UI/UX 页面操作/显示相关 label May 10, 2026
@CodFrm CodFrm requested a review from Copilot May 10, 2026 05:39
@cyfung1031 cyfung1031 changed the title fix(UI): 引入 ScrollBoundary 组件,隔离滚轮事件逻辑,避免页面互动触发 body 的 swipe 跳页 fix(UI): 在根布局建立滚轮事件边界,避免 ScriptCat 内部滚动触发浏览器 swipe 跳页 May 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

该 PR 旨在通过在 #root 层面隔离 wheel 事件与调整根节点滚动相关 CSS,避免页面互动时滚轮事件“穿透”到 body/html 从而触发浏览器的 swipe 跳页行为;同时对 Monaco 编辑器区域做特殊处理以保持编辑器滚动体验稳定。

Changes:

  • 新增 ScrollBoundary 组件:在指定根节点上注册 wheel 监听,对 Monaco 区域 preventDefault(),其他区域阻断事件传播。
  • MainLayout 根部引入并包裹 ScrollBoundary,将边界逻辑统一作用于页面根容器。
  • 调整全局样式:为 #root 增加 overscroll-behavior/contain/overflow,并新增全局 scrollbar-color 继承规则。

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/pages/components/layout/ScrollBoundary.tsx 新增滚轮事件边界组件并在根节点绑定 wheel 监听以隔离滚动手势影响范围
src/pages/components/layout/MainLayout.tsx 在布局根部包裹 ScrollBoundary,将滚轮隔离策略应用到整个页面内容树
src/index.css 通过 #root 的 overscroll/overflow/contain 等样式尝试抑制 swipe 跳页,并调整滚动条颜色继承策略

Comment on lines +52 to +56
export default function ScrollBoundary({ children, parentNodeSelector }: ScrollBoundaryProps) {
// Attach once per render; the handler is idempotent due to the
// remove-then-add pattern in attachScrollBoundaryHandler.
attachScrollBoundaryHandler(document.querySelector(parentNodeSelector));
return <>{children}</>;
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.

不用移除。都是加载在 DOM Node 本身。 DOM 被移除就会清掉 (虽然 #root 不会有这个可能)

* scrolling within non-editor children.)
*/
const handleScrollBoundaryWheel = (evt: Event) => {
if ((evt.target as Element).closest(".monaco-editor")) {
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.

只会是 Element (#root)

Comment thread src/pages/components/layout/ScrollBoundary.tsx Outdated
Comment thread src/pages/components/layout/ScrollBoundary.tsx Outdated
Comment thread src/index.css

#root {
overscroll-behavior: none;
contain: content;
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.

不需要。#root 已是最外层。不影响

Comment thread src/index.css
Comment on lines +5 to +8
* {
scrollbar-color: inherit;
}

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.

就是用来统一容器样式

cyfung1031 and others added 2 commits May 10, 2026 14:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UI/UX 页面操作/显示相关

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants