Skip to content

fix(history): remove visibilitychange listener to prevent focus steal…#2704

Open
Mini-Sylar wants to merge 1 commit into
vuejs:mainfrom
Mini-Sylar:fix/visibilitychange-focus-steal
Open

fix(history): remove visibilitychange listener to prevent focus steal…#2704
Mini-Sylar wants to merge 1 commit into
vuejs:mainfrom
Mini-Sylar:fix/visibilitychange-focus-steal

Conversation

@Mini-Sylar
Copy link
Copy Markdown

@Mini-Sylar Mini-Sylar commented May 8, 2026

Summary

Fixes a regression introduced in Fixes a regression introduced in #2537 - discussed in #2693. where adding a visibilitychange listener causes Edge on macOS (and Windows) to steal window focus during development.

Root Cause

visibilitychange was firing too broadly even when a tab goes to the background, including during Vite HMR updates on a background tab. When that happens, history.replaceState() is called, and Edge on macOS activates the window at the OS level in response. Chrome is unaffected by the same call.

Fix

Remove the visibilitychange listener and rely solely on pagehide, which:

Testing

  • iOS Safari scroll restoration: confirmed working ✅
  • Edge focus steal on macOS: confirmed fixed ✅

Summary by CodeRabbit

  • Bug Fixes
    • Improved scroll position persistence when navigating away from pages. The router now more reliably saves and restores scroll state during page transitions.

… in Edge

Fixes focus stealing in edge, does not affect Safari IOS scroll position
@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit bb21b7c
🔍 Latest deploy log https://app.netlify.com/projects/vue-router/deploys/69fdb6bcd05b8d000854d50f

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This change simplifies scroll state persistence in the Vue Router's HTML5 history implementation. The beforeUnloadListener now unconditionally persists scroll position to history.state on pagehide events, removing the prior visibility-state conditional check. The associated visibilitychange event listener and its cleanup were removed, leaving destroy() to only unregister popstate and pagehide listeners.

Changes

Scroll State Persistence Simplification

Layer / File(s) Summary
Listener Logic Update
packages/router/src/history/html5.ts
beforeUnloadListener unconditionally calls history.replaceState with computed scroll position on pagehide, removing the document.visibilityState guard and visibilitychange event wiring.
Cleanup Logic Update
packages/router/src/history/html5.ts
destroy() removes only popstate and pagehide listeners; the visibilitychange listener unregistration is eliminated.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through history's scroll,
No more the visibility's role,
Just pagehide and state so neat,
Persistence now—simple and fleet! 🐰

🚥 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 clearly and specifically describes the main change: removing the visibilitychange listener to fix the focus-steal issue, which directly matches the primary objective and code changes.
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.

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

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

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.

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)
packages/router/src/history/html5.ts (1)

129-129: 💤 Low value

Optional rename: beforeUnloadListenerpageHideListener.

The function is bound to pagehide, not beforeunload. The name is a pre-existing artefact but now that the listener is the only scroll-persistence handler, the mismatch is a bit more prominent.

♻️ Proposed rename
-  function beforeUnloadListener() {
+  function pageHideListener() {
     const { history } = window
     if (!history.state) return
     history.replaceState(
       assign({}, history.state, { scroll: computeScrollPosition() }),
       ''
     )
   }

   function destroy() {
     for (const teardown of teardowns) teardown()
     teardowns = []
     window.removeEventListener('popstate', popStateHandler)
-    window.removeEventListener('pagehide', beforeUnloadListener)
+    window.removeEventListener('pagehide', pageHideListener)
   }

   // ...
-  window.addEventListener('pagehide', beforeUnloadListener)
+  window.addEventListener('pagehide', pageHideListener)
🤖 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 `@packages/router/src/history/html5.ts` at line 129, Rename the function
beforeUnloadListener to pageHideListener to match its actual usage (it is bound
to the "pagehide" event and is the only scroll-persistence handler); update the
function declaration (beforeUnloadListener) and all references where it's
added/removed as an event listener (e.g., addEventListener("pagehide", ...) and
removeEventListener calls) to the new name so identifiers remain consistent and
descriptive.
🤖 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 `@packages/router/src/history/html5.ts`:
- Line 129: Rename the function beforeUnloadListener to pageHideListener to
match its actual usage (it is bound to the "pagehide" event and is the only
scroll-persistence handler); update the function declaration
(beforeUnloadListener) and all references where it's added/removed as an event
listener (e.g., addEventListener("pagehide", ...) and removeEventListener calls)
to the new name so identifiers remain consistent and descriptive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c1b8256-e3dc-43b6-9155-274cd48759c9

📥 Commits

Reviewing files that changed from the base of the PR and between af77a7c and bb21b7c.

📒 Files selected for processing (1)
  • packages/router/src/history/html5.ts

@posva posva moved this to 📆 Planned in Vue Router Roadmap May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📆 Planned

Development

Successfully merging this pull request may close these issues.

2 participants