tor-browser

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

commit 6ae41c310c7dbd4887baea378f3e3511f749feee
parent 02833d069ed67760396a4b69c79c4b3e0cf6498c
Author: mark <mkennedy@mozilla.com>
Date:   Wed,  5 Nov 2025 23:59:34 +0000

Bug 1994511 - Add support for nested control options on a config-based setting r=mstriemer

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

Diffstat:
Mbrowser/components/preferences/tests/chrome/test_setting_control_options.html | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/preferences/widgets/setting-control/setting-control.mjs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtoolkit/content/preferences/Preferences.mjs | 26++++++++++++++++++++++++--
3 files changed, 226 insertions(+), 33 deletions(-)

diff --git a/browser/components/preferences/tests/chrome/test_setting_control_options.html b/browser/components/preferences/tests/chrome/test_setting_control_options.html @@ -269,6 +269,145 @@ "Label was set from controlAttrs" ); }); + + add_task(async function testNestedControlOptions() { + const PREF = "test.setting-control-custom-nested.one"; + const SETTING = "setting-control-custom-nested-one"; + await SpecialPowers.pushPrefEnv({ + set: [[PREF, 1]], + }); + Preferences.addAll([{ id: PREF, type: "int" }]); + Preferences.addSetting({ + id: SETTING, + pref: PREF, + }); + + let itemConfig = { + control: "moz-button-group", + id: SETTING, + options: [ + { + control: "div", + options: [ + { + control: "moz-checkbox", + }, + ], + }, + ], + }; + let control = await renderTemplate(itemConfig); + + is( + control.controlEl.localName, + "moz-button-group", + "The control rendered a moz-button-group as parent control" + ); + const nestedControl = control.controlEl.children[0]; + + is( + nestedControl.localName, + "div", + "Rendered the nested div element control under the parent control" + ); + + const nestedControlOption = nestedControl.children[0]; + + is( + nestedControlOption.localName, + "moz-checkbox", + "Rendered the moz-checkbox option under the div element control" + ); + }); + + add_task(async function testNestedControlOptionItems() { + const PREF = "test.setting-control-custom-nested-items.one"; + const SETTING = "setting-control-custom-nested-items-one"; + + const FIRST_NESTED_ITEM_PREF = "test.settings-group.itemone"; + const FIRST_NESTED_ITEM_SETTING = "setting-item-one"; + + const SECOND_NESTED_ITEM_PREF = "test.settings-group.itemtwo"; + const SECOND_NESTED_ITEM_SETTING = "setting-item-two"; + + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF, 1], + [FIRST_NESTED_ITEM_PREF, true], + [SECOND_NESTED_ITEM_PREF, false], + ], + }); + Preferences.addAll([ + { id: PREF, type: "int" }, + { id: FIRST_NESTED_ITEM_PREF, type: "bool" }, + { id: SECOND_NESTED_ITEM_PREF, type: "bool" }, + ]); + Preferences.addSetting({ + id: SETTING, + pref: PREF, + }); + + Preferences.addSetting({ + id: FIRST_NESTED_ITEM_SETTING, + pref: FIRST_NESTED_ITEM_PREF, + }); + Preferences.addSetting({ + id: SECOND_NESTED_ITEM_SETTING, + pref: SECOND_NESTED_ITEM_PREF, + }); + + let itemConfig = { + control: "moz-radio-group", + l10nId: LABEL_L10N_ID, + id: SETTING, + options: [ + { + control: "moz-radio", + l10nId: LABEL_L10N_ID, + items: [ + { + l10nId: LABEL_L10N_ID, + id: FIRST_NESTED_ITEM_SETTING, + control: "moz-box-button", + }, + ], + }, + ], + }; + let control = await renderTemplate(itemConfig); + + is( + control.controlEl.localName, + "moz-radio-group", + "The control rendered a moz-radio-group as parent control" + ); + const nestedOptionControl = control.controlEl.children[0]; + + is( + nestedOptionControl.localName, + "moz-radio", + "Rendered moz-radio as the nested control option" + ); + + const nestedControlItems = nestedOptionControl.children; + is(nestedControlItems.length, 1, "Rendered nested control item"); + const [firstNestedControlItem] = nestedControlItems; + is( + firstNestedControlItem.localName, + "setting-control", + "Rendered the first nested setting-control item" + ); + is( + firstNestedControlItem.controlEl.id, + FIRST_NESTED_ITEM_SETTING, + "Rendered the first nested setting ID" + ); + is( + firstNestedControlItem.controlEl.localName, + "moz-box-button", + "Rendered moz-box-button as the first nested control item" + ); + }); </script> </head> <body> diff --git a/browser/components/preferences/widgets/setting-control/setting-control.mjs b/browser/components/preferences/widgets/setting-control/setting-control.mjs @@ -7,7 +7,6 @@ import { html, ifDefined, literal, - nothing, ref, staticHtml, unsafeStatic, @@ -19,6 +18,15 @@ import { /** @import MozCheckbox from "../../../../../toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs"*/ /** @import { Setting } from "chrome://global/content/preferences/Setting.mjs"; */ +/** @import { PreferencesSettingConfigNestedControlOption } from "chrome://global/content/preferences/Preferences.mjs"; */ + +/** + * Properties that represent a nested HTML element that will be a direct descendant of this setting control element + * @typedef {object} SettingNestedElementOption + * @property {Array<SettingNestedElementOption>} [options] + * @property {string} control - The {@link HTMLElement#localName} of any HTML element + * @property {Record<string, string>} [controlAttrs] - Attributes for the element + */ /** * Mapping of parent control tag names to the literal tag name for their @@ -43,6 +51,7 @@ const ITEM_SLOT_BY_PARENT = new Map([ ["moz-input-search", "nested"], ["moz-input-folder", "nested"], ["moz-input-password", "nested"], + ["moz-radio", "nested"], ["moz-radio-group", "nested"], // NOTE: moz-select does not support the nested slot. ["moz-toggle", "nested"], @@ -190,6 +199,7 @@ export class SettingControl extends SettingElement { /** * The default properties for an option. + * @param {PreferencesSettingConfigNestedControlOption | SettingNestedElementOption} config */ getOptionPropertyMapping(config) { const props = this.getCommonPropertyMapping(config); @@ -273,40 +283,62 @@ export class SettingControl extends SettingElement { return this.setting.controllingExtensionInfo.l10nId; } + /** + * Prepare nested item config and settings. + * @param {PreferencesSettingConfigNestedControlOption} config + * @returns {Array<string>} + */ + itemsTemplate(config) { + if (!config.items) { + return []; + } + + const itemArgs = config.items.map(i => ({ + config: i, + setting: this.getSetting(i.id), + })); + let control = config.control || "moz-checkbox"; + return itemArgs.map( + item => + html`<setting-control + .config=${item.config} + .setting=${item.setting} + .getSetting=${this.getSetting} + slot=${ifDefined(ITEM_SLOT_BY_PARENT.get(control))} + ></setting-control>` + ); + } + + /** + * Prepares any children (and any of its children's children) that this element may need. + * @param {PreferencesSettingConfigNestedControlOption | SettingNestedElementOption} config + * @returns {Array<string>} + */ + optionsTemplate(config) { + if (!config.options) { + return []; + } + let control = config.control || "moz-checkbox"; + return config.options.map(opt => { + let optionTag = opt.control + ? unsafeStatic(opt.control) + : KNOWN_OPTIONS.get(control); + return staticHtml`<${optionTag} + ${spread(this.getOptionPropertyMapping(opt))} + >${"items" in opt ? this.itemsTemplate(opt) : this.optionsTemplate(opt)}</${optionTag}>`; + }); + } + render() { // Allow the Setting to override the static config if necessary. this.config = this.setting.getControlConfig(this.config); let { config } = this; let control = config.control || "moz-checkbox"; - let getItemArgs = items => - items?.map(i => ({ - config: i, - setting: this.getSetting(i.id), - })) || []; - - // Prepare nested item config and settings. - let itemArgs = getItemArgs(config.items); - let itemTemplate = opts => - html`<setting-control - .config=${opts.config} - .setting=${opts.setting} - .getSetting=${this.getSetting} - slot=${ifDefined(ITEM_SLOT_BY_PARENT.get(control))} - ></setting-control>`; - let nestedSettings = itemArgs.map(itemTemplate); - - // Prepare any children that this element may need. - let controlChildren = nothing; - if (config.options) { - controlChildren = config.options.map(opt => { - let optionTag = opt.control - ? unsafeStatic(opt.control) - : KNOWN_OPTIONS.get(control); - return staticHtml`<${optionTag} - ${spread(this.getOptionPropertyMapping(opt))} - >${opt.items ? getItemArgs(opt.items).map(itemTemplate) : ""}</${optionTag}>`; - }); - } + + let nestedSettings = + "items" in config + ? this.itemsTemplate(config) + : this.optionsTemplate(config); // Get the properties for this element: id, fluent, disabled, etc. // These will be applied to the control using the spread directive. @@ -351,7 +383,7 @@ export class SettingControl extends SettingElement { ${spread(controlProps)} ${ref(this.controlRef)} tabindex=${ifDefined(this.tabIndex)} - >${controlChildren}${nestedSettings}</${tag}>`; + >${nestedSettings}</${tag}>`; } } customElements.define("setting-control", SettingControl); diff --git a/toolkit/content/preferences/Preferences.mjs b/toolkit/content/preferences/Preferences.mjs @@ -81,6 +81,27 @@ import { Setting } from "chrome://global/content/preferences/Setting.mjs"; */ /** + * @typedef {Record<string, any>} PreferencesSettingConfigControlAttributes + */ + +/** + * @typedef {Omit<PreferencesSettingConfigNestedControlOption, 'id | value'>} PreferencesSettingConfigNestedElementOption + */ + +/** + * A set of properties that represent a nested control or element. + * + * @typedef {object} PreferencesSettingConfigNestedControlOption + * @property {string} [control] - The {@link HTMLElement#localName} of any HTML element that will be nested as a direct descendant of the control element. A moz-checkbox will be rendered by default. + * @property {PreferencesSettingConfigControlAttributes} [controlAttrs] - A map of any attributes to add to the control + * @property {string} [l10nId] - The fluent ID of the control + * @property {Array<PreferencesSettingConfigNestedElementOption>} [options] - Options for additional nested HTML elements. This will be overridden if items property is used. + * @property {string} [id] + * @property {string} [value] - An optional initial value used for the control element if it's an input element that supports a value property + * @property {Array<PreferencesSettingsConfig>} [items] - A list of setting control items that will get rendered as direct descendants of the setting control. This overrides the options property. + */ + +/** * @typedef {object} PreferencesSettingsConfig * @property {string} id - The ID for the Setting, this should match the layout id * @property {string} [l10nId] - The Fluent l10n ID for the setting @@ -95,7 +116,7 @@ import { Setting } from "chrome://global/content/preferences/Setting.mjs"; * additional work that needs to happen, such as recording telemetry. * If you want to set the value of the Setting then use the {@link PreferencesSettingsConfig.set} function. * @property {Array<PreferencesSettingsConfig> | undefined} [items] - * @property {string | undefined} [control] + * @property {PreferencesSettingConfigNestedControlOption['control']} [control] - The {@link HTMLElement#localName} of any HTML element that will be rendered as a control in the UI for the setting. * @property {PreferencesSettingConfigSetupFunction} [setup] - A function to be called to register listeners for * the setting. It should return a {@link PreferencesSettingConfigTeardownFunction} function to * remove the listeners if necessary. This should emit change events when the setting has changed to @@ -106,7 +127,8 @@ import { Setting } from "chrome://global/content/preferences/Setting.mjs"; * in {@link file://./../../browser/components/preferences/widgets/setting-group/setting-group.mjs}. This should be * 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 {PreferencesSettingConfigControlAttributes} [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 {Array<PreferencesSettingConfigNestedControlOption>} [options] - An optional list of nested controls for this setting (select options, radio group radios, etc) * @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