Skip to content

feat: 为弹窗组件添加动画可见性控制#7045

Open
Passing-of-A-Dream wants to merge 2 commits into
ant-design:masterfrom
Passing-of-A-Dream:fix-6815
Open

feat: 为弹窗组件添加动画可见性控制#7045
Passing-of-A-Dream wants to merge 2 commits into
ant-design:masterfrom
Passing-of-A-Dream:fix-6815

Conversation

@Passing-of-A-Dream
Copy link
Copy Markdown
Contributor

@Passing-of-A-Dream Passing-of-A-Dream commented May 11, 2026

close #6815 修复页面不可见时动画暂停,并且切回来之后不继续执行

Summary by CodeRabbit

发布说明

  • 错误修复

    • 改进了弹窗与遮罩的可见性与动画终止处理,在浏览器标签页切换或组件卸载时能更可靠地触发/抑制 afterShow/afterClose 回调,避免重复或错过回调,提升动画与关闭逻辑的稳定性。
  • 测试

    • 新增完整的可见性状态管理测试套件,覆盖页面可见性切换与关闭回调行为验证。

Review Change Stack

- 新增 use-spring-visibility 工具函数
- 重构 CenterPopup、Mask 和 Popup 组件的动画逻辑
- 优化弹窗关闭时的回调触发时机
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label May 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

Preview is ready

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3310b5ca-a3bd-41df-8f65-2c82b5027e4a

📥 Commits

Reviewing files that changed from the base of the PR and between 20d84de and 8826578.

📒 Files selected for processing (1)
  • src/utils/use-spring-visibility.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/use-spring-visibility.ts

📝 Walkthrough

Walkthrough

引入 useSpringVisibility Hook,添加可见性监听以在页面从后台返回时触发关闭流程;将该 Hook 集成到 CenterPopup、Mask 和 Popup,并新增对应测试覆盖。

变更

可见性感知的弹窗生命周期管理

层级 / 文件 摘要
Hook 核心实现
src/utils/use-spring-visibility.ts
新 Hook 维护关闭状态和最新回调值的 refs。监听 visibilitychange 事件;当文档变为可见且组件处于激活但不可见且未关闭状态时,设置 activefalse 并调用 afterClose。返回 shouldCallAfterClose() 在每个关闭周期内控制一次性回调执行。
Hook 测试套件
src/utils/tests/use-spring-visibility.test.ts
全面的测试覆盖了可见性转换、卸载场景和 shouldCallAfterClose() 门控逻辑。通过模拟回调、操纵 document.visibilityState 和派发 visibilitychange 事件来验证回调执行、一次性调用和状态重置。
CenterPopup 集成
src/components/center-popup/center-popup.tsx
CenterPopup 引入 useSpringVisibility,用 visibleactivesetActiveafterCloseunmountedRef 连接 Hook。更新 useSpringonRest:可见时设置 active=true 并调用 afterShow;隐藏时通过 shouldCallAfterClose() 条件化调用 afterClose。保留 useIsomorphicLayoutEffect 确保打开时 active=true
Mask 集成
src/components/mask/mask.tsx
Mask 导入并调用 useSpringVisibility,采用相同接线模式。更新 useSpring onRest:可见时设置 active=true 并触发 afterShow;不可见时通过 shouldCallAfterClose() 门控 afterClose 调用。
Popup 集成
src/components/popup/popup.tsx
Popup 集成 useSpringVisibility,采用相同属性模式。更新 useSpring onRest 回调:可见时设置 active=true 并调用 afterShow;隐藏时使用 shouldCallAfterClose() 有条件地调用 afterClose 并设置 active=false。保留滑动关闭与位置百分比弹簧行为。

Sequence Diagram(s)

sequenceDiagram
  participant Document as 浏览器标签
  participant Hook as useSpringVisibility
  participant Component as 弹窗组件
  participant Callback as afterClose 回调

  rect rgba(220, 20, 60, 0.5)
  Note over Document,Callback: 页面在后台时关闭弹窗
  Component->>Component: visible=false (用户/代码触发关闭)
  Component->>Component: Spring 动画开始(onRest 可能不触发)
  Note over Document,Component: 页面隐藏,onRest 不一定触发
  end

  rect rgba(34, 139, 34, 0.5)
  Note over Document,Callback: 用户切回到页面
  Document->>Hook: visibilitychange (hidden→visible)
  Hook->>Hook: 检查 active===true && visible===false && !closed
  alt 条件满足
    Hook->>Component: setActive(false)
    Hook->>Callback: afterClose()
    Hook->>Hook: closed = true
  else 跳过
    Hook->>Hook: 无操作
  end
  Callback->>Component: 卸载组件、清理 DOM
  end
Loading

预估代码审查工作量

