Skip to content

fix(scrollbar): prevent cursor flicker with CSS-based auto-hide#616

Closed
pengfeixx wants to merge 2 commits into
linuxdeepin:masterfrom
pengfeixx:fix-cursor-flicker
Closed

fix(scrollbar): prevent cursor flicker with CSS-based auto-hide#616
pengfeixx wants to merge 2 commits into
linuxdeepin:masterfrom
pengfeixx:fix-cursor-flicker

Conversation

@pengfeixx
Copy link
Copy Markdown
Contributor

Remove JavaScript autoHide to prevent DOM updates causing cursor flicker. Implement CSS opacity transition for smooth scrollbar show/hide.

移除 JavaScript autoHide 防止 DOM 更新导致光标闪烁,
使用 CSS opacity 过渡实现平滑的滚动条显示/隐藏。

Log: 修复光标闪烁问题
PMS: BUG-359981
Influence: 鼠标悬停在文本上时不再出现光标闪烁,滚动条自动隐藏功能通过
CSS 实现,体验更流畅。

…lipping

Add renderView to customize view container style with overflowX: 'hidden'
and marginBottom: 0 to fix clipping issues.

添加 renderView 自定义视图容器样式,设置 overflowX: 'hidden' 和
marginBottom: 0 修复裁剪问题。

Log: 修复水平滚动条和底部内容裁剪问题
PMS: BUG-342675
Influence: 修复点击左侧大纲最后一个标题时出现水平滚动条的问题,
           修复左侧大纲和右侧内容区域最后一行被遮挡的问题。
@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: pengfeixx

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

你好!我是CodeGeeX。我已经仔细审查了你提供的 git diff。这次代码改动的核心目的是:通过自定义 JavaScript 控制和 CSS 变量(CSS Variables)来替代 react-custom-scrollbars 原生的 autoHide 功能,从而避免因 DOM 显隐切换导致的回流,解决光标闪烁问题。 这个优化思路非常棒,利用 CSS 变量只触发重绘而不触发回流的特性,确实能有效提升体验和性能。

不过,在代码的语法逻辑、性能和健壮性方面,我发现了几个需要改进的地方。以下是详细的审查意见:

1. 语法逻辑

  • 闭包陷阱与定时器竞态:
    useEffect 中,showScrollbarhideScrollbar 两个函数在组件初次渲染时被定义,并在事件监听器中被闭包引用。虽然你使用了 useRef 来存储 timeoutId(这很好,避免了闭包导致的状态不更新问题),但 hideScrollbar 内部清空定时器后将其设为 null,而 showScrollbar 每次触发时都会无条件地设置新的定时器。逻辑上没有大问题,但代码结构可以更清晰。
  • useEffect 依赖项缺失:
    useEffect 的依赖数组为空 [],这意味着事件监听器只在挂载时绑定一次。如果 scrollbarsRef.current 在挂载时还未赋值(例如由于父组件渲染时序问题),scrollbars 会为 null,后续的 viewcontainer 都无法获取,导致整个自动隐藏逻辑失效,且不会再次尝试绑定。

2. 代码性能

  • 高频事件触发频繁重绘:
    mousemove 是一个高频触发的事件。每次鼠标在容器内移动,都会执行 container.style.setProperty('--scrollbar-opacity', '1')。即使值没有改变(已经是 '1' 了),浏览器的引擎仍需处理这个 DOM 操作,这会导致不必要的性能开销和重绘。

3. 代码安全与健壮性

  • CSS 变量作用域污染:
    代码将 --scrollbar-opacity 设置在了 container 节点上。如果 container 是包裹滚动内容的最外层 DOM,这可能会导致该 CSS 变量向下泄漏,影响到子组件中同名的 CSS 变量。最好将变量设置在更具体的滚动条轨道或滑块节点上,或者确保变量名具有足够的唯一性。
  • 构建产物提交:
    你的 diff 中包含了 src/web_dist/toManual/index.jsindex.css 这种编译后的产物。通常情况下,编译后的文件不应该包含在业务代码的 git diff 中,应该由 CI/CD 流程自动构建生成,否则容易导致合并冲突和仓库体积膨胀。

改进建议与重构代码

