commit 84d778b410212f9652ad116983e7790dc8aeb14b
parent f3ee8157720b30fa344aa80b6e09be42a13d792b
Author: Stephen Thompson <sthompson@mozilla.com>
Date: Wed, 8 Oct 2025 16:20:06 +0000
Bug 1984234 - settings UI to toggle opening external links next to the active tab r=fluent-reviewers,mstriemer,bolsson
Add a new checkbox to the General > Tabs section of about:preferences to allow the user to open external links next to the active tab.
The current behavior defaults to looking up `browser.link.open_newwindow.override.external`, and if that's the default of `-1`, it falls back to `browser.link.open_newwindow` which defaults to `3` (open a new tab at the end of the tab strip).
We want the settings UI to be simple, so it's a checkbox to set the value of `browser.link.open_newwindow.override.external` to `7`, which indicates opening external links next to the active tab. However, when the user unchecks the box, the value returns to the browser default `-1` to force `browser.link.open_newwindow.override.external` back to the default `-1` rather than returning to any value that the user might have explicitly set before in about:config.
Differential Revision: https://phabricator.services.mozilla.com/D266734
Diffstat:
6 files changed, 133 insertions(+), 0 deletions(-)
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
@@ -77,6 +77,9 @@
<checkbox id="linkTargeting" data-l10n-id="open-new-link-as-tabs"
preference="browser.link.open_newwindow"/>
+ <checkbox id="openAppLinksNextToActiveTab" data-l10n-id="open-external-link-next-to-active-tab"
+ preference="browser.link.open_newwindow.override.external"/>
+
<checkbox id="warnOpenMany" data-l10n-id="warn-on-open-many-tabs"
preference="browser.tabs.warnOnOpen"/>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
@@ -52,6 +52,9 @@ const ICON_URL_APP =
// was set by us to a custom handler icon and CSS should not try to override it.
const APP_ICON_ATTR_NAME = "appHandlerIcon";
+const OPEN_EXTERNAL_LINK_NEXT_TO_ACTIVE_TAB_VALUE =
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT;
+
Preferences.addAll([
// Startup
{ id: "browser.startup.page", type: "int" },
@@ -73,6 +76,10 @@ Preferences.addAll([
1 opens such links in the most recent window or tab,
2 opens such links in a new window,
3 opens such links in a new tab
+ browser.link.open_newwindow.override.external
+ - this setting overrides `browser.link.open_newwindow` for externally
+ opened links.
+ - see `nsIBrowserDOMWindow` constants for the meaning of each value.
browser.tabs.loadInBackground
- true if display should switch to a new tab which has been opened from a
link, false if display shouldn't switch
@@ -89,6 +96,7 @@ Preferences.addAll([
*/
{ id: "browser.link.open_newwindow", type: "int" },
+ { id: "browser.link.open_newwindow.override.external", type: "int" },
{ id: "browser.tabs.loadInBackground", type: "bool", inverted: true },
{ id: "browser.tabs.warnOnClose", type: "bool" },
{ id: "browser.warnOnQuitShortcut", type: "bool" },
@@ -1647,6 +1655,11 @@ var gMainPane = {
* Initialization of gMainPane.
*/
init() {
+ /**
+ * @param {string} aId
+ * @param {string} aEventType
+ * @param {(ev: Event) => void} aCallback
+ */
function setEventListener(aId, aEventType, aCallback) {
document
.getElementById(aId)
@@ -2033,6 +2046,14 @@ var gMainPane = {
() => this.writeLinkTarget()
);
Preferences.addSyncFromPrefListener(
+ document.getElementById("openAppLinksNextToActiveTab"),
+ () => this.readExternalLinkNextToActiveTab()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("openAppLinksNextToActiveTab"),
+ inputElement => this.writeExternalLinkNextToActiveTab(inputElement)
+ );
+ Preferences.addSyncFromPrefListener(
document.getElementById("browserContainersCheckbox"),
() => this.readBrowserContainersCheckbox()
);
@@ -2933,6 +2954,44 @@ var gMainPane = {
},
/**
+ * @returns {boolean}
+ * Whether the "Open links in tabs instead of new windows" settings
+ * checkbox should be checked. Should only be checked if the
+ * `browser.link.open_newwindow.override.external` pref is set to the
+ * value of 7 (nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT).
+ */
+ readExternalLinkNextToActiveTab() {
+ const externalLinkOpenOverride = Preferences.get(
+ "browser.link.open_newwindow.override.external"
+ );
+
+ return (
+ externalLinkOpenOverride.value ==
+ Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT
+ );
+ },
+
+ /**
+ * This pref has at least 8 valid values but we are offering a checkbox
+ * to set one specific value (`7`).
+ *
+ * @param {HTMLInputElement} inputElement
+ * @returns {number}
+ * - `7` (`nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT`) if checked
+ * - the default value of
+ * `browser.link.open_newwindow.override.external` if not checked
+ */
+ writeExternalLinkNextToActiveTab(inputElement) {
+ const externalLinkOpenOverride = Preferences.get(
+ "browser.link.open_newwindow.override.external"
+ );
+
+ return inputElement.checked
+ ? Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT
+ : externalLinkOpenOverride.defaultValue;
+ },
+
+ /**
* Shows a subdialog containing the profile selector page.
*/
manageProfiles() {
diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml
@@ -308,6 +308,8 @@ support-files = [
["browser_sync_pairing.js"]
+["browser_tabs_open_external_next_to_active_tab.js"]
+
["browser_trendingsuggestions.js"]
["browser_usage_telemetry.js"]
diff --git a/browser/components/preferences/tests/browser_tabs_open_external_next_to_active_tab.js b/browser/components/preferences/tests/browser_tabs_open_external_next_to_active_tab.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_NAME = "browser.link.open_newwindow.override.external";
+const PREF_VALUE_FEATURE_OFF = Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND;
+const PREF_VALUE_FEATURE_ON = Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_AFTER_CURRENT;
+
+add_task(async function test_open_external_link_next_to_active_tab_pref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_NAME, PREF_VALUE_FEATURE_OFF]],
+ });
+
+ await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+
+ const doc = gBrowser.contentDocument;
+ const checkbox = doc.getElementById("openAppLinksNextToActiveTab");
+
+ info("validate default starting conditions");
+ Assert.ok(checkbox, "pref should have a checkbox");
+ Assert.ok(
+ !checkbox.checked,
+ "pref checkbox should not be checked by default"
+ );
+
+ info(
+ "validate checking and unchecking the pref checkbox under default conditions"
+ );
+ let becameChecked = BrowserTestUtils.waitForAttribute("checked", checkbox);
+ checkbox.click();
+ await becameChecked;
+
+ Assert.equal(
+ Services.prefs.getIntPref(PREF_NAME),
+ PREF_VALUE_FEATURE_ON,
+ "pref should be set to open external links after the active tab"
+ );
+
+ let becameUnchecked = BrowserTestUtils.waitForAttributeRemoval(
+ "checked",
+ checkbox
+ );
+ checkbox.click();
+ await becameUnchecked;
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(PREF_NAME),
+ "pref should have been reverted to its default value"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab, { animate: false });
+});
diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -188,6 +188,9 @@ open-new-link-as-tabs =
.label = Open links in tabs instead of new windows
.accesskey = w
+open-external-link-next-to-active-tab =
+ .label = Open links from apps next to your active tab
+
ask-on-close-multiple-tabs =
.label = Ask before closing multiple tabs
.accesskey = m
diff --git a/toolkit/content/preferences/Preferences.mjs b/toolkit/content/preferences/Preferences.mjs
@@ -415,9 +415,15 @@ export const Preferences = {
}
},
+ /** @type {WeakMap<Element, (el: Element) => any>} */
_syncFromPrefListeners: new WeakMap(),
+ /** @type {WeakMap<Element, (el: Element) => any>} */
_syncToPrefListeners: new WeakMap(),
+ /**
+ * @param {Element} aElement
+ * @param {(el: Element) => any} callback
+ */
addSyncFromPrefListener(aElement, callback) {
this._syncFromPrefListeners.set(aElement, callback);
if (this.updateQueued) {
@@ -433,6 +439,10 @@ export const Preferences = {
}
},
+ /**
+ * @param {Element} aElement
+ * @param {(el: Element) => any} callback
+ */
addSyncToPrefListener(aElement, callback) {
this._syncToPrefListeners.set(aElement, callback);
if (this.updateQueued) {