🎯 3 (中等) | ⏱️ ~20 分钟

概览

PR 引入了一个新的 useSpringVisibility React Hook,用于协调 React-Spring 动画回调与浏览器标签页可见性变化。该 Hook 被集成到 CenterPopup、Mask 和 Popup 三个弹窗组件中,解决了当页面在后台时 afterClose 回调未被触发导致组件无法卸载的问题。

当页面悄悄躲入后台,
弹窗的生命未完全,
新 Hook 如睿智的侦探,
监听窗帘拉开的瞬间,
确保每个 afterClose 的诺言——🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat: 为弹窗组件添加动画可见性控制" accurately summarizes the main change: adding animation visibility control to popup components.
Linked Issues check ✅ Passed The PR successfully addresses issue #6815 by implementing useSpringVisibility hook to ensure afterClose callbacks fire correctly when page visibility changes, fixing the cleanup issue.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing animation visibility control for popup components (CenterPopup, Mask, Popup) and the supporting useSpringVisibility hook with tests.

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

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

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@dosubot dosubot Bot added the feature label May 11, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

npm i https://pkg.pr.new/antd-mobile@7045

commit: 8826578

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.02%. Comparing base (b627e1f) to head (8826578).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7045      +/-   ##
==========================================
+ Coverage   92.97%   93.02%   +0.04%     
==========================================
  Files         337      338       +1     
  Lines        7373     7411      +38     
  Branches     1841     1883      +42     
==========================================
+ Hits         6855     6894      +39     
+ Misses        510      509       -1     
  Partials        8        8              

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

@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 a new useSpringVisibility hook to ensure that afterClose callbacks are reliably executed, even when page visibility changes during transitions. This hook is integrated into the CenterPopup, Mask, and Popup components, refining their animation onRest logic. Review feedback suggests optimizing the hook by memoizing the shouldCallAfterClose function with useCallback and using a useRef for the visible state to prevent redundant event listener re-attachments in the useEffect block.

Comment thread src/utils/use-spring-visibility.ts Outdated
@@ -0,0 +1,52 @@
import { useIsomorphicLayoutEffect } from 'ahooks'
import type { RefObject } from 'react'
import { useEffect, useRef } from 'react'
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.

medium

建议导入 useCallback 以便对 Hook 返回的函数进行记忆化处理,保持引用稳定。

Suggested change
import { useEffect, useRef } from 'react'
import { useCallback, useEffect, useRef } from 'react'

Comment thread src/utils/use-spring-visibility.ts Outdated
Comment on lines +31 to +43
useEffect(() => {
const handler = () => {
if (document.visibilityState !== 'visible') return
if (unmountedRef.current) return
if (!visible && activeRef.current && !closedRef.current) {
closedRef.current = true
setActive(false)
afterCloseRef.current?.()
}
}
document.addEventListener('visibilitychange', handler)
return () => document.removeEventListener('visibilitychange', handler)
}, [visible, setActive, unmountedRef])
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.

medium

建议使用 useRef 来追踪 visible 的最新值,并将 visibleuseEffect 的依赖数组中移除。这样可以避免在每次 visible 状态改变时都重新绑定和解绑 visibilitychange 事件监听器。对于全局事件监听器,保持监听函数的稳定是更好的实践,尤其是在组件频繁开关的情况下。

  const visibleRef = useRef(visible)
  visibleRef.current = visible

  useEffect(() => {
    const handler = () => {
      if (document.visibilityState !== 'visible') return
      if (unmountedRef.current) return
      if (!visibleRef.current && activeRef.current && !closedRef.current) {
        closedRef.current = true
        setActive(false)
        afterCloseRef.current?.()
      }
    }
    document.addEventListener('visibilitychange', handler)
    return () => document.removeEventListener('visibilitychange', handler)
  }, [setActive, unmountedRef])

Comment thread src/utils/use-spring-visibility.ts Outdated
Comment on lines +45 to +49
function shouldCallAfterClose(): boolean {
if (closedRef.current) return false
closedRef.current = true
return true
}
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.

medium

建议使用 useCallback 包裹 shouldCallAfterClose 函数。虽然目前组件中的 onRest 也是每次渲染重新创建的,但保持 Hook 返回的函数引用稳定有助于避免在 useSpring 内部产生不必要的更新逻辑。

Suggested change
function shouldCallAfterClose(): boolean {
if (closedRef.current) return false
closedRef.current = true
return true
}
const shouldCallAfterClose = useCallback((): boolean => {
if (closedRef.current) return false
closedRef.current = true
return true
}, [])

Copy link
Copy Markdown

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

