tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/app/profile/firefox.js | 3+++
Mbrowser/components/preferences/main.js | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/preferences.js | 5+++++
Mbrowser/components/preferences/privacy.inc.xhtml | 5+++--
Mbrowser/components/preferences/privacy.js | 414+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mbrowser/components/preferences/widgets/setting-element/setting-element.mjs | 2+-
Mbrowser/locales-preview/privacyPreferences.ftl | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/themes/shared/preferences/preferences.css | 4++++
Mtoolkit/content/tests/widgets/test_moz_message_bar.html | 42++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/content/widgets/lit-select-control.mjs | 6------
Mtoolkit/content/widgets/moz-message-bar/moz-message-bar.mjs | 45++++++++++++++++++++++++++++++++++++---------
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>