From ada2c4e0721d70c0f5974895436a2155a945b8c2 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:51:20 -0500 Subject: [PATCH] feat: add `focusOnNavigation` flag to WebPreferences (#49512) * feat: add focusOnNavigation webPreference Co-authored-by: Kyle Cutler * WebContentsView tests Co-authored-by: Kyle Cutler * fix Co-authored-by: Kyle Cutler * fix Co-authored-by: Kyle Cutler --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Kyle Cutler --- docs/api/structures/web-preferences.md | 2 ++ .../browser/api/electron_api_web_contents.cc | 4 +++ shell/browser/web_contents_preferences.cc | 3 ++ shell/browser/web_contents_preferences.h | 2 ++ shell/common/options_switches.h | 3 ++ spec/api-web-contents-spec.ts | 29 ++++++++++++++++ spec/api-web-contents-view-spec.ts | 34 +++++++++++++++++++ 7 files changed, 77 insertions(+) diff --git a/docs/api/structures/web-preferences.md b/docs/api/structures/web-preferences.md index dab0dbcc7b..8ff30a21ae 100644 --- a/docs/api/structures/web-preferences.md +++ b/docs/api/structures/web-preferences.md @@ -156,6 +156,8 @@ `WebContents` when the preferred size changes. Default is `false`. * `transparent` boolean (optional) - Whether to enable background transparency for the guest page. Default is `true`. **Note:** The guest page's text and background colors are derived from the [color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme) of its root element. When transparency is enabled, the text color will still change accordingly but the background will remain transparent. * `enableDeprecatedPaste` boolean (optional) _Deprecated_ - Whether to enable the `paste` [execCommand](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand). Default is `false`. +* `focusOnNavigation` boolean (optional) - Whether to focus the WebContents + when navigating. Default is `true`. [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment [runtime-enabled-features]: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/runtime_enabled_features.json5 diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index e9b3dee458..0da9af8330 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -2065,6 +2065,10 @@ void WebContents::ReadyToCommitNavigation( // Only focus for top-level contents. if (type_ != Type::kBrowserWindow) return; + // Don't focus if focusOnNavigation is disabled. + auto* prefs = WebContentsPreferences::From(web_contents()); + if (prefs && !prefs->ShouldFocusOnNavigation()) + return; web_contents()->SetInitialFocus(); } diff --git a/shell/browser/web_contents_preferences.cc b/shell/browser/web_contents_preferences.cc index ca1172ef0e..f92fbadc32 100644 --- a/shell/browser/web_contents_preferences.cc +++ b/shell/browser/web_contents_preferences.cc @@ -149,6 +149,7 @@ void WebContentsPreferences::Clear() { preload_path_ = std::nullopt; v8_cache_options_ = blink::mojom::V8CacheOptions::kDefault; deprecated_paste_enabled_ = false; + focus_on_navigation_ = true; #if BUILDFLAG(IS_MAC) scroll_bounce_ = false; @@ -249,6 +250,8 @@ void WebContentsPreferences::SetFromDictionary( web_preferences.Get(options::kEnableDeprecatedPaste, &deprecated_paste_enabled_); + web_preferences.Get(options::kFocusOnNavigation, &focus_on_navigation_); + #if BUILDFLAG(IS_MAC) web_preferences.Get(options::kScrollBounce, &scroll_bounce_); #endif diff --git a/shell/browser/web_contents_preferences.h b/shell/browser/web_contents_preferences.h index 4bb6132752..b92f6165f2 100644 --- a/shell/browser/web_contents_preferences.h +++ b/shell/browser/web_contents_preferences.h @@ -78,6 +78,7 @@ class WebContentsPreferences bool ShouldDisablePopups() const { return disable_popups_; } bool IsWebSecurityEnabled() const { return web_security_; } std::optional GetPreloadPath() const { return preload_path_; } + bool ShouldFocusOnNavigation() const { return focus_on_navigation_; } bool IsSandboxed() const; private: @@ -134,6 +135,7 @@ class WebContentsPreferences std::optional preload_path_; blink::mojom::V8CacheOptions v8_cache_options_; bool deprecated_paste_enabled_ = false; + bool focus_on_navigation_; #if BUILDFLAG(IS_MAC) bool scroll_bounce_; diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index a6410bef40..1c7ca73416 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -222,6 +222,9 @@ inline constexpr std::string_view kSpellcheck = "spellcheck"; inline constexpr std::string_view kEnableDeprecatedPaste = "enableDeprecatedPaste"; +// Whether to focus the webContents on navigation. +inline constexpr std::string_view kFocusOnNavigation = "focusOnNavigation"; + inline constexpr std::string_view kModal = "modal"; } // namespace options diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index 695b3d3575..c587a795a1 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/api-web-contents-spec.ts @@ -1610,6 +1610,35 @@ describe('webContents module', () => { await expect(blurPromise).to.eventually.be.fulfilled(); }); }); + + describe('focusOnNavigation webPreference', () => { + afterEach(closeAllWindows); + + it('focuses the webContents on navigation by default', async () => { + const w = new BrowserWindow({ show: true }); + await once(w, 'focus'); + await w.loadURL('about:blank'); + await moveFocusToDevTools(w); + expect(w.webContents.isFocused()).to.be.false(); + await w.loadURL('data:text/html,test'); + expect(w.webContents.isFocused()).to.be.true(); + }); + + it('does not focus the webContents on navigation when focusOnNavigation is false', async () => { + const w = new BrowserWindow({ + show: true, + webPreferences: { + focusOnNavigation: false + } + }); + await once(w, 'focus'); + await w.loadURL('about:blank'); + await moveFocusToDevTools(w); + expect(w.webContents.isFocused()).to.be.false(); + await w.loadURL('data:text/html,test'); + expect(w.webContents.isFocused()).to.be.false(); + }); + }); }); describe('getOSProcessId()', () => { diff --git a/spec/api-web-contents-view-spec.ts b/spec/api-web-contents-view-spec.ts index 3c306a32d8..af389d7106 100644 --- a/spec/api-web-contents-view-spec.ts +++ b/spec/api-web-contents-view-spec.ts @@ -398,4 +398,38 @@ describe('WebContentsView', () => { v.setBorderRadius(100); }); }); + + describe('focusOnNavigation webPreference', () => { + it('focuses the webContents on navigation by default', async () => { + const w = new BrowserWindow(); + await once(w, 'focus'); + const v = new WebContentsView(); + w.setContentView(v); + await v.webContents.loadURL('about:blank'); + const devToolsFocused = once(v.webContents, 'devtools-focused'); + v.webContents.openDevTools({ mode: 'right' }); + await devToolsFocused; + expect(v.webContents.isFocused()).to.be.false(); + await v.webContents.loadURL('data:text/html,test'); + expect(v.webContents.isFocused()).to.be.true(); + }); + + it('does not focus the webContents on navigation when focusOnNavigation is false', async () => { + const w = new BrowserWindow(); + await once(w, 'focus'); + const v = new WebContentsView({ + webPreferences: { + focusOnNavigation: false + } + }); + w.setContentView(v); + await v.webContents.loadURL('about:blank'); + const devToolsFocused = once(v.webContents, 'devtools-focused'); + v.webContents.openDevTools({ mode: 'right' }); + await devToolsFocused; + expect(v.webContents.isFocused()).to.be.false(); + await v.webContents.loadURL('data:text/html,test'); + expect(v.webContents.isFocused()).to.be.false(); + }); + }); });