Inline comments:
In `@src/utils/use-spring-visibility.ts`:
- Around line 31-43: The visibilitychange handler closes over a possibly stale
visible value; update the hook to track the latest visible in a ref (e.g.
visibleRef) and read visibleRef.current inside the handler instead of the
closed-over visible, and then re-register useEffect with only stable refs in its
dependency list (keep unmountedRef/activeRef/closedRef/afterCloseRef if they are
refs) so the listener is not needlessly reattached; ensure handler uses
visibleRef.current and calls setActive/afterCloseRef.current() as before.
🪄 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: 1204b9cb-8eba-4311-b7da-b22dcd76af86

📥 Commits

Reviewing files that changed from the base of the PR and between b627e1f and 20d84de.

📒 Files selected for processing (5)
  • src/components/center-popup/center-popup.tsx
  • src/components/mask/mask.tsx
  • src/components/popup/popup.tsx
  • src/utils/tests/use-spring-visibility.test.ts
  • src/utils/use-spring-visibility.ts

Comment thread src/utils/use-spring-visibility.ts Outdated
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels May 11, 2026
@zombieJ
Copy link
Copy Markdown
Member

zombieJ commented May 25, 2026

From CC:

核心思路是引入 useSpringVisibility:监听 visibilitychange,页面恢复可见时若 popup 已被关闭但 spring 因 rAF 节流没触发 onRest,就主动 setActive(false) + afterClose,再用 closedRef 在真正 onRest 时去重。能解决 #6815 的问题,整体方向 OK。下面是几点建议(按重要性排序):

1. 文件位置与命名

src/utils/use-spring-visibility.ts 起得太通用了。它实际上是「spring 动画 + active 状态 + afterClose 在 visibilitychange 时的兜底」三者耦合的一个小工具,非常专用。叫 useSpringResumeOnVisible 或者放到 src/components/popup/ 内部更合理;放在通用 utils 容易让人误以为是个独立 hook。

2. 只兜底了 afterClose,没兜底 afterShow

PR 描述写的是「修复页面不可见时动画暂停,并且切回来之后不继续执行」,听上去是双向的,但实现只处理了 !visible && active 的关闭兜底。打开过程中切走页面同样会卡住 afterShow,只是后果没那么严重(不会留空 div)。要么扩到对称处理,要么把描述改一下,避免误导。

3. hook 接口可以更简洁

现在同时传 active(state) 和 setActive,hook 内部又把 active 拷到 activeRef。其实 active 这个值没必要外部传——hook 里维护一个内部 ref 就行,或者改成 getActive: () => boolean 之类的形式。当前 API 让调用方多了一个心智负担(active 是 state,setActive 也是它的 setter,两者必须同步传)。

4. render 期间写 ref 的赋值

afterCloseRef.current = afterClose
activeRef.current = active
visibleRef.current = visible

这是常见模式,能 work,但严格来说在 concurrent 模式下并不完全安全(render 可能被丢弃)。仓库其它地方也有类似写法,不算新债,但建议加一句注释说明为什么不用 useEffect 同步。

5. 测试

  • 加了 hook 单测,覆盖度还可以。
  • 但没有针对 popup/center-popup/mask 三个组件本身的集成回归用例(对 页面不在前台时,Modal.close 不会销毁组件,导致页面部分区域无法点击 #6815 复现路径的端到端验证缺失)。改动既然落到三个组件 onRest 上,建议补一个 component-level 的测试,模拟 visibilitychange + visible=false 的组合,断言 wrapper div 真的消失。
  • it('should call setActive(false) ...') 那条用例里 const { result } = renderHook(...) 拿了 result 但没用,删掉。
  • document.visibilityState 的方式有点粗,建议封一个小 helper,多个用例复用,并放到 beforeEach 里 reset。

6. 一个小行为变化值得确认

原来 onRest 里是无条件 setActive(mergedProps.visible),现在拆成两支并把 close 支放在 shouldCallAfterClose() 后。如果 closedRef 已被 visibilitychange 置为 true,onRest 走的 close 分支整体被跳过,包括 setActive(false)。这没问题,因为 visibilitychange 那边已经做过了。但要确保未来改 shouldCallAfterClose 语义时不会破掉这一对约束——加一行注释说明「setActive 与 afterClose 必须成对触发,二者皆只触发一次」会更稳。

7. 视觉表现

visibilitychange 触发时直接 setActive(false),如果页面切回来时关闭动画还没播过(rAF 长期被节流),用户会看到 popup「啪」一下消失而不是淡出。close 场景下基本可接受,但如果以后想扩展处理 show 路径,需要考虑同样的视觉跳变。


整体方向 OK,可以合,但建议至少把 1(命名/位置)、3(接口)、5(补 component 级用例 + 删未用变量)处理掉再合。2 和 6 看作者取舍。

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

Labels

feature size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

页面不在前台时,Modal.close 不会销毁组件,导致页面部分区域无法点击

2 participants