针对以上问题,我对 scrollbar.jsx 进行了重构:

  1. 增加状态锁:避免在滚动条已显示时重复设置 CSS 变量,优化 mousemove 性能。
  2. 优化 useEffect 依赖:将 scrollbarsRef 加入依赖(或者使用更安全的判空逻辑),确保 ref 赋值后能正确绑定事件。
  3. 提取常量:将 CSS 变量名提取为常量,避免魔法字符串,便于维护。

重构后的 scrollbar.jsx

// SPDX-License-Identifier: GPL-3.0-or-later

import React, { useRef, useEffect, useCallback } from 'react';
import { Scrollbars } from 'react-custom-scrollbars';

const SCROLLBAR_OPACITY_VAR = '--scrollbar-opacity';
const HIDE_TIMEOUT = 800;

function renderScrollBarTrackHorizontal(props) {
  return <span />;
}

function renderView(props) {
  const mergedStyle = {
    ...props.style,
    overflowX: 'hidden',
    marginBottom: 0,
  };
  return <div {...props} style={mergedStyle} />;
}

export default function(props) {
  const scrollbarsRef = useRef(null);
  const timeoutRef = useRef(null);
  const isVisibleRef = useRef(false); // 增加状态锁,避免高频重绘

  const showScrollbar = useCallback(() => {
    const scrollbars = scrollbarsRef.current;
    if (!scrollbars) return;

    const container = scrollbars.container;
    if (container && !isVisibleRef.current) {
      container.style.setProperty(SCROLLBAR_OPACITY_VAR, '1');
      isVisibleRef.current = true;
    }
    
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    timeoutRef.current = setTimeout(hideScrollbar, HIDE_TIMEOUT);
  }, []);

  const hideScrollbar = useCallback(() => {
    const scrollbars = scrollbarsRef.current;
    if (!scrollbars) return;

    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }
    
    const container = scrollbars.container;
    if (container && isVisibleRef.current) {
      container.style.setProperty(SCROLLBAR_OPACITY_VAR, '0');
      isVisibleRef.current = false;
    }
  }, []);

  useEffect(() => {
    const scrollbars = scrollbarsRef.current;
    if (!scrollbars) return;

    const view = scrollbars.view;
    const container = scrollbars.container;

    if (view) {
      view.addEventListener('scroll', showScrollbar);
    }
    if (container) {
      container.addEventListener('mousemove', showScrollbar);
      container.addEventListener('mouseleave', hideScrollbar);
    }

    return () => {
      if (view) {
        view.removeEventListener('scroll', showScrollbar);
      }
      if (container) {
        container.removeEventListener('mousemove', showScrollbar);
        container.removeEventListener('mouseleave', hideScrollbar);
      }
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [showScrollbar, hideScrollbar]); // 依赖回调函数,确保逻辑最新

  return (
    <Scrollbars
      {...props}
      ref={scrollbarsRef}
      className="scrollbar"
      renderTrackHorizontal={renderScrollBarTrackHorizontal}
      renderView={renderView}
    >
      {props.children}
    </Scrollbars>
  );
}

CSS 部分补充建议

你当前的 SCSS 逻辑是没问题的,但要注意 body[style*='user-select: none'] 这种选择器具有较高特异性,且依赖于内联样式的字符串匹配,这在某些浏览器或压缩工具下可能存在脆弱性。如果可能,建议在 body 处于选择模式时,添加一个明确的 class(如 body.select-mode),而不是依赖属性选择器去匹配内联样式。

另外,你的 CSS 变量 --scrollbar-opacity 默认值为 0,在 JS 加载前滚动条会不可见。如果这是一个首屏可视区域的关键滚动条,可能会出现“无滚动条 -> 闪烁出滚动条”的现象。你可以考虑将 CSS 中的默认值设为 1,由 JS 接管后再隐藏;或者接受这种渐进增强的体验。

Remove JavaScript autoHide to prevent DOM updates causing cursor flicker.
Implement CSS opacity transition for smooth scrollbar show/hide.

移除 JavaScript autoHide 防止 DOM 更新导致光标闪烁,
使用 CSS opacity 过渡实现平滑的滚动条显示/隐藏。

Log: 修复光标闪烁问题
PMS: BUG-359981
Influence: 鼠标悬停在文本上时不再出现光标闪烁,滚动条自动隐藏功能通过
           CSS 实现,体验更流畅。
@pengfeixx pengfeixx force-pushed the fix-cursor-flicker branch from 6ed98aa to 0598493 Compare May 11, 2026 08:33
@pengfeixx pengfeixx closed this May 11, 2026
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