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:
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"
],