tor-browser

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

commit 481df5890a9398688e21d226a5ae773d81e05389
parent 60ddde8182fef6c697f9f40d3b76136b6653cc96
Author: mark <mkennedy@mozilla.com>
Date:   Tue, 21 Oct 2025 15:46:06 +0000

Bug 1986805 - Support the common config properties for the setting-group element r=mstriemer

Differential Revision: https://phabricator.services.mozilla.com/D267053

Diffstat:
Mbrowser/components/preferences/jar.mn | 1+
Mbrowser/components/preferences/tests/chrome/chrome.toml | 1+
Abrowser/components/preferences/tests/chrome/head.js | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/tests/chrome/test_setting_control_checkbox.html | 17+++++++++++++++++
Mbrowser/components/preferences/tests/chrome/test_setting_group.html | 18++++++++++++++++++
Mbrowser/components/preferences/widgets/setting-control/setting-control.mjs | 105+++++++------------------------------------------------------------------------
Abrowser/components/preferences/widgets/setting-element/setting-element.mjs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/widgets/setting-group/setting-group.mjs | 12+++++++-----
Mtoolkit/content/preferences/Preferences.mjs | 6+++++-
Mtools/@types/generated/tspaths.json | 3+++
10 files changed, 261 insertions(+), 102 deletions(-)

diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn @@ -32,6 +32,7 @@ browser.jar: content/browser/preferences/widgets/setting-control.mjs (widgets/setting-control/setting-control.mjs) content/browser/preferences/widgets/setting-group.mjs (widgets/setting-group/setting-group.mjs) content/browser/preferences/widgets/setting-group.css (widgets/setting-group/setting-group.css) + content/browser/preferences/widgets/setting-element.mjs (widgets/setting-element/setting-element.mjs) content/browser/preferences/widgets/setting-pane.mjs (widgets/setting-pane/setting-pane.mjs) content/browser/preferences/widgets/security-privacy-card.mjs (widgets/security-privacy/security-privacy-card/security-privacy-card.mjs) content/browser/preferences/widgets/security-privacy-card.css (widgets/security-privacy/security-privacy-card/security-privacy-card.css) diff --git a/browser/components/preferences/tests/chrome/chrome.toml b/browser/components/preferences/tests/chrome/chrome.toml @@ -1,5 +1,6 @@ [DEFAULT] support-files = [ + "head.js", "../../../../../toolkit/content/tests/widgets/lit-test-helpers.js", ] diff --git a/browser/components/preferences/tests/chrome/head.js b/browser/components/preferences/tests/chrome/head.js @@ -0,0 +1,78 @@ +/** + * @callback TestSettingControlCommonPropertiesFunction + * @param {(config: Record<string, any>) => Promise<HTMLElement>} renderTemplateFunction + */ + +/** + * Asserts all properties on the element + * that is returned from the provided function. + * + * @type {TestSettingControlCommonPropertiesFunction} + */ +async function testCommonSettingControlPropertiesSet(renderTemplateFunction) { + const l10nId = "l10n ID"; + const l10nArgs = { foo: "bar" }; + const iconSrc = "anicon.png"; + const supportPage = "https://support.page"; + const subcategory = "the sub category"; + const label = "foo-bar"; + + const element = await renderTemplateFunction({ + l10nId, + l10nArgs, + iconSrc, + supportPage, + subcategory: "the sub category", + controlAttrs: { + label, + }, + }); + + is( + element.getAttribute("data-l10n-id"), + l10nId, + "sets data-l10n-id attribute" + ); + + is( + element.getAttribute("data-l10n-args"), + JSON.stringify(l10nArgs), + "converts data-l10n-args to stringified JSON object" + ); + + is(element.dataset.subcategory, subcategory, "sets subcategory"); + + is(element.getAttribute("label"), label, "sets controlAttrs.label"); + + is(element.iconSrc, iconSrc, "sets iconSrc"); + + is(element.supportPage, supportPage, "sets supportPage"); +} + +/** + * Asserts all unset properties on the element + * that is returned from the provided function. + * + * @type {TestSettingControlCommonPropertiesFunction} + */ +async function testCommonSettingControlPropertiesUnset(renderTemplateFunction) { + info("Test common properties when unset"); + const element = await renderTemplateFunction({}); + ok(!element.hasAttribute("data-l10n-id"), "no data-l10n-id attribute"); + ok(!element.hasAttribute("data-l10n-args"), "no data-l10n-args"); + ok(!element.dataset.subcategory, "no subcategory"); + ok(!element.hasAttribute("label"), "no controlAttrs.label"); + ok(!element.iconSrc, "no iconSrc"); + ok(!element.supportPage, "no supportPage"); +} + +/** + * Asserts all properties set on the element + * that is returned from the provided function. + * + * @type {TestSettingControlCommonPropertiesFunction} + */ +async function testCommonSettingControlProperties(renderTemplateFunction) { + await testCommonSettingControlPropertiesSet(renderTemplateFunction); + await testCommonSettingControlPropertiesUnset(renderTemplateFunction); +} diff --git a/browser/components/preferences/tests/chrome/test_setting_control_checkbox.html b/browser/components/preferences/tests/chrome/test_setting_control_checkbox.html @@ -11,6 +11,7 @@ <link rel="stylesheet" href="chrome://global/skin/global.css" /> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> + <script src="./head.js"></script> <script type="module" src="chrome://browser/content/preferences/widgets/setting-group.mjs" @@ -181,6 +182,22 @@ ); }); + add_task(async function testCommonControlProperties() { + const SETTING = "setting-common-props"; + Preferences.addSetting({ + id: SETTING, + get: () => true, + }); + + await testCommonSettingControlProperties(async commonConfig => { + const control = await renderTemplate({ + id: SETTING, + ...commonConfig, + }); + return control.querySelector("moz-checkbox"); + }); + }); + add_task(async function testSupportLinkSubcategory() { const SETTING = "setting-control-subcategory"; Preferences.addSetting({ diff --git a/browser/components/preferences/tests/chrome/test_setting_group.html b/browser/components/preferences/tests/chrome/test_setting_group.html @@ -11,6 +11,7 @@ <link rel="stylesheet" href="chrome://global/skin/global.css" /> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script> + <script src="./head.js"></script> <script type="module" src="chrome://browser/content/preferences/widgets/setting-group.mjs" @@ -195,6 +196,23 @@ ); }); + add_task(async function testCommonControlProperties() { + const SETTING = "setting-control-support-link"; + Preferences.addSetting({ + id: SETTING, + get: () => true, + }); + + await testCommonSettingControlProperties(async commonConfig => { + const control = await renderTemplate({ + ...commonConfig, + id: SETTING, + items: [], + }); + return control.querySelector("moz-fieldset"); + }); + }); + add_task(async function testSettingGroupCallbacks() { const SETTING_ONE = "setting-first"; const SETTING_TWO = "setting-second"; diff --git a/browser/components/preferences/widgets/setting-control/setting-control.mjs b/browser/components/preferences/widgets/setting-control/setting-control.mjs @@ -3,104 +3,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { - Directive, createRef, - directive, html, ifDefined, literal, - noChange, nothing, ref, staticHtml, unsafeStatic, } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { + SettingElement, + spread, +} from "chrome://browser/content/preferences/widgets/setting-element.mjs"; /** @import MozCheckbox from "../../../../../toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs"*/ /** @import { Setting } from "chrome://global/content/preferences/Setting.mjs"; */ /** - * A Lit directive that applies all properties of an object to a DOM element. - * - * This directive interprets keys in the provided props object as follows: - * - Keys starting with `?` set or remove boolean attributes using `toggleAttribute`. - * - Keys starting with `.` set properties directly on the element. - * - Keys starting with `@` are currently not supported and will throw an error. - * - All other keys are applied as regular attributes using `setAttribute`. - * - * It avoids reapplying values that have not changed, but does not currently - * remove properties that were previously set and are no longer present in the new input. - * - * This directive is useful to "spread" an object of attributes/properties declaratively onto an - * element in a Lit template. - */ -class SpreadDirective extends Directive { - /** - * A record of previously applied properties to avoid redundant updates. - * @type {Record<string, unknown>} - */ - #prevProps = {}; - - /** - * Render nothing by default as all changes are made in update using DOM APIs - * on the element directly. - * @returns {typeof nothing} - */ - render() { - return nothing; - } - - /** - * Apply props to the element using DOM APIs, updating only changed values. - * @param {AttributePart} part - The part of the template this directive is bound to. - * @param {[Record<string, unknown>]} propsArray - An array with a single object containing props to apply. - * @returns {typeof noChange} - Indicates to Lit that no re-render is needed. - */ - update(part, [props]) { - // TODO: This doesn't clear any values that were set in previous calls if - // they are no longer present. - // It isn't entirely clear to me (mstriemer) what we should do if a prop is - // removed, or if the prop has changed from say ?foo to foo. By not - // implementing the auto-clearing hopefully the consumer will do something - // that fits their use case. - - /** @type {HTMLElement} */ - let el = part.element; - - for (let [key, value] of Object.entries(props)) { - // Skip if the value hasn't changed since the last update. - if (value === this.#prevProps[key]) { - continue; - } - - // Update the element based on the property key matching Lit's templates: - // ?key -> el.toggleAttribute(key, value) - // .key -> el.key = value - // key -> el.setAttribute(key, value) - if (key.startsWith("?")) { - el.toggleAttribute(key.slice(1), Boolean(value)); - } else if (key.startsWith(".")) { - el[key.slice(1)] = value; - } else if (key.startsWith("@")) { - throw new Error( - `Event listeners are not yet supported with spread (${key})` - ); - } else { - el.setAttribute(key, String(value)); - } - } - - // Save current props for comparison in the next update. - this.#prevProps = props; - - return noChange; - } -} - -const spread = directive(SpreadDirective); - -/** * Mapping of parent control tag names to the literal tag name for their * expected children. eg. "moz-radio-group"->literal`moz-radio`. * @type Map<string, literal> @@ -128,7 +48,7 @@ const ITEM_SLOT_BY_PARENT = new Map([ ["moz-toggle", "nested"], ]); -export class SettingControl extends MozLitElement { +export class SettingControl extends SettingElement { /** * @type {Setting | undefined} */ @@ -190,7 +110,7 @@ export class SettingControl extends MozLitElement { }; /** - * @type {MozLitElement['willUpdate']} + * @type {SettingElement['willUpdate']} */ willUpdate(changedProperties) { if (changedProperties.has("setting")) { @@ -234,22 +154,15 @@ export class SettingControl extends MozLitElement { * Note: for the disabled property, a setting can either be locked, * or controlled by an extension but not both. * + * @override * @param {PreferencesSettingsConfig} config - * @returns {Record<string, any>} + * @returns {ReturnType<SettingElement['getCommonPropertyMapping']>} */ getCommonPropertyMapping(config) { return { - id: config.id, - "data-l10n-id": config.l10nId, - "data-l10n-args": config.l10nArgs - ? JSON.stringify(config.l10nArgs) - : undefined, - ".iconSrc": config.iconSrc, - ".supportPage": config.supportPage, + ...super.getCommonPropertyMapping(config), ".setting": this.setting, ".control": this, - "data-subcategory": config.subcategory, - ...config.controlAttrs, }; } diff --git a/browser/components/preferences/widgets/setting-element/setting-element.mjs b/browser/components/preferences/widgets/setting-element/setting-element.mjs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 { + Directive, + noChange, + nothing, + directive, +} from "chrome://global/content/vendor/lit.all.mjs"; + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A Lit directive that applies all properties of an object to a DOM element. + * + * This directive interprets keys in the provided props object as follows: + * - Keys starting with `?` set or remove boolean attributes using `toggleAttribute`. + * - Keys starting with `.` set properties directly on the element. + * - Keys starting with `@` are currently not supported and will throw an error. + * - All other keys are applied as regular attributes using `setAttribute`. + * + * It avoids reapplying values that have not changed, but does not currently + * remove properties that were previously set and are no longer present in the new input. + * + * This directive is useful to "spread" an object of attributes/properties declaratively onto an + * element in a Lit template. + */ +class SpreadDirective extends Directive { + /** + * A record of previously applied properties to avoid redundant updates. + * @type {Record<string, unknown>} + */ + #prevProps = {}; + + /** + * Render nothing by default as all changes are made in update using DOM APIs + * on the element directly. + * @returns {typeof nothing} + */ + render() { + return nothing; + } + + /** + * Apply props to the element using DOM APIs, updating only changed values. + * @param {AttributePart} part - The part of the template this directive is bound to. + * @param {[Record<string, unknown>]} propsArray - An array with a single object containing props to apply. + * @returns {typeof noChange} - Indicates to Lit that no re-render is needed. + */ + update(part, [props]) { + // TODO: This doesn't clear any values that were set in previous calls if + // they are no longer present. + // It isn't entirely clear to me (mstriemer) what we should do if a prop is + // removed, or if the prop has changed from say ?foo to foo. By not + // implementing the auto-clearing hopefully the consumer will do something + // that fits their use case. + + /** @type {HTMLElement} */ + let el = part.element; + + for (let [key, value] of Object.entries(props)) { + // Skip if the value hasn't changed since the last update. + if (value === this.#prevProps[key]) { + continue; + } + + // Update the element based on the property key matching Lit's templates: + // ?key -> el.toggleAttribute(key, value) + // .key -> el.key = value + // key -> el.setAttribute(key, value) + if (key.startsWith("?")) { + el.toggleAttribute(key.slice(1), Boolean(value)); + } else if (key.startsWith(".")) { + el[key.slice(1)] = value; + } else if (key.startsWith("@")) { + throw new Error( + `Event listeners are not yet supported with spread (${key})` + ); + } else { + el.setAttribute(key, String(value)); + } + } + + // Save current props for comparison in the next update. + this.#prevProps = props; + + return noChange; + } +} + +export const spread = directive(SpreadDirective); + +export class SettingElement extends MozLitElement { + /** + * The default properties that the setting element accepts. + * + * @param {PreferencesSettingsConfig} config + * @returns {Record<string, any>} + */ + getCommonPropertyMapping(config) { + /** + * @type {Record<string, any>} + */ + const result = { + id: config.id, + "data-l10n-args": config.l10nArgs + ? JSON.stringify(config.l10nArgs) + : undefined, + ".iconSrc": config.iconSrc, + "data-subcategory": config.subcategory, + ...config.controlAttrs, + }; + if (config.supportPage) { + result[".supportPage"] = config.supportPage; + } + if (config.l10nId) { + result["data-l10n-id"] = config.l10nId; + } + return result; + } +} diff --git a/browser/components/preferences/widgets/setting-group/setting-group.mjs b/browser/components/preferences/widgets/setting-group/setting-group.mjs @@ -2,8 +2,11 @@ * 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 "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { + SettingElement, + spread, +} from "chrome://browser/content/preferences/widgets/setting-element.mjs"; /** @import { SettingControl } from "../setting-control/setting-control.mjs"; */ /** @import {PreferencesSettingsConfig, Preferences} from "chrome://global/content/preferences/Preferences.mjs" */ @@ -17,7 +20,7 @@ const CLICK_HANDLERS = new Set([ "moz-box-group", ]); -export class SettingGroup extends MozLitElement { +export class SettingGroup extends SettingElement { constructor() { super(); @@ -113,12 +116,11 @@ export class SettingGroup extends MozLitElement { return ""; } return html`<moz-fieldset - data-l10n-id=${ifDefined(this.config.l10nId)} .headingLevel=${this.config.headingLevel} - .supportPage=${ifDefined(this.config.supportPage)} @change=${this.onChange} @click=${this.onClick} @visibility-change=${this.handleVisibilityChange} + ${spread(this.getCommonPropertyMapping(this.config))} >${this.config.items.map(item => this.itemTemplate(item))}</moz-fieldset >`; } diff --git a/toolkit/content/preferences/Preferences.mjs b/toolkit/content/preferences/Preferences.mjs @@ -83,7 +83,8 @@ import { Setting } from "chrome://global/content/preferences/Setting.mjs"; /** * @typedef {object} PreferencesSettingsConfig * @property {string} id - The ID for the Setting, this should match the layout id - * @property {string} [l10nId] + * @property {string} [l10nId] - The Fluent l10n ID for the setting + * @property {Record<string, string>} [l10nArgs] - An object containing l10n IDs and their values that will be translated with Fluent * @property {string} [pref] - A {@link Services.prefs} id that will be used as the backend if it is provided * @property {PreferenceSettingVisibleFunction} [visible] - Function to determine if a setting is visible in the UI * @property {PreferenceSettingGetter} [get] - Function to get the value of the setting. Optional if {@link PreferencesSettingsConfig#pref} is set. @@ -106,6 +107,9 @@ import { Setting } from "chrome://global/content/preferences/Setting.mjs"; * used for controls that aren’t regular form controls but instead perform an action when clicked, like a button or link. * @property {Array<string> | void} [deps] - An array of setting IDs that this setting depends on, when these settings change this setting will emit a change event to update the UI * @property {Record<string, any>} [controlAttrs] - An object of additional attributes to be set on the control. These can be used to further customize the control for example a message bar of the warning type, or what dialog a button should open + * @property {string} [iconSrc] - A path to the icon for the control (if the control supports one) + * @property {string} [supportPage] - The SUMO support page slug for the setting + * @property {string} [subcategory] - The sub-category slug used for direct linking to a setting from SUMO */ const lazy = {}; diff --git a/tools/@types/generated/tspaths.json b/tools/@types/generated/tspaths.json @@ -113,6 +113,9 @@ "chrome://browser/content/preferences/widgets/setting-control.mjs": [ "browser/components/preferences/widgets/setting-control/setting-control.mjs" ], + "chrome://browser/content/preferences/widgets/setting-element.mjs": [ + "browser/components/preferences/widgets/setting-element/setting-element.mjs" + ], "chrome://browser/content/preferences/widgets/setting-group.mjs": [ "browser/components/preferences/widgets/setting-group/setting-group.mjs" ],