commit 779e85fffe11ef045e40c0448fe6ab25b71f1739
parent 0a7c1df529aac9bcd0a782642e8dd748d18285c3
Author: Benjamin VanderSloot <bvandersloot@mozilla.com>
Date: Sun, 16 Nov 2025 13:42:39 +0000
Bug 1971428 - Convert DoH to config-based prefs - r=valentin,fluent-reviewers,desktop-theme-reviewers,hjones,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D269262
Diffstat:
11 files changed, 656 insertions(+), 25 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -2822,6 +2822,9 @@ pref("browser.screenshots.dir", "");
// DoH Rollout: whether to clear the mode value at shutdown.
pref("doh-rollout.clearModeOnShutdown", false);
+// DoH UI: default the fallback checkbox to on.
+pref("network.trr_ui.fallback_was_checked", true);
+
// Normandy client preferences
pref("app.normandy.api_url", "https://normandy.cdn.mozilla.net/api/v1");
pref("app.normandy.dev_mode", false);
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
@@ -1995,6 +1995,84 @@ let SETTINGS_CONFIG = {
},
],
},
+ dnsOverHttps: {
+ inProgress: true,
+ items: [
+ {
+ id: "dohBox",
+ control: "moz-box-group",
+ items: [
+ {
+ id: "dohModeBoxItem",
+ control: "moz-box-item",
+ },
+ {
+ id: "dohAdvancedButton",
+ l10nId: "preferences-doh-advanced-button",
+ control: "moz-box-button",
+ },
+ ],
+ },
+ ],
+ },
+ dnsOverHttpsAdvanced: {
+ inProgress: true,
+ l10nId: "preferences-doh-advanced-section",
+ supportPage: "dns-over-https",
+ headingLevel: 2,
+ items: [
+ {
+ id: "dohStatusBox",
+ control: "moz-message-bar",
+ },
+ {
+ id: "dohRadioGroup",
+ control: "moz-radio-group",
+ options: [
+ {
+ id: "dohRadioDefault",
+ value: "default",
+ l10nId: "preferences-doh-radio-default",
+ },
+ {
+ id: "dohRadioCustom",
+ value: "custom",
+ l10nId: "preferences-doh-radio-custom",
+ items: [
+ {
+ id: "dohFallbackIfCustom",
+ l10nId: "preferences-doh-fallback-label",
+ },
+ {
+ id: "dohProviderSelect",
+ l10nId: "preferences-doh-select-resolver-label",
+ control: "moz-select",
+ },
+ {
+ id: "dohCustomProvider",
+ control: "moz-input-text",
+ l10nId: "preferences-doh-custom-provider-label",
+ },
+ ],
+ },
+ {
+ id: "dohRadioOff",
+ value: "off",
+ l10nId: "preferences-doh-radio-off",
+ },
+ ],
+ },
+ {
+ id: "dohExceptionsButton",
+ l10nId: "preferences-doh-manage-exceptions2",
+ control: "moz-box-button",
+ controlAttrs: {
+ "search-l10n-ids":
+ "permissions-doh-entry-field,permissions-doh-add-exception.label,permissions-doh-remove.label,permissions-doh-remove-all.label,permissions-exceptions-doh-window.title,permissions-exceptions-manage-doh-desc,",
+ },
+ },
+ ],
+ },
};
/**
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
@@ -172,6 +172,11 @@ const CONFIG_PANES = {
l10nId: "containers-section-header",
groupIds: ["containers"],
},
+ dnsOverHttps: {
+ parent: "privacy",
+ l10nId: "preferences-doh-header2",
+ groupIds: ["dnsOverHttpsAdvanced"],
+ },
};
var gLastCategory = { category: undefined, subcategory: undefined };
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
@@ -576,7 +576,7 @@
class="subcategory"
hidden="true"
data-category="panePrivacy">
- <html:h1 data-l10n-id="permissions-header"/>
+ <html:h1 data-l10n-id="permissions-header2"/>
</hbox>
<!-- Permissions -->
@@ -740,7 +740,8 @@
<html:h1 data-l10n-id="preferences-doh-header"/>
</hbox>
-<groupbox id="dohBox" data-category="panePrivacy" data-subcategory="doh" hidden="true" class="highlighting-group">
+<html:setting-group hidden="true" data-category="panePrivacy" groupid="dnsOverHttps" />
+<groupbox id="dohBox" data-category="panePrivacy" data-subcategory="doh" hidden="true" class="highlighting-group" data-srd-groupid="dnsOverHttps">
<label class="search-header" searchkeywords="doh trr" hidden="true"><html:h2 data-l10n-id="preferences-doh-header"/></label>
<vbox flex="1">
<description id="dohDescription" class="tail-with-learn-more description-deemphasized" data-l10n-id="preferences-doh-description2"></description>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
@@ -279,6 +279,7 @@ Preferences.addAll([
{ id: "network.trr.uri", type: "string" },
{ id: "network.trr.default_provider_uri", type: "string" },
{ id: "network.trr.custom_uri", type: "string" },
+ { id: "network.trr_ui.fallback_was_checked", type: "bool" },
{ id: "doh-rollout.disable-heuristics", type: "bool" },
// Local Network Access
@@ -2203,6 +2204,410 @@ Preferences.addSetting({
onUserClick: () => gPrivacyPane.showXRExceptions(),
});
+Preferences.addSetting({
+ id: "dohBox",
+});
+
+Preferences.addSetting({
+ id: "dohAdvancedButton",
+ onUserClick(e) {
+ e.preventDefault();
+ gotoPref("paneDnsOverHttps");
+ },
+});
+
+Preferences.addSetting({
+ id: "dohExceptionsButton",
+ onUserClick: () => gPrivacyPane.showDoHExceptions(),
+});
+
+Preferences.addSetting({
+ id: "dohMode",
+ pref: "network.trr.mode",
+ setup(emitChange) {
+ Services.obs.addObserver(emitChange, "network:trr-mode-changed");
+ Services.obs.addObserver(emitChange, "network:trr-confirmation");
+ return () => {
+ Services.obs.removeObserver(emitChange, "network:trr-mode-changed");
+ Services.obs.removeObserver(emitChange, "network:trr-confirmation");
+ };
+ },
+});
+
+Preferences.addSetting({
+ id: "dohURL",
+ pref: "network.trr.uri",
+ setup(emitChange) {
+ Services.obs.addObserver(emitChange, "network:trr-uri-changed");
+ Services.obs.addObserver(emitChange, "network:trr-confirmation");
+ return () => {
+ Services.obs.removeObserver(emitChange, "network:trr-uri-changed");
+ Services.obs.removeObserver(emitChange, "network:trr-confirmation");
+ };
+ },
+});
+
+Preferences.addSetting({
+ id: "dohDefaultURL",
+ pref: "network.trr.default_provider_uri",
+});
+
+Preferences.addSetting({
+ id: "dohDisableHeuristics",
+ pref: "doh-rollout.disable-heuristics",
+});
+
+Preferences.addSetting({
+ id: "dohModeBoxItem",
+ deps: ["dohMode"],
+ getControlConfig: (config, deps) => {
+ let l10nId = "preferences-doh-overview-off";
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_NATIVEONLY) {
+ l10nId = "preferences-doh-overview-default";
+ } else if (
+ deps.dohMode.value == Ci.nsIDNSService.MODE_TRRFIRST ||
+ deps.dohMode.value == Ci.nsIDNSService.MODE_TRRONLY
+ ) {
+ l10nId = "preferences-doh-overview-custom";
+ }
+ return {
+ ...config,
+ l10nId,
+ };
+ },
+});
+
+Preferences.addSetting({
+ id: "dohStatusBox",
+ deps: ["dohMode", "dohURL"],
+ getControlConfig: config => {
+ let l10nId = "preferences-doh-status-item-off";
+ let l10nArgs = {};
+ let supportPage = "";
+ let controlAttrs = { type: "info" };
+
+ let trrURI = Services.dns.currentTrrURI;
+ let hostname = URL.parse(trrURI)?.hostname;
+
+ let name = hostname || trrURI;
+ let nameFound = false;
+ let steering = false;
+ for (let resolver of DoHConfigController.currentConfig.providerList) {
+ if (resolver.uri == trrURI) {
+ name = resolver.UIName || name;
+ nameFound = true;
+ break;
+ }
+ }
+ if (!nameFound) {
+ for (let resolver of DoHConfigController.currentConfig.providerSteering
+ .providerList) {
+ if (resolver.uri == trrURI) {
+ steering = true;
+ name = resolver.UIName || name;
+ break;
+ }
+ }
+ }
+
+ let mode = Services.dns.currentTrrMode;
+ if (
+ (mode == Ci.nsIDNSService.MODE_TRRFIRST ||
+ mode == Ci.nsIDNSService.MODE_TRRONLY) &&
+ lazy.gParentalControlsService?.parentalControlsEnabled
+ ) {
+ l10nId = "preferences-doh-status-item-not-active";
+ supportPage = "doh-status";
+ l10nArgs = {
+ reason: Services.dns.getTRRSkipReasonName(
+ Ci.nsITRRSkipReason.TRR_PARENTAL_CONTROL
+ ),
+ name,
+ };
+ } else {
+ let confirmationState = Services.dns.currentTrrConfirmationState;
+ if (
+ mode != Ci.nsIDNSService.MODE_TRRFIRST &&
+ mode != Ci.nsIDNSService.MODE_TRRONLY
+ ) {
+ l10nId = "preferences-doh-status-item-off";
+ } else if (
+ confirmationState == Ci.nsIDNSService.CONFIRM_TRYING_OK ||
+ confirmationState == Ci.nsIDNSService.CONFIRM_OK ||
+ confirmationState == Ci.nsIDNSService.CONFIRM_DISABLED
+ ) {
+ if (steering) {
+ l10nId = "preferences-doh-status-item-active-local";
+ controlAttrs = { type: "success" };
+ } else {
+ l10nId = "preferences-doh-status-item-active";
+ controlAttrs = { type: "success" };
+ }
+ } else if (steering) {
+ l10nId = "preferences-doh-status-item-not-active-local";
+ supportPage = "doh-status";
+ controlAttrs = { type: "warning" };
+ } else {
+ l10nId = "preferences-doh-status-item-not-active";
+ supportPage = "doh-status";
+ controlAttrs = { type: "warning" };
+ }
+
+ let confirmationStatus = Services.dns.lastConfirmationStatus;
+ if (confirmationStatus != Cr.NS_OK) {
+ l10nArgs = {
+ reason: ChromeUtils.getXPCOMErrorName(confirmationStatus),
+ name,
+ };
+ } else {
+ l10nArgs = {
+ reason: Services.dns.getTRRSkipReasonName(
+ Services.dns.lastConfirmationSkipReason
+ ),
+ name,
+ };
+ if (
+ Services.dns.lastConfirmationSkipReason ==
+ Ci.nsITRRSkipReason.TRR_BAD_URL ||
+ !name
+ ) {
+ l10nId = "preferences-doh-status-item-not-active-bad-url";
+ supportPage = "doh-status";
+ controlAttrs = { type: "warning" };
+ }
+ }
+ }
+
+ return {
+ ...config,
+ l10nId,
+ l10nArgs,
+ supportPage,
+ controlAttrs,
+ };
+ },
+});
+
+Preferences.addSetting({
+ id: "dohRadioGroup",
+ // These deps are complicated:
+ // this radio group, along with dohFallbackIfCustom controls the mode and URL.
+ // Therefore, we set dohMode and dohURL as deps here. This is a smell, but needed
+ // for the mismatch of control-to-pref.
+ deps: ["dohFallbackIfCustom", "dohMode", "dohURL"],
+ onUserChange: (val, deps) => {
+ let value = null;
+ if (val == "default") {
+ value = "dohDefaultRadio";
+ } else if (val == "off") {
+ value = "dohOffRadio";
+ } else if (val == "custom" && deps.dohFallbackIfCustom.value) {
+ value = "dohEnabledRadio";
+ } else if (val == "custom" && !deps.dohFallbackIfCustom.value) {
+ value = "dohStrictRadio";
+ }
+ if (value) {
+ Glean.securityDohSettings.modeChangedButton.record({
+ value,
+ });
+ }
+ },
+ get: (_val, deps) => {
+ switch (deps.dohMode.value) {
+ case Ci.nsIDNSService.MODE_NATIVEONLY:
+ return "default";
+ case Ci.nsIDNSService.MODE_TRRFIRST:
+ case Ci.nsIDNSService.MODE_TRRONLY:
+ return "custom";
+ case Ci.nsIDNSService.MODE_TRROFF:
+ case Ci.nsIDNSService.MODE_RESERVED1:
+ case Ci.nsIDNSService.MODE_RESERVED4:
+ default:
+ return "off";
+ }
+ },
+ set: (val, deps) => {
+ if (val == "custom") {
+ if (deps.dohFallbackIfCustom.value) {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_TRRFIRST;
+ } else {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_TRRONLY;
+ }
+ } else if (val == "off") {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_TRROFF;
+ } else {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_NATIVEONLY;
+ }
+
+ // When the mode is set to 0 we need to clear the URI so
+ // doh-rollout can kick in.
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_NATIVEONLY) {
+ deps.dohURL.pref.value = undefined;
+ Services.prefs.clearUserPref("doh-rollout.disable-heuristics");
+ }
+
+ // Bug 1861285
+ // When the mode is set to 2 or 3, we need to check if network.trr.uri is a empty string.
+ // In this case, we need to update network.trr.uri to default to fallbackProviderURI.
+ // This occurs when the mode is previously set to 0 (Default Protection).
+ if (
+ deps.dohMode.value == Ci.nsIDNSService.MODE_TRRFIRST ||
+ deps.dohMode.value == Ci.nsIDNSService.MODE_TRRONLY
+ ) {
+ if (!deps.dohURL.value) {
+ deps.dohURL.value =
+ DoHConfigController.currentConfig.fallbackProviderURI;
+ }
+ }
+
+ // Bug 1900672
+ // When the mode is set to 5, clear the pref to ensure that
+ // network.trr.uri is set to fallbackProviderURIwhen the mode is set to 2 or 3 afterwards
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_TRROFF) {
+ deps.dohURL.pref.value = undefined;
+ }
+ },
+});
+
+Preferences.addSetting({
+ id: "dohFallbackIfCustom",
+ pref: "network.trr_ui.fallback_was_checked",
+ // These deps are complicated:
+ // this checkbox, along with dohRadioGroup controls the mode and URL.
+ // Therefore, we set dohMode as a dep here. This is a smell, but needed
+ // for the mismatch of control-to-pref.
+ deps: ["dohMode"],
+ onUserChange: val => {
+ if (val) {
+ Glean.securityDohSettings.modeChangedButton.record({
+ value: "dohEnabledRadio",
+ });
+ } else {
+ Glean.securityDohSettings.modeChangedButton.record({
+ value: "dohStrictRadio",
+ });
+ }
+ },
+ get: (val, deps) => {
+ // If we are in a custom mode, we need to get the value from the Setting
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_TRRFIRST) {
+ return true;
+ }
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_TRRONLY) {
+ return false;
+ }
+
+ // Propagate the preference otherwise
+ return val;
+ },
+ set: (val, deps) => {
+ // Toggle the preference that controls the setting if are in a custom mode
+ // This should be the only case where the checkbox is enabled, but we can be
+ // careful and test.
+ if (deps.dohMode.value == Ci.nsIDNSService.MODE_TRRFIRST && !val) {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_TRRONLY;
+ } else if (deps.dohMode.value == Ci.nsIDNSService.MODE_TRRONLY && val) {
+ deps.dohMode.value = Ci.nsIDNSService.MODE_TRRFIRST;
+ }
+ // Propagate to the real preference
+ return val;
+ },
+});
+
+Preferences.addSetting({
+ id: "dohCustomProvider",
+ deps: ["dohProviderSelect", "dohURL"],
+ _value: null,
+ visible: deps => {
+ return deps.dohProviderSelect.value == "custom";
+ },
+ get(_val, deps) {
+ if (this._value === null) {
+ return deps.dohURL.value;
+ }
+ return this._value;
+ },
+ set(val, deps) {
+ this._value = val;
+ if (val == "") {
+ val = " ";
+ }
+ deps.dohURL.value = val;
+ },
+});
+
+Preferences.addSetting({
+ id: "dohProviderSelect",
+ deps: ["dohURL", "dohDefaultURL"],
+ _custom: false,
+ onUserChange: value => {
+ Glean.securityDohSettings.providerChoiceValue.record({
+ value,
+ });
+ },
+ getControlConfig(config, deps) {
+ let options = [];
+
+ let resolvers = DoHConfigController.currentConfig.providerList;
+ // if there's no default, we'll hold its position with an empty string
+ let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI;
+ let defaultFound = resolvers.some(p => p.uri == defaultURI);
+ if (!defaultFound && defaultURI) {
+ // the default value for the pref isn't included in the resolvers list
+ // so we'll make a stub for it. Without an id, we'll have to use the url as the label
+ resolvers.unshift({ uri: defaultURI });
+ }
+ let currentURI = deps.dohURL.value;
+ if (currentURI && !resolvers.some(p => p.uri == currentURI)) {
+ this._custom = true;
+ }
+
+ options = resolvers.map(resolver => {
+ let option = {
+ value: resolver.uri,
+ l10nArgs: {
+ name: resolver.UIName || resolver.uri,
+ },
+ };
+ if (resolver.uri == defaultURI) {
+ option.l10nId = "connection-dns-over-https-url-item-default";
+ } else {
+ option.l10nId = "connection-dns-over-https-url-item";
+ }
+ return option;
+ });
+ options.push({
+ value: "custom",
+ l10nId: "connection-dns-over-https-url-custom",
+ });
+
+ return {
+ options,
+ ...config,
+ };
+ },
+ get(_val, deps) {
+ if (this._custom) {
+ return "custom";
+ }
+ let currentURI = deps.dohURL.value;
+ if (!currentURI) {
+ currentURI = deps.dohDefaultURL.value;
+ }
+ return currentURI;
+ },
+ set(val, deps, setting) {
+ if (val != "custom") {
+ this._custom = false;
+ deps.dohURL.value = val;
+ } else {
+ this._custom = true;
+ }
+ setting.emit("change");
+ return val;
+ },
+});
+
function setEventListener(aId, aEventType, aCallback) {
document
.getElementById(aId)
@@ -2762,6 +3167,8 @@ var gPrivacyPane = {
initSettingGroup("ipprotection");
initSettingGroup("history");
initSettingGroup("permissions");
+ initSettingGroup("dnsOverHttps");
+ initSettingGroup("dnsOverHttpsAdvanced");
/* Initialize Content Blocking */
this.initContentBlocking();
@@ -2857,13 +3264,6 @@ var gPrivacyPane = {
setSyncFromPrefListener("savePasswords", () => this.readSavePasswords());
- setSyncFromPrefListener("popupPolicy", () =>
- this.updateButtons("popupPolicyButton", "dom.disable_open_during_load")
- );
- setSyncFromPrefListener("warnAddonInstall", () =>
- this.readWarnAddonInstall()
- );
-
if (AlertsServiceDND) {
let notificationsDoNotDisturbBox = document.getElementById(
"notificationsDoNotDisturbBox"
diff --git a/browser/components/preferences/widgets/setting-element/setting-element.mjs b/browser/components/preferences/widgets/setting-element/setting-element.mjs
@@ -114,7 +114,7 @@ export class SettingElement extends MozLitElement {
"data-subcategory": config.subcategory,
...config.controlAttrs,
};
- if (config.supportPage) {
+ if (config.supportPage != undefined) {
result[".supportPage"] = config.supportPage;
}
if (config.l10nId) {
diff --git a/browser/locales-preview/privacyPreferences.ftl b/browser/locales-preview/privacyPreferences.ftl
@@ -131,3 +131,80 @@ security-privacy-issue-warning-inner-html-ltgt =
security-privacy-issue-warning-file-uri-origin =
.label = File URI strict origin policy is disabled
.description = Files loaded in { -brand-short-name } should be cross-origin from files in the same folder
+
+## DNS-Over-HTTPS
+
+preferences-doh-overview-default =
+ .label = Default
+ .description = Use secure DNS in regions where it’s available
+
+preferences-doh-overview-custom =
+ .label = Custom
+ .description = You control when to use secure DNS and choose your provider.
+
+preferences-doh-overview-off =
+ .label = Off
+ .description = Use your default DNS resolver
+
+preferences-doh-advanced-button =
+ .label = Advanced settings
+
+preferences-doh-advanced-section =
+ .label = Advanced settings
+ .description = Domain Name System (DNS) over HTTPS sends your request for a domain name through an encrypted connection, providing a secure DNS and making it harder for others to see which website you’re about to access.
+
+preferences-doh-manage-exceptions2 =
+ .label = Manage exceptions
+ .accesskey = x
+
+preferences-doh-radio-default =
+ .label = Default (when available)
+ .description = Use secure DNS in regions where it’s available
+
+preferences-doh-radio-custom =
+ .label = Custom (always on)
+
+preferences-doh-radio-off =
+ .label = Off
+ .description = Use your default DNS resolver
+
+preferences-doh-fallback-label =
+ .label = Always fallback to default DNS
+ .description = Fall back to your default DNS resolver if there is a problem with secure DNS
+
+preferences-doh-status-item-off =
+ .message = DNS-over-HTTPS is off
+
+## Variables:
+## $reason (string) - A string representation of the reason DoH is not active. For example NS_ERROR_UNKNOWN_HOST or TRR_RCODE_FAIL.
+## $name (string) - The name of the DNS over HTTPS resolver. If a custom resolver is used, the name will be the domain of the URL.
+
+preferences-doh-status-item-not-active =
+ .message = DNS-over-HTTPS is not working because we encountered an error ({ $reason }) while trying to use the provider { $name }
+
+preferences-doh-status-item-not-active-bad-url =
+ .message = DNS-over-HTTPS is not working because we received an invalid URL ({ $reason })
+
+preferences-doh-status-item-active =
+ .message = DNS-over-HTTPS is using the provider { $name }
+
+preferences-doh-status-item-not-active-local =
+ .message = DNS-over-HTTPS is not working because we encountered an error ({ $reason }) while trying to use the local provider { $name }
+
+preferences-doh-status-item-active-local =
+ .message = DNS-over-HTTPS is using the local provider { $name }
+
+preferences-doh-select-resolver-label =
+ .label = Choose provider
+
+# Variables:
+# $name (String) - Display name or URL for the DNS over HTTPS provider
+connection-dns-over-https-url-item =
+ .label = { $name }
+ .tooltiptext = Use this provider for resolving DNS over HTTPS
+
+preferences-doh-custom-provider-label =
+ .aria-label = Enter a custom provider URL
+
+preferences-doh-header2 =
+ .heading = DNS over HTTPS
diff --git a/browser/themes/shared/preferences/preferences.css b/browser/themes/shared/preferences/preferences.css
@@ -1489,3 +1489,7 @@ setting-group[groupid="home"] {
font-size: var(--font-size-small);
}
}
+
+#dohProviderSelect {
+ --select-max-width: 235px;
+}
diff --git a/toolkit/content/tests/widgets/test_moz_message_bar.html b/toolkit/content/tests/widgets/test_moz_message_bar.html
@@ -46,6 +46,12 @@
<moz-message-bar id="messageWithLink" message="Test message">
<a slot="support-link" href="#learn-less">Learn less</a>
</moz-message-bar>
+ <moz-message-bar
+ id="messageWithSupportPage"
+ message="Test message"
+ supportPage="support-test"
+ >
+ </moz-message-bar>
<moz-message-bar id="slottedMessage">
<span slot="message"
>Here is a message with a nested
@@ -157,6 +163,42 @@
is(iconAlt, "Error", "Alternate text for the error icon is present.");
});
+ add_task(async function test_support_page_attribute() {
+ const mozMessageBar = document.querySelector("#messageWithSupportPage");
+ const link = mozMessageBar.shadowRoot.querySelector(
+ "[is='moz-support-link']"
+ );
+ const msgEl = mozMessageBar.messageEl;
+ is(mozMessageBar.supportLinkEls[0], link, "Link is assigned");
+ // Inspect internal class name - this is relied upon to toggle the
+ // visibility of the gap between the message and link (bug 1938906).
+ is(msgEl.className, "message has-link-after", "Expected class list");
+
+ mozMessageBar.supportPage = "";
+ await mozMessageBar.updateComplete;
+ is(msgEl.className, "message", "has-link-after class should be gone");
+
+ mozMessageBar.supportPage = "support-test";
+ await mozMessageBar.updateComplete;
+ is(msgEl.className, "message has-link-after", "class should be back");
+
+ mozMessageBar.message = "Different test message";
+ await mozMessageBar.updateComplete;
+ is(
+ msgEl.className,
+ "message has-link-after",
+ "class should not change"
+ );
+
+ mozMessageBar.message = "Test message";
+ await mozMessageBar.updateComplete;
+ is(
+ msgEl.className,
+ "message has-link-after",
+ "class should not change"
+ );
+ });
+
add_task(async function test_support_link_slot() {
const mozMessageBar = document.querySelector("#messageWithLink");
const link = mozMessageBar.querySelector("[href='#learn-less']");
diff --git a/toolkit/content/widgets/lit-select-control.mjs b/toolkit/content/widgets/lit-select-control.mjs
@@ -285,12 +285,6 @@ export class SelectControlBaseElement extends MozLitElement {
});
}
- // Re-dispatch change event so it's re-targeted to the custom element.
- handleChange(event) {
- event.stopPropagation();
- this.dispatchEvent(new Event(event.type, event));
- }
-
handleSlotChange() {
this.#childElements = null;
this.#focusedIndex = undefined;
diff --git a/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs b/toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-import { html, ifDefined } from "../vendor/lit.all.mjs";
+import { html, ifDefined, when } from "../vendor/lit.all.mjs";
import { MozLitElement } from "../lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
@@ -54,6 +54,7 @@ export default class MozMessageBar extends MozLitElement {
closeButton: "moz-button.close",
messageEl: ".message",
supportLinkSlot: "slot[name=support-link]",
+ supportLinkHolder: ".link",
};
static properties = {
@@ -61,6 +62,7 @@ export default class MozMessageBar extends MozLitElement {
heading: { type: String, fluent: true },
message: { type: String, fluent: true },
dismissable: { type: Boolean },
+ supportPage: { type: String },
messageL10nId: { type: String },
messageL10nArgs: { type: String },
};
@@ -109,6 +111,13 @@ export default class MozMessageBar extends MozLitElement {
* @type {string | undefined}
*/
this.heading = undefined;
+
+ /**
+ * The support page stub.
+ *
+ * @type {string | undefined}
+ */
+ this.supportPage = undefined;
}
onActionSlotchange() {
@@ -119,7 +128,7 @@ export default class MozMessageBar extends MozLitElement {
onLinkSlotChange() {
this.messageEl.classList.toggle(
"has-link-after",
- !!this.supportLinkEls.length
+ !!this.supportLinkEls.length || !!this.supportPage
);
}
@@ -134,9 +143,27 @@ export default class MozMessageBar extends MozLitElement {
}
get supportLinkEls() {
+ if (this.supportPage) {
+ return this.supportLinkHolder.children;
+ }
return this.supportLinkSlot.assignedElements();
}
+ supportLinkTemplate() {
+ if (this.supportPage) {
+ return html`<a
+ is="moz-support-link"
+ support-page=${this.supportPage}
+ part="support-link"
+ aria-describedby="heading message"
+ ></a>`;
+ }
+ return html`<slot
+ name="support-link"
+ @slotchange=${this.onLinkSlotChange}
+ ></slot>`;
+ }
+
iconTemplate() {
let iconData = messageTypeToIconData[this.type];
if (iconData) {
@@ -192,7 +219,12 @@ export default class MozMessageBar extends MozLitElement {
<div>
<slot name="message">
<span
- class="message"
+ id="message"
+ class=${when(
+ this.supportPage,
+ () => "message has-link-after",
+ () => "message"
+ )}
data-l10n-id=${ifDefined(this.messageL10nId)}
data-l10n-args=${ifDefined(
JSON.stringify(this.messageL10nArgs)
@@ -201,12 +233,7 @@ export default class MozMessageBar extends MozLitElement {
${this.message}
</span>
</slot>
- <span class="link">
- <slot
- name="support-link"
- @slotchange=${this.onLinkSlotChange}
- ></slot>
- </span>
+ <span class="link"> ${this.supportLinkTemplate()} </span>
</div>
</div>
</div>