commit 483d918796d7b3db934b48da392643df22b3da13
parent dcfc64fe2d8dc6d64ab0dd7b5fb685d4e9f8d93d
Author: Benjamin VanderSloot <bvandersloot@mozilla.com>
Date: Tue, 11 Nov 2025 23:02:48 +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:
12 files changed, 641 insertions(+), 23 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -2829,6 +2829,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
@@ -1832,6 +1832,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
@@ -664,7 +664,7 @@
class="subcategory"
hidden="true"
data-category="panePrivacy">
- <html:h1 data-l10n-id="permissions-header"/>
+ <html:h1 data-l10n-id="permissions-header2"/>
</hbox>
<!-- Permissions -->
@@ -828,7 +828,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
@@ -1984,6 +1985,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)
@@ -2542,6 +2947,8 @@ var gPrivacyPane = {
initSettingGroup("certificates");
initSettingGroup("ipprotection");
initSettingGroup("permissions");
+ initSettingGroup("dnsOverHttps");
+ initSettingGroup("dnsOverHttpsAdvanced");
this._updateSanitizeSettingsButton();
this.initializeHistoryMode();
@@ -2679,13 +3086,6 @@ var gPrivacyPane = {
setSyncFromPrefListener("rememberForms", microControlHandler);
setSyncFromPrefListener("alwaysClear", microControlHandler);
- 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/components/storybook/component-status/components.json b/browser/components/storybook/component-status/components.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2025-10-15T13:18:04.980Z",
+ "generatedAt": "2025-10-29T17:19:39.854Z",
"count": 28,
"items": [
{
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
@@ -1479,3 +1479,7 @@ richlistitem .text-link:hover {
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"
+ support-page="support-test"
+ >
+ </moz-message-bar>
<moz-message-bar id="slottedMessage">
<span slot="message"
>Here is a message with a nested
@@ -157,6 +163,40 @@
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.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");
+
+ link.remove();
+ await mozMessageBar.updateComplete;
+ is(msgEl.className, "message", "has-link-after class should be gone");
+
+ mozMessageBar.append(link);
+ 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
@@ -271,12 +271,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
@@ -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 },
};
@@ -134,9 +136,27 @@ export default class MozMessageBar extends MozLitElement {
}
get supportLinkEls() {
+ if (this.supportPage) {
+ return this.supportLinkHolder.childElements();
+ }
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,6 +212,7 @@ export default class MozMessageBar extends MozLitElement {
<div>
<slot name="message">
<span
+ id="message"
class="message"
data-l10n-id=${ifDefined(this.messageL10nId)}
data-l10n-args=${ifDefined(
@@ -201,12 +222,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>