commit 34b94c2ba9563b11b24f89a825b0254e3a0f64aa
parent d2e94fc8ba3c0914e6393092a8bbbb1e7006fcbb
Author: Mark Striemer <mstriemer@mozilla.com>
Date: Fri, 14 Nov 2025 19:28:53 +0000
Bug 1996849 - Improve typing in Settings code r=mconley,tgiles
Differential Revision: https://phabricator.services.mozilla.com/D270779
Diffstat:
15 files changed, 986 insertions(+), 661 deletions(-)
diff --git a/browser/components/preferences/findInPage.js b/browser/components/preferences/findInPage.js
@@ -9,9 +9,12 @@
// inside the button, which allows the text to be highlighted when the user
// is searching.
-const MozButton = customElements.get("button");
-class HighlightableButton extends MozButton {
+/** @import MozInputSearch from "chrome://global/content/elements/moz-input-search.mjs" */
+
+const MozButtonClass = customElements.get("button");
+class HighlightableButton extends MozButtonClass {
static get inheritedAttributes() {
+ // @ts-expect-error super is MozButton from toolkit/content/widgets/button.js
return Object.assign({}, super.inheritedAttributes, {
".button-text": "text=label,accesskey,crop",
});
@@ -22,9 +25,14 @@ customElements.define("highlightable-button", HighlightableButton, {
});
var gSearchResultsPane = {
+ /** @type {string} */
+ query: undefined,
listSearchTooltips: new Set(),
listSearchMenuitemIndicators: new Set(),
+ /** @type {MozInputSearch} */
searchInput: null,
+ /** @type {HTMLDivElement} */
+ searchTooltipContainer: null,
// A map of DOM Elements to a string of keywords used in search
// XXX: We should invalidate this cache on `intl:app-locales-changed`
searchKeywords: new WeakMap(),
@@ -49,9 +57,11 @@ var gSearchResultsPane = {
return;
}
this.inited = true;
- this.searchInput = document.getElementById("searchInput");
- this.searchTooltipContainer = document.getElementById(
- "search-tooltip-container"
+ this.searchInput = /** @type {MozInputSearch} */ (
+ document.getElementById("searchInput")
+ );
+ this.searchTooltipContainer = /** @type {HTMLDivElement} */ (
+ document.getElementById("search-tooltip-container")
);
window.addEventListener("resize", () => {
@@ -71,6 +81,7 @@ var gSearchResultsPane = {
ensureScrollPadding();
},
+ /** @param {InputEvent} event */
async handleEvent(event) {
// Ensure categories are initialized if idle callback didn't run sooo enough.
await this.initializeCategories();
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
@@ -2,10 +2,6 @@
* 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 MozButton from "chrome://global/content/elements/moz-button.mjs"; */
-/** @import { SettingGroup } from "./widgets/setting-group/setting-group.mjs" */
-/** @import { PreferencesSettingsConfig } from "chrome://global/content/preferences/Preferences.mjs" */
-
/* import-globals-from extensionControlled.js */
/* import-globals-from preferences.js */
/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
@@ -228,104 +224,122 @@ Preferences.addSetting({
pref: "browser.privatebrowsing.autostart",
});
-Preferences.addSetting({
- id: "launchOnLoginApproved",
- _getLaunchOnLoginApprovedCachedValue: true,
- get() {
- return this._getLaunchOnLoginApprovedCachedValue;
- },
- // Check for a launch on login registry key
- // This accounts for if a user manually changes it in the registry
- // Disabling in Task Manager works outside of just deleting the registry key
- // in HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run
- // but it is not possible to change it back to enabled as the disabled value is just a random
- // hexadecimal number
- async setup() {
- if (AppConstants.platform !== "win") {
- /**
- * WindowsLaunchOnLogin isnt available if not on windows
- * but this setup function still fires, so must prevent
- * WindowsLaunchOnLogin.getLaunchOnLoginApproved
- * below from executing unnecessarily.
- */
- return;
- }
- this._getLaunchOnLoginApprovedCachedValue =
- await WindowsLaunchOnLogin.getLaunchOnLoginApproved();
- },
-});
+Preferences.addSetting(
+ /** @type {{ _getLaunchOnLoginApprovedCachedValue: boolean } & SettingConfig} */ ({
+ id: "launchOnLoginApproved",
+ _getLaunchOnLoginApprovedCachedValue: true,
+ get() {
+ return this._getLaunchOnLoginApprovedCachedValue;
+ },
+ // Check for a launch on login registry key
+ // This accounts for if a user manually changes it in the registry
+ // Disabling in Task Manager works outside of just deleting the registry key
+ // in HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run
+ // but it is not possible to change it back to enabled as the disabled value is just a random
+ // hexadecimal number
+ setup() {
+ if (AppConstants.platform !== "win") {
+ /**
+ * WindowsLaunchOnLogin isnt available if not on windows
+ * but this setup function still fires, so must prevent
+ * WindowsLaunchOnLogin.getLaunchOnLoginApproved
+ * below from executing unnecessarily.
+ */
+ return;
+ }
+ // @ts-ignore bug 1996860
+ WindowsLaunchOnLogin.getLaunchOnLoginApproved().then(val => {
+ this._getLaunchOnLoginApprovedCachedValue = val;
+ });
+ },
+ })
+);
Preferences.addSetting({
id: "windowsLaunchOnLoginEnabled",
pref: "browser.startup.windowsLaunchOnLogin.enabled",
});
-Preferences.addSetting({
- id: "windowsLaunchOnLogin",
- deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
- _getLaunchOnLoginEnabledValue: false,
- get startWithLastProfile() {
- return Cc["@mozilla.org/toolkit/profile-service;1"].getService(
- Ci.nsIToolkitProfileService
- ).startWithLastProfile;
- },
- get() {
- return this._getLaunchOnLoginEnabledValue;
- },
- async setup(emitChange) {
- if (AppConstants.platform !== "win") {
- /**
- * WindowsLaunchOnLogin isnt available if not on windows
- * but this setup function still fires, so must prevent
- * WindowsLaunchOnLogin.getLaunchOnLoginEnabled
- * below from executing unnecessarily.
- */
- return;
- }
+Preferences.addSetting(
+ /** @type {{_getLaunchOnLoginEnabledValue: boolean, startWithLastProfile: boolean} & SettingConfig} */ ({
+ id: "windowsLaunchOnLogin",
+ deps: ["launchOnLoginApproved", "windowsLaunchOnLoginEnabled"],
+ _getLaunchOnLoginEnabledValue: false,
+ get startWithLastProfile() {
+ return Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ ).startWithLastProfile;
+ },
+ get() {
+ return this._getLaunchOnLoginEnabledValue;
+ },
+ setup(emitChange) {
+ if (AppConstants.platform !== "win") {
+ /**
+ * WindowsLaunchOnLogin isnt available if not on windows
+ * but this setup function still fires, so must prevent
+ * WindowsLaunchOnLogin.getLaunchOnLoginEnabled
+ * below from executing unnecessarily.
+ */
+ return;
+ }
- let getLaunchOnLoginEnabledValue;
- if (!this.startWithLastProfile) {
- getLaunchOnLoginEnabledValue = false;
- } else {
- getLaunchOnLoginEnabledValue =
- await WindowsLaunchOnLogin.getLaunchOnLoginEnabled();
- }
- if (getLaunchOnLoginEnabledValue !== this._getLaunchOnLoginEnabledValue) {
- this._getLaunchOnLoginEnabledValue = getLaunchOnLoginEnabledValue;
- emitChange();
- }
- },
- visible: ({ windowsLaunchOnLoginEnabled }) => {
- let isVisible =
- AppConstants.platform === "win" && windowsLaunchOnLoginEnabled.value;
- if (isVisible) {
- NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
- once: true,
- });
- }
- return isVisible;
- },
- disabled({ launchOnLoginApproved }) {
- return !this.startWithLastProfile || !launchOnLoginApproved.value;
- },
- onUserChange(checked) {
- if (checked) {
- // windowsLaunchOnLogin has been checked: create registry key or shortcut
- // The shortcut is created with the same AUMID as Firefox itself. However,
- // this is not set during browser tests and the fallback of checking the
- // registry fails. As such we pass an arbitrary AUMID for the purpose
- // of testing.
- WindowsLaunchOnLogin.createLaunchOnLogin();
- Services.prefs.setBoolPref(
- "browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
- true
- );
- } else {
- // windowsLaunchOnLogin has been unchecked: delete registry key and shortcut
- WindowsLaunchOnLogin.removeLaunchOnLogin();
- }
- },
-});
+ /** @type {boolean} */
+ let getLaunchOnLoginEnabledValue;
+ let maybeEmitChange = () => {
+ if (
+ getLaunchOnLoginEnabledValue !== this._getLaunchOnLoginEnabledValue
+ ) {
+ this._getLaunchOnLoginEnabledValue = getLaunchOnLoginEnabledValue;
+ emitChange();
+ }
+ };
+ if (!this.startWithLastProfile) {
+ getLaunchOnLoginEnabledValue = false;
+ maybeEmitChange();
+ } else {
+ // @ts-ignore bug 1996860
+ WindowsLaunchOnLogin.getLaunchOnLoginEnabled().then(val => {
+ getLaunchOnLoginEnabledValue = val;
+ maybeEmitChange();
+ });
+ }
+ },
+ visible: ({ windowsLaunchOnLoginEnabled }) => {
+ let isVisible =
+ AppConstants.platform === "win" && windowsLaunchOnLoginEnabled.value;
+ if (isVisible) {
+ // @ts-ignore bug 1996860
+ NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
+ once: true,
+ });
+ }
+ return isVisible;
+ },
+ disabled({ launchOnLoginApproved }) {
+ return !this.startWithLastProfile || !launchOnLoginApproved.value;
+ },
+ onUserChange(checked) {
+ if (checked) {
+ // windowsLaunchOnLogin has been checked: create registry key or shortcut
+ // The shortcut is created with the same AUMID as Firefox itself. However,
+ // this is not set during browser tests and the fallback of checking the
+ // registry fails. As such we pass an arbitrary AUMID for the purpose
+ // of testing.
+ // @ts-ignore bug 1996860
+ WindowsLaunchOnLogin.createLaunchOnLogin();
+ Services.prefs.setBoolPref(
+ "browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
+ true
+ );
+ } else {
+ // windowsLaunchOnLogin has been unchecked: delete registry key and shortcut
+ // @ts-ignore bug 1996860
+ WindowsLaunchOnLogin.removeLaunchOnLogin();
+ }
+ },
+ })
+);
Preferences.addSetting({
id: "windowsLaunchOnLoginDisabledProfileBox",
@@ -393,6 +407,7 @@ Preferences.addSetting({
if (checked) {
// We need to restore the blank homepage setting in our other pref
if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
+ // @ts-ignore bug 1996860
HomePage.safeSet("about:blank");
}
newValue = gMainPane.STARTUP_PREF_RESTORE_SESSION;
@@ -431,51 +446,56 @@ Preferences.addSetting({
id: "useCursorNavigation",
pref: "accessibility.browsewithcaret",
});
-Preferences.addSetting({
- id: "useFullKeyboardNavigation",
- pref: "accessibility.tabfocus",
- visible: () => AppConstants.platform == "macosx",
- /**
- * Returns true if any full keyboard nav is enabled and false otherwise, caching
- * the current value to enable proper pref restoration if the checkbox is
- * never changed.
- *
- * accessibility.tabfocus
- * - an integer controlling the focusability of:
- * 1 text controls
- * 2 form elements
- * 4 links
- * 7 all of the above
- */
- get(prefVal) {
- this._storedFullKeyboardNavigation = prefVal;
- return prefVal == 7;
- },
- /**
- * Returns the value of the full keyboard nav preference represented by UI,
- * preserving the preference's "hidden" value if the preference is
- * unchanged and represents a value not strictly allowed in UI.
- */
- set(checked) {
- if (checked) {
- return 7;
- }
- if (this._storedFullKeyboardNavigation != 7) {
- // 1/2/4 values set via about:config should persist
- return this._storedFullKeyboardNavigation;
- }
- // When the checkbox is unchecked, default to just text controls.
- return 1;
- },
-});
+Preferences.addSetting(
+ /** @type {{ _storedFullKeyboardNavigation: number } & SettingConfig} */ ({
+ _storedFullKeyboardNavigation: -1,
+ id: "useFullKeyboardNavigation",
+ pref: "accessibility.tabfocus",
+ visible: () => AppConstants.platform == "macosx",
+ /**
+ * Returns true if any full keyboard nav is enabled and false otherwise, caching
+ * the current value to enable proper pref restoration if the checkbox is
+ * never changed.
+ *
+ * accessibility.tabfocus
+ * - an integer controlling the focusability of:
+ * 1 text controls
+ * 2 form elements
+ * 4 links
+ * 7 all of the above
+ */
+ get(prefVal) {
+ this._storedFullKeyboardNavigation = prefVal;
+ return prefVal == 7;
+ },
+ /**
+ * Returns the value of the full keyboard nav preference represented by UI,
+ * preserving the preference's "hidden" value if the preference is
+ * unchanged and represents a value not strictly allowed in UI.
+ */
+ set(checked) {
+ if (checked) {
+ return 7;
+ }
+ if (this._storedFullKeyboardNavigation != 7) {
+ // 1/2/4 values set via about:config should persist
+ return this._storedFullKeyboardNavigation;
+ }
+ // When the checkbox is unchecked, default to just text controls.
+ return 1;
+ },
+ })
+);
Preferences.addSetting({
id: "linkPreviewEnabled",
pref: "browser.ml.linkPreview.enabled",
+ // @ts-ignore bug 1996860
visible: () => LinkPreview.canShowPreferences,
});
Preferences.addSetting({
id: "linkPreviewKeyPoints",
pref: "browser.ml.linkPreview.optin",
+ // @ts-ignore bug 1996860
visible: () => LinkPreview.canShowKeyPoints,
});
Preferences.addSetting({
@@ -532,38 +552,46 @@ Preferences.addSetting({
},
});
-Preferences.addSetting({
- id: "web-appearance-chooser",
- themeNames: ["dark", "light", "auto"],
- pref: "layout.css.prefers-color-scheme.content-override",
- setup(emitChange) {
- Services.obs.addObserver(emitChange, "look-and-feel-changed");
- return () =>
- Services.obs.removeObserver(emitChange, "look-and-feel-changed");
- },
- get(val, _, setting) {
- return this.themeNames[val] || this.themeNames[setting.pref.defaultValue];
- },
- set(val) {
- return this.themeNames.indexOf(val);
- },
- getControlConfig(config) {
- // Set the auto theme image to the light/dark that matches.
- let systemThemeIndex = Services.appinfo.contentThemeDerivedColorSchemeIsDark
- ? 2
- : 1;
- config.options[0].controlAttrs = {
- ...config.options[0].controlAttrs,
- imagesrc: config.options[systemThemeIndex].controlAttrs.imagesrc,
- };
- return config;
- },
-});
+Preferences.addSetting(
+ /** @type {{ themeNames: string[] } & SettingConfig}} */ ({
+ id: "web-appearance-chooser",
+ themeNames: ["dark", "light", "auto"],
+ pref: "layout.css.prefers-color-scheme.content-override",
+ setup(emitChange) {
+ Services.obs.addObserver(emitChange, "look-and-feel-changed");
+ return () =>
+ Services.obs.removeObserver(emitChange, "look-and-feel-changed");
+ },
+ get(val, _, setting) {
+ return (
+ this.themeNames[val] ||
+ this.themeNames[/** @type {number} */ (setting.pref.defaultValue)]
+ );
+ },
+ /** @param {string} val */
+ set(val) {
+ return this.themeNames.indexOf(val);
+ },
+ getControlConfig(config) {
+ // Set the auto theme image to the light/dark that matches.
+ let systemThemeIndex = Services.appinfo
+ .contentThemeDerivedColorSchemeIsDark
+ ? 2
+ : 1;
+ config.options[0].controlAttrs = {
+ ...config.options[0].controlAttrs,
+ imagesrc: config.options[systemThemeIndex].controlAttrs.imagesrc,
+ };
+ return config;
+ },
+ })
+);
Preferences.addSetting({
id: "web-appearance-manage-themes-link",
onUserClick: e => {
e.preventDefault();
+ // @ts-ignore topChromeWindow global
window.browsingContext.topChromeWindow.BrowserAddonUI.openAddonsMgr(
"addons://list/theme"
);
@@ -893,7 +921,11 @@ const DefaultBrowserHelper = {
* @type {typeof import('../shell/ShellService.sys.mjs').ShellService | undefined}
*/
get shellSvc() {
- return AppConstants.HAVE_SHELL_SERVICE && getShellService();
+ return (
+ AppConstants.HAVE_SHELL_SERVICE &&
+ // @ts-ignore from utilityOverlay.js
+ getShellService()
+ );
},
/**
@@ -1017,7 +1049,7 @@ Preferences.addSetting({
* browser is already the default browser.
*/
visible: () => DefaultBrowserHelper.canCheck,
- disabled: (deps, setting) =>
+ disabled: (_, setting) =>
!DefaultBrowserHelper.canCheck ||
setting.locked ||
DefaultBrowserHelper.isBrowserDefault,
@@ -1095,9 +1127,7 @@ Preferences.addSetting({
},
});
-/**
- * @type {Record<string, PreferencesSettingsConfig>} SettingConfig
- */
+/** @type {Record<string, SettingGroupConfig>} */
let SETTINGS_CONFIG = {
containers: {
// This section is marked as in progress for testing purposes
@@ -2575,7 +2605,7 @@ var gMainPane = {
* The fully initialized state.
*
* @param {Object} supportedLanguages
- * @param {Array<{ langTag: string, displayName: string}} languageList
+ * @param {Array<{ langTag: string, displayName: string}>} languageList
* @param {Map<string, DownloadPhase>} downloadPhases
*/
constructor(supportedLanguages, languageList, downloadPhases) {
@@ -2611,8 +2641,8 @@ var gMainPane = {
/**
* Determine the download phase of each language file.
*
- * @param {Array<{ langTag: string, displayName: string}} languageList.
- * @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded.
+ * @param {Array<{ langTag: string, displayName: string}>} languageList
+ * @returns {Promise<Map<string, DownloadPhase>>} Map the language tag to whether it is downloaded.
*/
static async createDownloadPhases(languageList) {
const downloadPhases = new Map();
@@ -2885,7 +2915,7 @@ var gMainPane = {
case "loading":
downloadButton.hidden = false;
deleteButton.hidden = true;
- downloadButton.setAttribute("disabled", true);
+ downloadButton.setAttribute("disabled", "true");
break;
}
}
@@ -3309,7 +3339,7 @@ var gMainPane = {
* The search mode is only available from the menu to change the primary browser
* language.
*
- * @param {{ search: boolean }}
+ * @param {{ search: boolean }} search
*/
showBrowserLanguagesSubDialog({ search }) {
// Record the telemetry event with an id to associate related actions.
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
@@ -16,6 +16,12 @@
/* import-globals-from /browser/base/content/utilityOverlay.js */
/* import-globals-from /toolkit/content/preferencesBindings.js */
+/** @import MozButton from "chrome://global/content/elements/moz-button.mjs" */
+/** @import {SettingConfig, SettingEmitChange} from "chrome://global/content/preferences/Setting.mjs" */
+/** @import {SettingControlConfig} from "chrome://browser/content/preferences/widgets/setting-control.mjs" */
+/** @import {SettingGroup, SettingGroupConfig} from "chrome://browser/content/preferences/widgets/setting-group.mjs" */
+/** @import {SettingPane, SettingPaneConfig} from "chrome://browser/content/preferences/widgets/setting-pane.mjs" */
+
"use strict";
var { AppConstants } = ChromeUtils.importESModule(
@@ -146,6 +152,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
});
});
+/** @type {Record<string, boolean>} */
const srdSectionPrefs = {};
XPCOMUtils.defineLazyPreferenceGetter(
srdSectionPrefs,
@@ -154,6 +161,9 @@ XPCOMUtils.defineLazyPreferenceGetter(
false
);
+/**
+ * @param {string} section
+ */
function srdSectionEnabled(section) {
if (!(section in srdSectionPrefs)) {
XPCOMUtils.defineLazyPreferenceGetter(
@@ -166,6 +176,7 @@ function srdSectionEnabled(section) {
return srdSectionPrefs.all || srdSectionPrefs[section];
}
+/** @type {Record<string, SettingPaneConfig>} */
const CONFIG_PANES = {
containers2: {
parent: "general",
@@ -228,10 +239,12 @@ function init_all() {
for (let [subPane, config] of Object.entries(CONFIG_PANES)) {
subPane = friendlyPrefCategoryNameToInternalName(subPane);
- let settingPane = document.createElement("setting-pane");
+ let settingPane = /** @type {SettingPane} */ (
+ document.createElement("setting-pane")
+ );
settingPane.name = subPane;
settingPane.config = config;
- settingPane.isSubPane = config.parent;
+ settingPane.isSubPane = !!config.parent;
document.getElementById("mainPrefPane").append(settingPane);
register_module(subPane, {
init() {
@@ -318,6 +331,12 @@ function onHashChange() {
gotoPref(null, "Hash");
}
+/**
+ * @param {string} [aCategory] The pane to show, defaults to the hash of URL or general
+ * @param {"Click"|"Initial"|"Hash"} [aShowReason]
+ * What triggered the navigation. Defaults to "Click" if aCategory is provided,
+ * otherwise "Initial".
+ */
async function gotoPref(
aCategory,
aShowReason = aCategory ? "Click" : "Initial"
@@ -326,7 +345,7 @@ async function gotoPref(
const kDefaultCategoryInternalName = "paneGeneral";
const kDefaultCategory = "general";
let hash = document.location.hash;
- let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+ let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName;
let breakIndex = category.indexOf("-");
// Subcategories allow for selecting smaller sections of the preferences
@@ -362,8 +381,8 @@ async function gotoPref(
element.hidden = true;
}
- item = categories.querySelector(
- ".category[value=" + CSS.escape(category) + "]"
+ item = /** @type {HTMLElement} */ (
+ categories.querySelector(".category[value=" + CSS.escape(category) + "]")
);
if (!item || item.hidden) {
unknownCategory = true;
@@ -397,8 +416,10 @@ async function gotoPref(
gLastCategory.category = category;
gLastCategory.subcategory = subcategory;
if (item) {
+ // @ts-ignore MozElements.RichListBox
categories.selectedItem = item;
} else {
+ // @ts-ignore MozElements.RichListBox
categories.clearSelection();
}
window.history.replaceState(category, document.title);
@@ -451,7 +472,10 @@ async function gotoPref(
categoryModule.handlePrefControlledSection?.();
// Record which category is shown
- Glean.aboutpreferences["show" + aShowReason].record({ value: category });
+ let gleanId = /** @type {"showClick" | "showHash" | "showInitial"} */ (
+ "show" + aShowReason
+ );
+ Glean.aboutpreferences[gleanId].record({ value: category });
document.dispatchEvent(
new CustomEvent("paneshown", {
@@ -464,9 +488,15 @@ async function gotoPref(
);
}
+/**
+ * @param {string} aQuery
+ * @param {string} aAttribute
+ */
function search(aQuery, aAttribute) {
let mainPrefPane = document.getElementById("mainPrefPane");
- let elements = mainPrefPane.children;
+ let elements = /** @type {HTMLElement[]} */ (
+ Array.from(mainPrefPane.children)
+ );
for (let element of elements) {
// If the "data-hidden-from-search" is "true", the
// element will not get considered during search.
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
@@ -564,58 +564,73 @@ if (Services.prefs.getBoolPref("privacy.ui.status_card", false)) {
pref: "browser.contentblocking.category",
get: prefValue => prefValue == "strict",
});
- Preferences.addSetting({
- id: "trackerCount",
- cachedValue: null,
- async setup(emitChange) {
- const now = Date.now();
- const aMonthAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
- const events = await lazy.TrackingDBService.getEventsByDateRange(
- now,
- aMonthAgo
- );
- const total = events.reduce((acc, day) => {
- return acc + day.getResultByName("count");
- }, 0);
- this.cachedValue = total;
- emitChange();
- },
- get() {
- return this.cachedValue;
- },
- });
- Preferences.addSetting({
- id: "appUpdateStatus",
- cachedValue: AppUpdater.STATUS.NO_UPDATER,
- async setup(emitChange) {
- if (AppConstants.MOZ_UPDATER && !gIsPackagedApp) {
- let appUpdater = new AppUpdater();
- let listener = (status, ..._args) => {
- this.cachedValue = status;
- emitChange();
- };
- appUpdater.addListener(listener);
- await appUpdater.check();
- return () => {
- appUpdater.removeListener(listener);
- appUpdater.stop();
- };
- }
- return () => {};
- },
- get() {
- return this.cachedValue;
- },
- set(value) {
- this.cachedValue = value;
- },
- });
+ Preferences.addSetting(
+ /** @type {{ cachedValue: number, loadTrackerCount: (emitChange: SettingEmitChange) => Promise<void> } & SettingConfig} */ ({
+ id: "trackerCount",
+ cachedValue: null,
+ async loadTrackerCount(emitChange) {
+ const now = Date.now();
+ const aMonthAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
+ /** @type {{ getResultByName: (_: string) => number }[]} */
+ const events = await lazy.TrackingDBService.getEventsByDateRange(
+ now,
+ aMonthAgo
+ );
+
+ const total = events.reduce((acc, day) => {
+ return acc + day.getResultByName("count");
+ }, 0);
+ this.cachedValue = total;
+ emitChange();
+ },
+ setup(emitChange) {
+ this.loadTrackerCount(emitChange);
+ },
+ get() {
+ return this.cachedValue;
+ },
+ })
+ );
+ Preferences.addSetting(
+ /** @type {{ cachedValue: any } & SettingConfig} */ ({
+ id: "appUpdateStatus",
+ cachedValue: AppUpdater.STATUS.NO_UPDATER,
+ setup(emitChange) {
+ if (AppConstants.MOZ_UPDATER && !gIsPackagedApp) {
+ let appUpdater = new AppUpdater();
+ /**
+ * @param {number} status
+ * @param {any[]} _args
+ */
+ let listener = (status, ..._args) => {
+ this.cachedValue = status;
+ emitChange();
+ };
+ appUpdater.addListener(listener);
+ appUpdater.check();
+ return () => {
+ appUpdater.removeListener(listener);
+ appUpdater.stop();
+ };
+ }
+ return () => {};
+ },
+ get() {
+ return this.cachedValue;
+ },
+ set(value) {
+ this.cachedValue = value;
+ },
+ })
+ );
}
/**
* This class is used to create Settings that are used to warn the user about
* potential misconfigurations. It should be passed into Preferences.addSetting
* to create the Preference for a <moz-box-item> because it creates
* separate members on pref.config
+ *
+ * @implements {SettingConfig}
*/
class WarningSettingConfig {
/**
@@ -685,9 +700,9 @@ class WarningSettingConfig {
* This initializes the Setting created with this config, starting listeners for all dependent
* Preferences and providing a cleanup callback to remove them
*
- * @param {Function} emitChange - a callback to be invoked any time that the Setting created
+ * @param {() => any} emitChange - a callback to be invoked any time that the Setting created
* with this config is changed
- * @returns {Function} a function that cleans up the state from this Setting, namely pref change listeners.
+ * @returns {() => any} a function that cleans up the state from this Setting, namely pref change listeners.
*/
setup(emitChange) {
for (let [getter, prefId] of Object.entries(this.prefMapping)) {
@@ -706,7 +721,7 @@ class WarningSettingConfig {
* "dismiss" action depending on the target, and those callbacks are defined
* in this class.
*
- * @param {Event} event - The event for the user click
+ * @param {PointerEvent} event - The event for the user click
*/
onUserClick(event) {
switch (event.target.id) {
@@ -1108,6 +1123,7 @@ if (Services.prefs.getBoolPref("privacy.ui.status_card", false)) {
);
}
+/** @type {SettingControlConfig[]} */
const SECURITY_WARNINGS = [
{
l10nId: "security-privacy-issue-warning-test",
@@ -1203,39 +1219,41 @@ const SECURITY_WARNINGS = [
},
];
-Preferences.addSetting({
- id: "securityWarningsGroup",
- makeSecurityWarningItems() {
- return SECURITY_WARNINGS.map(({ id, l10nId }) => ({
- id,
- l10nId,
- control: "moz-box-item",
- options: [
- {
- control: "moz-button",
- l10nId: "issue-card-reset-button",
- controlAttrs: { slot: "actions", size: "small", id: "reset" },
- },
- {
- control: "moz-button",
- l10nId: "issue-card-dismiss-button",
- controlAttrs: {
- slot: "actions",
- size: "small",
- iconsrc: "chrome://global/skin/icons/close.svg",
- id: "dismiss",
+Preferences.addSetting(
+ /** @type {{ makeSecurityWarningItems: () => SettingControlConfig[] } & SettingConfig} */ ({
+ id: "securityWarningsGroup",
+ makeSecurityWarningItems() {
+ return SECURITY_WARNINGS.map(({ id, l10nId }) => ({
+ id,
+ l10nId,
+ control: "moz-box-item",
+ options: [
+ {
+ control: "moz-button",
+ l10nId: "issue-card-reset-button",
+ controlAttrs: { slot: "actions", size: "small", id: "reset" },
},
- },
- ],
- }));
- },
- getControlConfig(config) {
- if (!config.items) {
- return { ...config, items: this.makeSecurityWarningItems() };
- }
- return config;
- },
-});
+ {
+ control: "moz-button",
+ l10nId: "issue-card-dismiss-button",
+ controlAttrs: {
+ slot: "actions",
+ size: "small",
+ iconsrc: "chrome://global/skin/icons/close.svg",
+ id: "dismiss",
+ },
+ },
+ ],
+ }));
+ },
+ getControlConfig(config) {
+ if (!config.items) {
+ return { ...config, items: this.makeSecurityWarningItems() };
+ }
+ return config;
+ },
+ })
+);
Preferences.addSetting({
id: "privacyCard",
@@ -1504,7 +1522,7 @@ Preferences.addSetting({
deps.blockUnwantedDownloads.value = value;
let malwareTable = Preferences.get("urlclassifier.malwareTable");
- let malware = malwareTable.value
+ let malware = /** @type {string} */ (malwareTable.value)
.split(",")
.filter(
x =>
@@ -1541,61 +1559,68 @@ Preferences.addSetting({
Preferences.addSetting({
id: "manageDataSettingsGroup",
});
-Preferences.addSetting({
- id: "siteDataSize",
- setup(emitChange) {
- let onUsageChanged = async () => {
- let [siteDataUsage, cacheUsage] = await Promise.all([
- SiteDataManager.getTotalUsage(),
- SiteDataManager.getCacheSize(),
- ]);
- let totalUsage = siteDataUsage + cacheUsage;
- let [value, unit] = DownloadUtils.convertByteUnits(totalUsage);
- this.usage = { value, unit };
-
- this.isUpdatingSites = false;
- emitChange();
- };
-
- let onUpdatingSites = () => {
- this.isUpdatingSites = true;
- emitChange();
- };
+Preferences.addSetting(
+ /** @type {{ isUpdatingSites: boolean, usage: { value: number, unit: string } | void } & SettingConfig} */ ({
+ id: "siteDataSize",
+ usage: null,
+ isUpdatingSites: false,
+ setup(emitChange) {
+ let onUsageChanged = async () => {
+ let [siteDataUsage, cacheUsage] = await Promise.all([
+ SiteDataManager.getTotalUsage(),
+ SiteDataManager.getCacheSize(),
+ ]);
+ let totalUsage = siteDataUsage + cacheUsage;
+ let [value, unit] = DownloadUtils.convertByteUnits(totalUsage);
+ this.usage = { value, unit };
+
+ this.isUpdatingSites = false;
+ emitChange();
+ };
- Services.obs.addObserver(onUsageChanged, "sitedatamanager:sites-updated");
- Services.obs.addObserver(onUpdatingSites, "sitedatamanager:updating-sites");
+ let onUpdatingSites = () => {
+ this.isUpdatingSites = true;
+ emitChange();
+ };
- return () => {
- Services.obs.removeObserver(
- onUsageChanged,
- "sitedatamanager:sites-updated"
- );
- Services.obs.removeObserver(
+ Services.obs.addObserver(onUsageChanged, "sitedatamanager:sites-updated");
+ Services.obs.addObserver(
onUpdatingSites,
"sitedatamanager:updating-sites"
);
- };
- },
- getControlConfig(config) {
- if (this.isUpdatingSites || !this.usage) {
- // Data not retrieved yet, show a loading state.
+
+ return () => {
+ Services.obs.removeObserver(
+ onUsageChanged,
+ "sitedatamanager:sites-updated"
+ );
+ Services.obs.removeObserver(
+ onUpdatingSites,
+ "sitedatamanager:updating-sites"
+ );
+ };
+ },
+ getControlConfig(config) {
+ if (this.isUpdatingSites || !this.usage) {
+ // Data not retrieved yet, show a loading state.
+ return {
+ ...config,
+ l10nId: "sitedata-total-size-calculating",
+ };
+ }
+
+ let { value, unit } = this.usage;
return {
...config,
- l10nId: "sitedata-total-size-calculating",
+ l10nId: "sitedata-total-size2",
+ l10nArgs: {
+ value,
+ unit,
+ },
};
- }
-
- let { value, unit } = this.usage;
- return {
- ...config,
- l10nId: "sitedata-total-size2",
- l10nArgs: {
- value,
- unit,
- },
- };
- },
-});
+ },
+ })
+);
Preferences.addSetting({
id: "deleteOnCloseInfo",
@@ -1605,91 +1630,104 @@ Preferences.addSetting({
},
});
-Preferences.addSetting({
- id: "clearSiteDataButton",
- setup(emitChange) {
- let onSitesUpdated = async () => {
- this.isUpdatingSites = false;
- emitChange();
- };
-
- let onUpdatingSites = () => {
- this.isUpdatingSites = true;
- emitChange();
- };
+Preferences.addSetting(
+ /** @type {{ isUpdatingSites: boolean } & SettingConfig} */ ({
+ id: "clearSiteDataButton",
+ isUpdatingSites: false,
+ setup(emitChange) {
+ let onSitesUpdated = async () => {
+ this.isUpdatingSites = false;
+ emitChange();
+ };
- Services.obs.addObserver(onSitesUpdated, "sitedatamanager:sites-updated");
- Services.obs.addObserver(onUpdatingSites, "sitedatamanager:updating-sites");
+ let onUpdatingSites = () => {
+ this.isUpdatingSites = true;
+ emitChange();
+ };
- return () => {
- Services.obs.removeObserver(
- onSitesUpdated,
- "sitedatamanager:sites-updated"
- );
- Services.obs.removeObserver(
+ Services.obs.addObserver(onSitesUpdated, "sitedatamanager:sites-updated");
+ Services.obs.addObserver(
onUpdatingSites,
"sitedatamanager:updating-sites"
);
- };
- },
- onUserClick() {
- let uri;
- if (useOldClearHistoryDialog) {
- uri = "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml";
- } else {
- uri = "chrome://browser/content/sanitize_v2.xhtml";
- }
- gSubDialog.open(
- uri,
- {
- features: "resizable=no",
- },
- {
- mode: "clearSiteData",
+ return () => {
+ Services.obs.removeObserver(
+ onSitesUpdated,
+ "sitedatamanager:sites-updated"
+ );
+ Services.obs.removeObserver(
+ onUpdatingSites,
+ "sitedatamanager:updating-sites"
+ );
+ };
+ },
+ onUserClick() {
+ let uri;
+ if (useOldClearHistoryDialog) {
+ uri =
+ "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml";
+ } else {
+ uri = "chrome://browser/content/sanitize_v2.xhtml";
}
- );
- },
- disabled() {
- return this.isUpdatingSites;
- },
-});
-Preferences.addSetting({
- id: "siteDataSettings",
- setup(emitChange) {
- let onSitesUpdated = async () => {
- this.isUpdatingSites = false;
- emitChange();
- };
- let onUpdatingSites = () => {
- this.isUpdatingSites = true;
- emitChange();
- };
+ gSubDialog.open(
+ uri,
+ {
+ features: "resizable=no",
+ },
+ {
+ mode: "clearSiteData",
+ }
+ );
+ },
+ disabled() {
+ return this.isUpdatingSites;
+ },
+ })
+);
+Preferences.addSetting(
+ /** @type {{ isUpdatingSites: boolean } & SettingConfig} */ ({
+ id: "siteDataSettings",
+ isUpdatingSites: false,
+ setup(emitChange) {
+ let onSitesUpdated = async () => {
+ this.isUpdatingSites = false;
+ emitChange();
+ };
- Services.obs.addObserver(onSitesUpdated, "sitedatamanager:sites-updated");
- Services.obs.addObserver(onUpdatingSites, "sitedatamanager:updating-sites");
+ let onUpdatingSites = () => {
+ this.isUpdatingSites = true;
+ emitChange();
+ };
- return () => {
- Services.obs.removeObserver(
- onSitesUpdated,
- "sitedatamanager:sites-updated"
- );
- Services.obs.removeObserver(
+ Services.obs.addObserver(onSitesUpdated, "sitedatamanager:sites-updated");
+ Services.obs.addObserver(
onUpdatingSites,
"sitedatamanager:updating-sites"
);
- };
- },
- onUserClick() {
- gSubDialog.open(
- "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml"
- );
- },
- disabled() {
- return this.isUpdatingSites;
- },
-});
+
+ return () => {
+ Services.obs.removeObserver(
+ onSitesUpdated,
+ "sitedatamanager:sites-updated"
+ );
+ Services.obs.removeObserver(
+ onUpdatingSites,
+ "sitedatamanager:updating-sites"
+ );
+ };
+ },
+ onUserClick() {
+ gSubDialog.open(
+ "chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml"
+ );
+ },
+ disabled() {
+ return this.isUpdatingSites;
+ },
+ })
+);
Preferences.addSetting({
id: "cookieExceptions",
onUserClick() {
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.mjs b/browser/components/preferences/widgets/setting-control/setting-control.mjs
@@ -15,25 +15,55 @@ import {
SettingElement,
spread,
} from "chrome://browser/content/preferences/widgets/setting-element.mjs";
+import { MozBaseInputElement } from "chrome://global/content/lit-utils.mjs";
+import { MozRadio } from "chrome://global/content/elements/moz-radio-group.mjs";
+import MozInputFolder from "chrome://global/content/elements/moz-input-folder.mjs";
-/** @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"; */
+/** @import { LitElement, Ref, TemplateResult } from "chrome://global/content/vendor/lit.all.mjs" */
+/** @import { SettingElementConfig } from "chrome://browser/content/preferences/widgets/setting-element.mjs" */
+/** @import { Setting } from "chrome://global/content/preferences/Setting.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
+ * @typedef {Object} SettingNestedConfig
+ * @property {SettingControlConfig[]} [items] Additional nested SettingControls to render.
+ * @property {SettingOptionConfig[]} [options]
+ * Additional nested plain elements to render (may have SettingControls nested within them, though).
+ */
+
+/**
+ * @typedef {Object} SettingOptionConfigExtensions
+ * @property {string} [control]
+ * The element tag to render, default assumed based on parent control.
+ * @property {any} [value] A value to set on the option.
+ */
+
+/**
+ * @typedef {Object} SettingControlConfigExtensions
+ * @property {string} id
+ * The ID for the Setting, also set in the DOM unless overridden with controlAttrs.id
+ * @property {string} [control] The element to render, default to "moz-checkbox".
+ * @property {string} [controllingExtensionInfo]
+ * ExtensionSettingStore id for checking if a setting is controlled by an extension.
+ */
+
+/**
+ * @typedef {SettingOptionConfigExtensions & SettingElementConfig & SettingNestedConfig} SettingOptionConfig
+ * @typedef {SettingControlConfigExtensions & SettingElementConfig & SettingNestedConfig} SettingControlConfig
+ * @typedef {{ control: SettingControl } & HTMLElement} SettingControlChild
+ */
+
+/**
+ * @template T=Event
+ * @typedef {T & { target: SettingControlChild }} SettingControlEvent
+ * SettingControlEvent simplifies the types in this file, but causes issues when
+ * doing more involved work when used in Setting.mjs. When casting the
+ * `event.target` to a more specific type like MozButton (or even
+ * HTMLButtonElement) it gets flagged as being too different from SettingControlChild.
*/
/**
* 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>
*/
const KNOWN_OPTIONS = new Map([
["moz-radio-group", literal`moz-radio`],
@@ -89,6 +119,7 @@ export class SettingControl extends SettingElement {
constructor() {
super();
+ /** @type {Ref<LitElement>} */
this.controlRef = createRef();
/**
@@ -102,7 +133,7 @@ export class SettingControl extends SettingElement {
this.setting = undefined;
/**
- * @type {PreferencesSettingsConfig | undefined}
+ * @type {SettingControlConfig | undefined}
*/
this.config = undefined;
@@ -122,7 +153,7 @@ export class SettingControl extends SettingElement {
}
focus() {
- this.controlRef.value.focus();
+ this.controlEl.focus();
}
get controlEl() {
@@ -162,9 +193,6 @@ export class SettingControl extends SettingElement {
}
}
- /**
- * @type {MozLitElement['updated']}
- */
updated() {
const control = this.controlRef?.value;
if (!control) {
@@ -189,7 +217,7 @@ export class SettingControl extends SettingElement {
* or controlled by an extension but not both.
*
* @override
- * @param {PreferencesSettingsConfig} config
+ * @param {SettingElementConfig} config
* @returns {ReturnType<SettingElement['getCommonPropertyMapping']>}
*/
getCommonPropertyMapping(config) {
@@ -203,7 +231,7 @@ export class SettingControl extends SettingElement {
/**
* The default properties for an option.
*
- * @param {PreferencesSettingConfigNestedControlOption | SettingNestedElementOption} config
+ * @param {SettingOptionConfig} config
*/
getOptionPropertyMapping(config) {
const props = this.getCommonPropertyMapping(config);
@@ -215,6 +243,8 @@ export class SettingControl extends SettingElement {
/**
* The default properties for this control.
+ *
+ * @param {SettingControlConfig} config
*/
getControlPropertyMapping(config) {
const props = this.getCommonPropertyMapping(config);
@@ -236,23 +266,34 @@ export class SettingControl extends SettingElement {
};
/**
- * @param {MozCheckbox | HTMLInputElement} el
- * @returns {boolean | string | undefined}
+ * @param {HTMLElement} el
+ * @returns {any}
*/
controlValue(el) {
- if (el.constructor.activatedProperty && el.localName != "moz-radio") {
- return el[el.constructor.activatedProperty];
- } else if (el.localName == "moz-input-folder") {
+ if (el instanceof MozBaseInputElement && !(el instanceof MozRadio)) {
+ let C = /** @type {typeof MozBaseInputElement} */ (el.constructor);
+ return el[C.activatedProperty];
+ }
+ if (el instanceof MozInputFolder) {
return el.folder;
}
- return el.value;
+ return "value" in el ? el.value : null;
}
- // Called by our parent when our input changed.
+ /**
+ * Called by our parent when our input changed.
+ *
+ * @param {SettingControlChild} el
+ */
onChange(el) {
this.setting.userChange(this.controlValue(el));
}
+ /**
+ * Called by our parent when our input is clicked.
+ *
+ * @param {MouseEvent} event
+ */
onClick(event) {
this.setting.userClick(event);
}
@@ -273,9 +314,14 @@ export class SettingControl extends SettingElement {
this.showEnableExtensionMessage = false;
}
+ /**
+ * @param {MouseEvent} event
+ */
navigateToAddons(event) {
- if (event.target.matches("a[data-l10n-name='addons-link']")) {
+ let link = /** @type {HTMLAnchorElement} */ (event.target);
+ if (link.matches("a[data-l10n-name='addons-link']")) {
event.preventDefault();
+ // @ts-ignore
let mainWindow = window.browsingContext.topChromeWindow;
mainWindow.BrowserAddonUI.openAddonsMgr("addons://list/theme");
}
@@ -292,8 +338,8 @@ export class SettingControl extends SettingElement {
/**
* Prepare nested item config and settings.
*
- * @param {PreferencesSettingConfigNestedControlOption} config
- * @returns {Array<string>}
+ * @param {SettingControlConfig | SettingOptionConfig} config
+ * @returns {TemplateResult[]}
*/
itemsTemplate(config) {
if (!config.items) {
@@ -319,8 +365,8 @@ export class SettingControl extends SettingElement {
/**
* Prepares any children (and any of its children's children) that this element may need.
*
- * @param {PreferencesSettingConfigNestedControlOption | SettingNestedElementOption} config
- * @returns {Array<string>}
+ * @param {SettingOptionConfig} config
+ * @returns {TemplateResult[]}
*/
optionsTemplate(config) {
if (!config.options) {
@@ -331,9 +377,11 @@ export class SettingControl extends SettingElement {
let optionTag = opt.control
? unsafeStatic(opt.control)
: KNOWN_OPTIONS.get(control);
+ let children =
+ "items" in opt ? this.itemsTemplate(opt) : this.optionsTemplate(opt);
return staticHtml`<${optionTag}
${spread(this.getOptionPropertyMapping(opt))}
- >${"items" in opt ? this.itemsTemplate(opt) : this.optionsTemplate(opt)}</${optionTag}>`;
+ >${children}</${optionTag}>`;
});
}
diff --git a/browser/components/preferences/widgets/setting-element/setting-element.mjs b/browser/components/preferences/widgets/setting-element/setting-element.mjs
@@ -11,6 +11,19 @@ import {
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+/** @import { AttributePart } from "chrome://global/content/vendor/lit.all.mjs" */
+
+/**
+ * @typedef {Object} SettingElementConfig
+ * @property {string} [id] - The ID for the Setting, this should match the layout id
+ * @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 {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
+ */
+
/**
* A Lit directive that applies all properties of an object to a DOM element.
*
@@ -38,9 +51,10 @@ class SpreadDirective extends Directive {
* Render nothing by default as all changes are made in update using DOM APIs
* on the element directly.
*
- * @returns {typeof nothing}
+ * @param {Record<string, unknown>} props The props to apply to this element.
*/
- render() {
+ // eslint-disable-next-line no-unused-vars
+ render(props) {
return nothing;
}
@@ -59,7 +73,6 @@ class SpreadDirective extends Directive {
// 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)) {
@@ -75,6 +88,7 @@ class SpreadDirective extends Directive {
if (key.startsWith("?")) {
el.toggleAttribute(key.slice(1), Boolean(value));
} else if (key.startsWith(".")) {
+ // @ts-ignore
el[key.slice(1)] = value;
} else if (key.startsWith("@")) {
throw new Error(
@@ -98,28 +112,19 @@ export class SettingElement extends MozLitElement {
/**
* The default properties that the setting element accepts.
*
- * @param {PreferencesSettingsConfig} config
- * @returns {Record<string, any>}
+ * @param {SettingElementConfig} config
*/
getCommonPropertyMapping(config) {
- /**
- * @type {Record<string, any>}
- */
- const result = {
+ return {
id: config.id,
+ "data-l10n-id": config.l10nId ? config.l10nId : undefined,
"data-l10n-args": config.l10nArgs
? JSON.stringify(config.l10nArgs)
: undefined,
".iconSrc": config.iconSrc,
"data-subcategory": config.subcategory,
+ ".supportPage": config.supportPage ? config.supportPage : undefined,
...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
@@ -8,8 +8,19 @@ import {
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" */
+/** @import { SettingElementConfig } from "chrome://browser/content/preferences/widgets/setting-element.mjs" */
+/** @import { SettingControlConfig, SettingControlEvent } from "../setting-control/setting-control.mjs" */
+/** @import { Preferences } from "chrome://global/content/preferences/Preferences.mjs" */
+
+/**
+ * @typedef {object} SettingGroupConfigExtensions
+ * @property {SettingControlConfig[]} items Array of SettingControlConfigs to render.
+ * @property {number} [headingLevel] A heading level to create the legend as (1-6).
+ * @property {boolean} [inProgress]
+ * Hide this section unless the browser.settings-redesign.enabled or
+ * browser.settings-redesign.<groupid>.enabled prefs are true.
+ */
+/** @typedef {SettingElementConfig & SettingGroupConfigExtensions} SettingGroupConfig */
const CLICK_HANDLERS = new Set([
"dialog-button",
@@ -30,7 +41,7 @@ export class SettingGroup extends SettingElement {
this.getSetting = undefined;
/**
- * @type {PreferencesSettingsConfig | undefined}
+ * @type {SettingGroupConfig | undefined}
*/
this.config = undefined;
}
@@ -51,9 +62,10 @@ export class SettingGroup extends SettingElement {
async handleVisibilityChange() {
await this.updateComplete;
+ // @ts-expect-error bug 1997478
let hasVisibleControls = [...this.controlEls].some(el => !el.hidden);
this.hidden = !hasVisibleControls;
- let groupbox = this.closest("groupbox");
+ let groupbox = /** @type {XULElement} */ (this.closest("groupbox"));
if (hasVisibleControls) {
this.removeAttribute("data-hidden-from-search");
if (groupbox && groupbox.hasAttribute("data-hidden-by-setting-group")) {
@@ -73,6 +85,7 @@ export class SettingGroup extends SettingElement {
async getUpdateComplete() {
let result = await super.getUpdateComplete();
+ // @ts-expect-error bug 1997478
await Promise.all([...this.controlEls].map(el => el.updateComplete));
return result;
}
@@ -81,24 +94,31 @@ export class SettingGroup extends SettingElement {
* Notify child controls when their input has fired an event. When controls
* are nested the parent receives events for the nested controls, so this is
* actually easier to manage here; it also registers fewer listeners.
+ *
+ * @param {SettingControlEvent<InputEvent>} e
*/
onChange(e) {
let inputEl = e.target;
- let control = inputEl.control;
- control?.onChange(inputEl);
+ inputEl.control?.onChange(inputEl);
}
+ /**
+ * Notify child controls when their input has been clicked. When controls
+ * are nested the parent receives events for the nested controls, so this is
+ * actually easier to manage here; it also registers fewer listeners.
+ *
+ * @param {SettingControlEvent<MouseEvent>} e
+ */
onClick(e) {
- if (!CLICK_HANDLERS.has(e.target.localName)) {
+ let inputEl = e.target;
+ if (!CLICK_HANDLERS.has(inputEl.localName)) {
return;
}
- let inputEl = e.target;
- let control = inputEl.control;
- control?.onClick(e);
+ inputEl.control?.onClick(e);
}
/**
- * @param {PreferencesSettingsConfig} item
+ * @param {SettingControlConfig} item
*/
itemTemplate(item) {
let setting = this.getSetting(item.id);
diff --git a/browser/components/preferences/widgets/setting-pane/setting-pane.mjs b/browser/components/preferences/widgets/setting-pane/setting-pane.mjs
@@ -5,6 +5,13 @@
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+/**
+ * @typedef {object} SettingPaneConfig
+ * @property {string} [parent] The pane that links to this one.
+ * @property {string} l10nId Fluent id for the heading/description.
+ * @property {string[]} groupIds What setting groups should be rendered.
+ */
+
export class SettingPane extends MozLitElement {
static properties = {
name: { type: String },
@@ -12,6 +19,16 @@ export class SettingPane extends MozLitElement {
config: { type: Object },
};
+ constructor() {
+ super();
+ /** @type {string} */
+ this.name = undefined;
+ /** @type {boolean} */
+ this.isSubPane = false;
+ /** @type {SettingPaneConfig} */
+ this.config = undefined;
+ }
+
createRenderRoot() {
return this;
}
@@ -50,6 +67,7 @@ export class SettingPane extends MozLitElement {
document.getElementById("categories").append(categoryButton);
}
+ /** @param {string} groupId */
groupTemplate(groupId) {
return html`<setting-group groupid=${groupId}></setting-group>`;
}
diff --git a/toolkit/components/extensions/ExtensionSettingsStore.sys.mjs b/toolkit/components/extensions/ExtensionSettingsStore.sys.mjs
@@ -548,7 +548,7 @@ export var ExtensionSettingsStore = {
* The id of the extension for which the setting is being retrieved.
* Defaults to undefined, in which case the top setting is returned.
*
- * @returns {object} An object with properties for key, value and id.
+ * @returns {{ id: string, key: string, value: string}} An object with properties for key, value and id.
*/
getSetting(type, key, id) {
return getItem(type, key, id);
diff --git a/toolkit/content/preferences/AsyncSetting.mjs b/toolkit/content/preferences/AsyncSetting.mjs
@@ -6,21 +6,25 @@ const { EventEmitter } = ChromeUtils.importESModule(
"resource://gre/modules/EventEmitter.sys.mjs"
);
+/** @import { SettingControlConfig } from "chrome://browser/content/preferences/widgets/setting-control.mjs" */
+/** @import { SettingConfig, SettingValue } from "./Setting.mjs" */
+
/**
* This is the interface for the async setting classes to implement.
*
* For the actual implementation see AsyncSettingMixin.
*/
export class AsyncSetting extends EventEmitter {
- /** @type {string} */
static id = "";
- /** @type {Object} */
+ /** @type {SettingConfig['controllingExtensionInfo']} */
static controllingExtensionInfo;
+ /** @type {SettingValue} */
defaultValue = "";
defaultDisabled = false;
defaultVisible = true;
+ /** @type {Partial<SettingControlConfig>} */
defaultGetControlConfig = {};
/**
@@ -35,7 +39,7 @@ export class AsyncSetting extends EventEmitter {
* Setup any external listeners that are required for managing this
* setting's state. When the state needs to update the Setting.emitChange method should be called.
*
- * @returns {Function | void} Teardown function to clean up external listeners.
+ * @returns {ReturnType<SettingConfig['setup']>} Teardown function to clean up external listeners.
*/
setup() {}
@@ -43,7 +47,7 @@ export class AsyncSetting extends EventEmitter {
* Get the value of this setting.
*
* @abstract
- * @returns {Promise<boolean | number | string | void>}
+ * @returns {Promise<SettingValue>}
*/
async get() {}
@@ -51,10 +55,11 @@ export class AsyncSetting extends EventEmitter {
* Set the value of this setting.
*
* @abstract
- * @param value {any} The value from the input that triggered the update.
+ * @param {SettingValue} value The value from the input that triggered the update.
* @returns {Promise<void>}
*/
- async set() {}
+ // eslint-disable-next-line no-unused-vars
+ async set(value) {}
/**
* Whether the control should be disabled.
@@ -78,7 +83,7 @@ export class AsyncSetting extends EventEmitter {
* Override the initial control config. This will be spread into the
* initial config, with this object taking precedence.
*
- * @returns {Promise<Object>}
+ * @returns {Promise<Partial<SettingControlConfig>>}
*/
async getControlConfig() {
return {};
@@ -88,15 +93,25 @@ export class AsyncSetting extends EventEmitter {
* Callback fired after a user has changed the setting's value. Useful for
* recording telemetry.
*
- * @param value {any}
- * @returns {Promise<void>}
+ * @param {SettingValue} value
*/
- async onUserChange() {}
+ // eslint-disable-next-line no-unused-vars
+ onUserChange(value) {}
+
+ /**
+ * Callback fired after a user has clicked a setting's control.
+ *
+ * @param {MouseEvent} event
+ */
+ // eslint-disable-next-line no-unused-vars
+ onUserClick(event) {}
}
/**
* Wraps an AsyncSetting and adds caching of values to provide a synchronous
* API to the Setting class.
+ *
+ * @implements {SettingConfig}
*/
export class AsyncSettingHandler {
/** @type {AsyncSetting} */
@@ -105,41 +120,48 @@ export class AsyncSettingHandler {
/** @type {Function} */
#emitChange;
- /** @type {import("./Setting.mjs").PreferenceSettingDepsMap} */
- deps;
+ /** @type {string} */
+ pref;
+
+ /**
+ * Dependencies are not supported on AsyncSettings, but we include an empty
+ * array for consistency with {@link SettingConfig}.
+ *
+ * @type {string[]}
+ */
+ deps = [];
- /** @type {Setting} */
- setting;
+ /** @type {SettingConfig['controllingExtensionInfo']} */
+ controllingExtensionInfo;
/**
- * @param {AsyncSetting} asyncSetting
+ * @param {string} id
+ * @param {typeof AsyncSetting} AsyncSettingClass
*/
- constructor(asyncSetting) {
- this.asyncSetting = asyncSetting;
+ constructor(id, AsyncSettingClass) {
+ this.asyncSetting = new AsyncSettingClass();
+ this.id = id;
+ this.controllingExtensionInfo = AsyncSettingClass.controllingExtensionInfo;
this.#emitChange = () => {};
// Initialize cached values with defaults
- this.cachedValue = asyncSetting.defaultValue;
- this.cachedDisabled = asyncSetting.defaultDisabled;
- this.cachedVisible = asyncSetting.defaultVisible;
- this.cachedGetControlConfig = asyncSetting.defaultGetControlConfig;
+ this.cachedValue = this.asyncSetting.defaultValue;
+ this.cachedDisabled = this.asyncSetting.defaultDisabled;
+ this.cachedVisible = this.asyncSetting.defaultVisible;
+ this.cachedGetControlConfig = this.asyncSetting.defaultGetControlConfig;
// Listen for change events from the async setting
this.asyncSetting.on("change", () => this.refresh());
}
/**
- * @param emitChange {Function}
- * @param deps {Record<string, Setting>}
- * @param setting {Setting}
- * @returns {Function | void}
+ * @param {() => any} emitChange
+ * @returns {ReturnType<SettingConfig['setup']>}
*/
- setup(emitChange, deps, setting) {
+ setup(emitChange) {
let teardown = this.asyncSetting.setup();
this.#emitChange = emitChange;
- this.deps = deps;
- this.setting = setting;
this.refresh();
return teardown;
@@ -164,14 +186,14 @@ export class AsyncSettingHandler {
}
/**
- * @returns {boolean | number | string | void}
+ * @returns {SettingValue}
*/
get() {
return this.cachedValue;
}
/**
- * @param value {any}
+ * @param {any} value
* @returns {Promise<void>}
*/
set(value) {
@@ -193,8 +215,8 @@ export class AsyncSettingHandler {
}
/**
- * @param config {Object}
- * @returns {Object}
+ * @param {SettingControlConfig} config
+ * @returns {SettingControlConfig}
*/
getControlConfig(config) {
return {
@@ -204,10 +226,16 @@ export class AsyncSettingHandler {
}
/**
- * @param value {any}
- * @returns {Promise<void>}
+ * @param {SettingValue} value
*/
onUserChange(value) {
return this.asyncSetting.onUserChange(value);
}
+
+ /**
+ * @param {MouseEvent} event
+ */
+ onUserClick(event) {
+ this.asyncSetting.onUserClick(event);
+ }
}
diff --git a/toolkit/content/preferences/Preference.mjs b/toolkit/content/preferences/Preference.mjs
@@ -9,7 +9,11 @@ const { EventEmitter } = ChromeUtils.importESModule(
);
/**
- * @typedef {object} PreferenceConfigInfo
+ * @typedef {string | boolean | number | nsIFile | void} PreferenceValue
+ */
+
+/**
+ * @typedef {object} PreferenceConfig
* @property {string} id
* @property {string} type
* @property {boolean} [inverted]
@@ -29,12 +33,12 @@ function getElementsByAttribute(name, value) {
export class Preference extends EventEmitter {
/**
- * @type {string | undefined | null}
+ * @type {PreferenceValue}
*/
_value;
/**
- * @param {PreferenceConfigInfo} configInfo
+ * @param {PreferenceConfig} configInfo
*/
constructor({ id, type, inverted }) {
super();
@@ -89,7 +93,7 @@ export class Preference extends EventEmitter {
}
if (aElement.labels?.length) {
for (let label of aElement.labels) {
- label.toggleAttribute("disabled", this.locked);
+ /** @type {Element} */ (label).toggleAttribute("disabled", this.locked);
}
}
@@ -117,9 +121,14 @@ export class Preference extends EventEmitter {
* the property setter does not yet exist by setting the same attribute
* on the XUL element using DOM apis and assuming the element's
* constructor or property getters appropriately handle this state.
+ *
+ * @param {Element} element
+ * @param {string} attribute
+ * @param {string} value
*/
function setValue(element, attribute, value) {
if (attribute in element) {
+ // @ts-expect-error The property might not be writable...
element[attribute] = value;
} else if (attribute === "checked" || attribute === "pressed") {
// The "checked" attribute can't simply be set to the specified value;
@@ -151,7 +160,7 @@ export class Preference extends EventEmitter {
/**
* @param {HTMLElement} aElement
- * @returns {string}
+ * @returns {PreferenceValue}
*/
getElementValue(aElement) {
if (Preferences._syncToPrefListeners.has(aElement)) {
@@ -170,10 +179,14 @@ export class Preference extends EventEmitter {
* attribute is a property on the element's node API. If the property
* is not present in the API, then assume its value is contained in
* an attribute, as is the case before a binding has been attached.
+ *
+ * @param {Element} element
+ * @param {string} attribute
+ * @returns {any}
*/
function getValue(element, attribute) {
if (attribute in element) {
- return element[attribute];
+ return element[/** @type {keyof typeof element} */ (attribute)];
}
return element.getAttribute(attribute);
}
@@ -181,7 +194,8 @@ export class Preference extends EventEmitter {
if (
aElement.localName == "checkbox" ||
aElement.localName == "moz-checkbox" ||
- (aElement.localName == "input" && aElement.type == "checkbox")
+ (aElement.localName == "input" &&
+ /** @type {HTMLInputElement} */ (aElement).type == "checkbox")
) {
value = getValue(aElement, "checked");
} else if (aElement.localName == "moz-toggle") {
@@ -201,7 +215,6 @@ export class Preference extends EventEmitter {
/**
* @param {HTMLElement} aElement
- * @returns {boolean}
*/
isElementEditable(aElement) {
switch (aElement.localName) {
@@ -241,14 +254,14 @@ export class Preference extends EventEmitter {
}
/**
- * @type {Preference['_value']}
+ * @type {PreferenceValue}
*/
get value() {
return this._value;
}
/**
- * @param {string} val
+ * @param {PreferenceValue} val
*/
set value(val) {
if (this.value !== val) {
@@ -293,6 +306,7 @@ export class Preference extends EventEmitter {
return Services.prefs.prefHasUserValue(this.id) && this.value !== undefined;
}
+ /** @returns {PreferenceValue} */
get defaultValue() {
this._useDefault = true;
const val = this.valueFromPreferences;
@@ -305,7 +319,7 @@ export class Preference extends EventEmitter {
}
/**
- * @type {string}
+ * @type {PreferenceValue}
*/
get valueFromPreferences() {
try {
@@ -344,7 +358,7 @@ export class Preference extends EventEmitter {
}
/**
- * @param {string} val
+ * @param {PreferenceValue} val
*/
set valueFromPreferences(val) {
// Exit early if nothing to do.
@@ -361,23 +375,23 @@ export class Preference extends EventEmitter {
// Force a resync of preferences with value.
switch (this.type) {
case "int":
- Services.prefs.setIntPref(this.id, val);
+ Services.prefs.setIntPref(this.id, /** @type {number} */ (val));
break;
case "bool":
- Services.prefs.setBoolPref(this.id, this.inverted ? !val : val);
+ Services.prefs.setBoolPref(this.id, this.inverted ? !val : !!val);
break;
case "wstring": {
const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
Ci.nsIPrefLocalizedString
);
- pls.data = val;
+ pls.data = /** @type {string} */ (val);
Services.prefs.setComplexValue(this.id, Ci.nsIPrefLocalizedString, pls);
break;
}
case "string":
case "unichar":
case "fontname":
- Services.prefs.setStringPref(this.id, val);
+ Services.prefs.setStringPref(this.id, /** @type {string} */ (val));
break;
case "file": {
let lf;
@@ -388,7 +402,7 @@ export class Preference extends EventEmitter {
lf.initWithPath(val);
}
} else {
- lf = val.QueryInterface(Ci.nsIFile);
+ lf = /** @type {nsIFile} */ (val).QueryInterface(Ci.nsIFile);
}
Services.prefs.setComplexValue(this.id, Ci.nsIFile, lf);
break;
diff --git a/toolkit/content/preferences/Preferences.mjs b/toolkit/content/preferences/Preferences.mjs
@@ -6,134 +6,17 @@ import { AsyncSetting } from "chrome://global/content/preferences/AsyncSetting.m
import { Preference } from "chrome://global/content/preferences/Preference.mjs";
import { Setting } from "chrome://global/content/preferences/Setting.mjs";
-/** @import {PreferenceConfigInfo} from "chrome://global/content/preferences/Preference.mjs" */
-/** @import {PreferenceSettingDepsMap} from "chrome://global/content/preferences/Setting.mjs" */
+/** @import {PreferenceConfig} from "chrome://global/content/preferences/Preference.mjs" */
+/** @import {SettingConfig} from "chrome://global/content/preferences/Setting.mjs" */
+/** @import {DeferredTask} from "resource://gre/modules/DeferredTask.sys.mjs" */
/**
- * @callback PreferenceSettingVisibleFunction
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {boolean | string | undefined} If truthy shows the setting in the UI, or hides it if not
- */
-
-/**
- * Gets the value of a {@link PreferencesSettingsConfig}.
- *
- * @callback PreferenceSettingGetter
- * @param {string | number} val - The value that was retrieved from the preferences backend
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {any} - The value to set onto the setting
- */
-
-/**
- * Sets the value of a {@link PreferencesSettingsConfig}.
- *
- * @callback PreferenceSettingSetter
- * @param {string | undefined} val - The value/pressed/checked from the input (control) associated with the setting
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {void}
- */
-
-/**
- * @callback PreferencesSettingOnUserChangeFunction
- * @param {string} val - The value/pressed/checked from the input of the control associated with the setting
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {void}
- */
-
-/**
- * @callback PreferencesSettingConfigDisabledFunction
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {boolean}
- */
-
-/**
- * @callback PreferencesSettingGetControlConfigFunction
- * @param {PreferencesSettingsConfig} config
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {PreferencesSettingsConfig | undefined}
- */
-
-/**
- * @callback PreferencesSettingConfigTeardownFunction
- * @returns {void}
- */
-
-/**
- * @callback PreferencesSettingConfigSetupFunction
- * @param {Function} emitChange
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {PreferencesSettingConfigTeardownFunction | void}
- */
-
-/**
- * @callback PreferencesSettingConfigOnUserClickFunction
- * @param {Event} event
- * @param {PreferenceSettingDepsMap} deps
- * @param {Setting} setting
- * @returns {void}
- */
-
-/**
- * @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
- * @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.
- * @property {PreferenceSettingSetter} [set] - Function to set the value of the setting. Optional if {@link PreferencesSettingsConfig#pref} is set.
- * @property {PreferencesSettingGetControlConfigFunction} [getControlConfig] - Function that allows the setting to modify its layout, this is intended to be used to provide the options, {@link PreferencesSettingsConfig#l10nId} or {@link PreferencesSettingsConfig#l10nArgs} data if necessary, but technically it can change anything (that doesn't mean it will have any effect though).
- * @property {PreferencesSettingOnUserChangeFunction} [onUserChange] - A function that will be called when the setting
- * has been modified by the user, it is passed the value/pressed/checked from its input. NOTE: This should be used for
- * 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 {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
- * ensure the UI stays in sync if possible.
- * @property {PreferencesSettingConfigDisabledFunction} [disabled] - A function to determine if a setting should be disabled
- * @property {PreferencesSettingConfigOnUserClickFunction} [onUserClick] - A function that will be called when a setting has been
- * clicked, the element name must be included in the CLICK_HANDLERS array
- * 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 {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
+ * @typedef {{ _deferredValueUpdateTask: DeferredTask }} DeferredValueObject
+ * @typedef {DeferredValueObject & HTMLElement} DeferredValueHTMLElement
*/
+/** @type {{ DeferredTask: typeof DeferredTask }} */
+// @ts-expect-error bug 1996860
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
@@ -158,7 +41,7 @@ export const Preferences = {
_settings: new Map(),
/**
- * @param {PreferenceConfigInfo} prefInfo
+ * @param {PreferenceConfig} prefInfo
*/
_add(prefInfo) {
if (this._all[prefInfo.id]) {
@@ -175,7 +58,7 @@ export const Preferences = {
},
/**
- * @param {PreferenceConfigInfo} prefInfo
+ * @param {PreferenceConfig} prefInfo
* @returns {Preference}
*/
add(prefInfo) {
@@ -184,7 +67,7 @@ export const Preferences = {
},
/**
- * @param {Array<PreferenceConfigInfo>} prefInfos
+ * @param {Array<PreferenceConfig>} prefInfos
*/
addAll(prefInfos) {
prefInfos.map(prefInfo => this._add(prefInfo));
@@ -199,7 +82,7 @@ export const Preferences = {
},
/**
- * @returns {Array<PreferenceConfigInfo>}
+ * @returns {Array<Preference>}
*/
getAll() {
return Object.values(this._all);
@@ -210,7 +93,7 @@ export const Preferences = {
* that includes all of the configuration for the control
* such as its Fluent strings, support page, subcategory etc.
*
- * @param {PreferencesSettingsConfig} settingConfig
+ * @param {SettingConfig} settingConfig
*/
addSetting(settingConfig) {
this._settings.set(
@@ -252,7 +135,12 @@ export const Preferences = {
this._instantApplyForceEnabled = true;
},
- observe(subject, topic, data) {
+ /**
+ * @param {nsISupports} _
+ * @param {string} __
+ * @param {string} data
+ */
+ observe(_, __, data) {
const pref = this._all[data];
if (pref) {
pref.value = pref.valueFromPreferences;
@@ -293,15 +181,21 @@ export const Preferences = {
});
},
- onUnload() {
+ /**
+ * @param {Event} _
+ */
+ onUnload(_) {
this._settings.forEach(setting => setting?.destroy?.());
- Services.prefs.removeObserver("", this);
+ Services.prefs.removeObserver("", /** @type {nsIObserver} */ (this));
},
QueryInterface: ChromeUtils.generateQI(["nsITimerCallback", "nsIObserver"]),
_deferredValueUpdateElements: new Set(),
+ /**
+ * @param {boolean} aFlushToDisk
+ */
writePreferences(aFlushToDisk) {
// Write all values to preferences.
if (this._deferredValueUpdateElements.size) {
@@ -319,6 +213,9 @@ export const Preferences = {
}
},
+ /**
+ * @param {HTMLElement} aStartElement
+ */
getPreferenceElement(aStartElement) {
let temp = aStartElement;
while (
@@ -326,11 +223,15 @@ export const Preferences = {
temp.nodeType == Node.ELEMENT_NODE &&
!temp.hasAttribute("preference")
) {
+ // @ts-expect-error
temp = temp.parentNode;
}
return temp && temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement;
},
+ /**
+ * @param {DeferredValueHTMLElement} aElement
+ */
_deferredValueUpdate(aElement) {
delete aElement._deferredValueUpdateTask;
const prefID = aElement.getAttribute("preference");
@@ -348,8 +249,13 @@ export const Preferences = {
}
},
+ /**
+ * @param {HTMLElement} aElement
+ */
userChangedValue(aElement) {
- const element = this.getPreferenceElement(aElement);
+ const element = /** @type {DeferredValueHTMLElement} */ (
+ this.getPreferenceElement(aElement)
+ );
if (element.hasAttribute("preference")) {
if (element.getAttribute("delayprefsave") != "true") {
const preference = Preferences.get(element.getAttribute("preference"));
@@ -371,27 +277,37 @@ export const Preferences = {
}
},
+ /**
+ * @typedef {{ sourceEvent: CommandEventWithSource } & CommandEvent} CommandEventWithSource
+ * @param {CommandEventWithSource} event
+ */
onCommand(event) {
// This "command" event handler tracks changes made to preferences by
// the user in this window.
if (event.sourceEvent) {
event = event.sourceEvent;
}
- this.userChangedValue(event.target);
+ this.userChangedValue(/** @type {HTMLElement} */ (event.target));
},
+ /** @param {Event} event */
onChange(event) {
// This "change" event handler tracks changes made to preferences by
// the user in this window.
- this.userChangedValue(event.target);
+ this.userChangedValue(/** @type {HTMLElement} */ (event.target));
},
+ /** @param {Event} event */
onInput(event) {
// This "input" event handler tracks changes made to preferences by
// the user in this window.
- this.userChangedValue(event.target);
+ this.userChangedValue(/** @type {HTMLElement} */ (event.target));
},
+ /**
+ * @param {string} aEventName
+ * @param {HTMLElement} aTarget
+ */
_fireEvent(aEventName, aTarget) {
try {
const event = new CustomEvent(aEventName, {
@@ -405,6 +321,9 @@ export const Preferences = {
return false;
},
+ /**
+ * @param {Event} event
+ */
onDialogAccept(event) {
let dialog = document.querySelector("dialog");
if (!this._fireEvent("beforeaccept", dialog)) {
@@ -415,6 +334,9 @@ export const Preferences = {
return true;
},
+ /**
+ * @param {Event} event
+ */
close(event) {
if (Preferences.instantApply) {
window.close();
@@ -423,13 +345,16 @@ export const Preferences = {
event.preventDefault();
},
+ /**
+ * @param {Event} event
+ */
handleEvent(event) {
switch (event.type) {
case "toggle":
case "change":
return this.onChange(event);
case "command":
- return this.onCommand(event);
+ return this.onCommand(/** @type {CommandEventWithSource} */ (event));
case "dialogaccept":
return this.onDialogAccept(event);
case "input":
@@ -484,10 +409,16 @@ export const Preferences = {
}
},
+ /**
+ * @param {Element} aElement
+ */
removeSyncFromPrefListener(aElement) {
this._syncFromPrefListeners.delete(aElement);
},
+ /**
+ * @param {Element} aElement
+ */
removeSyncToPrefListener(aElement) {
this._syncToPrefListeners.delete(aElement);
},
@@ -497,7 +428,7 @@ export const Preferences = {
Setting,
};
-Services.prefs.addObserver("", Preferences);
+Services.prefs.addObserver("", /** @type {nsIObserver} */ (Preferences));
window.addEventListener("toggle", Preferences);
window.addEventListener("change", Preferences);
window.addEventListener("command", Preferences);
diff --git a/toolkit/content/preferences/Setting.mjs b/toolkit/content/preferences/Setting.mjs
@@ -8,13 +8,140 @@ import {
} from "chrome://global/content/preferences/AsyncSetting.mjs";
import { Preferences } from "chrome://global/content/preferences/Preferences.mjs";
-/** @import { type Preference } from "chrome://global/content/preferences/Preference.mjs" */
-/** @import { PreferencesSettingsConfig } from "chrome://global/content/preferences/Preferences.mjs" */
+/**
+ * @import { type Preference } from "chrome://global/content/preferences/Preference.mjs"
+ * @import { SettingControlConfig } from "chrome://browser/content/preferences/widgets/setting-control.mjs"
+ * @import { ExtensionSettingsStore } from "resource://gre/modules/ExtensionSettingsStore.sys.mjs"
+ * @import { AddonManager } from "resource://gre/modules/AddonManager.sys.mjs"
+ * @import { Management } from "resource://gre/modules/Extension.sys.mjs"
+ */
+
+/**
+ * A map of Setting instances (values) along with their IDs
+ * (keys) so that the dependencies of a setting can
+ * be easily looked up by just their ID.
+ *
+ * @typedef {Record<string, Setting>} SettingDeps
+ */
+
+/**
+ * @typedef {string | boolean | number | nsIFile | void} SettingValue
+ */
+
+/**
+ * @callback SettingVisibleCallback
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {any} If truthy shows the setting in the UI, or hides it if not
+ */
+
+/**
+ * Gets the value of a {@link Setting}.
+ *
+ * @callback SettingGetCallback
+ * @param {any} val - The value that was retrieved from the preferences backend
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {any} - The value to set onto the setting
+ */
+
+/**
+ * Sets the value of a {@link Setting}.
+ *
+ * @callback SettingSetCallback
+ * @param {SettingValue} val - The value/pressed/checked from the input (control) associated with the setting
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {void}
+ */
+
+/**
+ * @callback SettingOnUserChangeCallback
+ * @param {SettingValue} val - The value/pressed/checked from the input of the control associated with the setting
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {void}
+ */
+
+/**
+ * @callback SettingDisabledCallback
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {any}
+ */
+
+/**
+ * @callback SettingGetControlConfigCallback
+ * @param {SettingControlConfig} config
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {SettingControlConfig}
+ */
+
+/**
+ * @callback SettingTeardownCallback
+ * @returns {void}
+ */
+
+/**
+ * @callback SettingEmitChange
+ */
+
+/**
+ * @callback SettingSetupCallback
+ * @param {SettingEmitChange} emitChange Notify listeners of a change to this setting.
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {SettingTeardownCallback | void}
+ */
+
+/**
+ * @callback SettingOnUserClickCallback
+ * @param {MouseEvent} event
+ * @param {SettingDeps} deps
+ * @param {Setting} setting
+ * @returns {void}
+ */
+
+/**
+ * @typedef {Object} SettingControllingExtensionInfo
+ * @property {string} storeId The ExtensionSettingsStore id that controls this setting.
+ * @property {string} l10nId A fluent id to show in a controlled by extension message.
+ * @property {string} [name] The controlling extension's name.
+ * @property {string} [id] The controlling extension's id.
+ */
+
+/**
+ * @typedef {object} SettingConfig
+ * @property {string} id - The ID for the Setting, this should match the layout id
+ * @property {string} [pref] - A {@link Services.prefs} id that will be used as the backend if it is provided
+ * @property {string[]} [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 {Pick<SettingControllingExtensionInfo, "storeId" | "l10nId">} [controllingExtensionInfo] Data related to the setting being controlled by an extension.
+ * @property {SettingVisibleCallback} [visible] - Function to determine if a setting is visible in the UI
+ * @property {SettingGetCallback} [get] - Function to get the value of the setting. Optional if {@link SettingConfig#pref} is set.
+ * @property {SettingSetCallback} [set] - Function to set the value of the setting. Optional if {@link SettingConfig#pref} is set.
+ * @property {SettingGetControlConfigCallback} [getControlConfig] - Function that allows the setting to modify its layout, this is intended to be used to provide the options, {@link SettingConfig#l10nId} or {@link SettingConfig#l10nArgs} data if necessary, but technically it can change anything (that doesn't mean it will have any effect though).
+ * @property {SettingOnUserChangeCallback} [onUserChange] - A function that will be called when the setting
+ * has been modified by the user, it is passed the value/pressed/checked from its input. NOTE: This should be used for
+ * additional work that needs to happen, such as recording telemetry.
+ * If you want to set the value of the Setting then use the {@link SettingConfig.set} function.
+ * @property {SettingSetupCallback} [setup] - A function to be called to register listeners for
+ * the setting. It should return a {@link SettingTeardownCallback} function to
+ * remove the listeners if necessary. This should emit change events when the setting has changed to
+ * ensure the UI stays in sync if possible.
+ * @property {SettingDisabledCallback} [disabled] - A function to determine if a setting should be disabled
+ * @property {SettingOnUserClickCallback} [onUserClick] - A function that will be called when a setting has been
+ * clicked, the element name must be included in the CLICK_HANDLERS array
+ * 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.
+ */
const { EventEmitter } = ChromeUtils.importESModule(
"resource://gre/modules/EventEmitter.sys.mjs"
);
+/** @type {{ ExtensionSettingsStore: typeof ExtensionSettingsStore, AddonManager: typeof AddonManager, Management: typeof Management }} */
+// @ts-expect-error bug 1996860
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ExtensionSettingsStore:
@@ -23,19 +150,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
Management: "resource://gre/modules/Extension.sys.mjs",
});
-/**
- * A map of Setting instances (values) along with their IDs
- * (keys) so that the dependencies of a setting can
- * be easily looked up by just their ID.
- *
- * @typedef {Record<string, Setting | undefined>} PreferenceSettingDepsMap
- */
-
-/**
- * @typedef {string | boolean | number} SettingValue
- */
-
class PreferenceNotAddedError extends Error {
+ /**
+ * @param {string} settingId
+ * @param {string} prefId
+ */
constructor(settingId, prefId) {
super(
`Setting "${settingId}" was unable to find Preference "${prefId}". Did you register it with Preferences.add/addAll?`
@@ -48,7 +167,7 @@ class PreferenceNotAddedError extends Error {
export class Setting extends EventEmitter {
/**
- * @type {Preference | undefined | null}
+ * @type {Preference}
*/
pref;
@@ -56,36 +175,45 @@ export class Setting extends EventEmitter {
* Keeps a cache of each dep's Setting so that
* it can be easily looked up by its ID.
*
- * @type {PreferenceSettingDepsMap | undefined}
+ * @type {SettingDeps}
*/
_deps;
/**
- * @type {PreferencesSettingsConfig}
+ * @type {SettingConfig | AsyncSettingHandler}
*/
config;
/**
- * @param {PreferencesSettingsConfig['id']} id
- * @param {PreferencesSettingsConfig} config
+ * @param {SettingConfig['id']} id
+ * @param {SettingConfig | typeof AsyncSetting} config
* @throws {Error} Will throw an error (PreferenceNotAddedError) if
* config.pref was not registered
*/
constructor(id, config) {
super();
+ /** @type {SettingConfig | AsyncSettingHandler} */
+ let configObj;
+
if (Object.getPrototypeOf(config) == AsyncSetting) {
- config = new AsyncSettingHandler(new config());
+ configObj = new AsyncSettingHandler(
+ id,
+ /** @type {typeof AsyncSetting} */ (config)
+ );
+ } else {
+ configObj = config;
}
this.id = id;
- this.config = config;
- this.pref = config.pref && Preferences.get(config.pref);
- if (config.pref && !this.pref) {
- throw new PreferenceNotAddedError(id, config.pref);
+ this.config = configObj;
+ this.pref = configObj.pref && Preferences.get(configObj.pref);
+ if (configObj.pref && !this.pref) {
+ throw new PreferenceNotAddedError(id, configObj.pref);
}
this._emitting = false;
+ /** @type {SettingControllingExtensionInfo} */
this.controllingExtensionInfo = {
...this.config.controllingExtensionInfo,
};
@@ -113,14 +241,14 @@ export class Setting extends EventEmitter {
/**
* A map of each dep and it's associated {@link Setting} instance.
*
- * @type {PreferenceSettingDepsMap}
+ * @type {SettingDeps}
*/
get deps() {
if (this._deps) {
return this._deps;
}
/**
- * @type {PreferenceSettingDepsMap}
+ * @type {SettingDeps}
*/
const deps = {};
@@ -157,7 +285,7 @@ export class Setting extends EventEmitter {
*/
set value(val) {
let newVal = this.config.set ? this.config.set(val, this.deps, this) : val;
- if (this.pref) {
+ if (this.pref && newVal instanceof Object && !("then" in newVal)) {
this.pref.value = newVal;
}
}
@@ -178,8 +306,8 @@ export class Setting extends EventEmitter {
}
/**
- * @param {PreferencesSettingsConfig} config
- * @returns {PreferencesSettingsConfig | undefined}
+ * @param {SettingControlConfig} config
+ * @returns {SettingControlConfig}
*/
getControlConfig(config) {
if (this.config.getControlConfig) {
@@ -188,6 +316,9 @@ export class Setting extends EventEmitter {
return config;
}
+ /**
+ * @param {MouseEvent} event
+ */
userClick(event) {
if (this.config.onUserClick) {
this.config.onUserClick(event, this.deps, this);
@@ -210,7 +341,7 @@ export class Setting extends EventEmitter {
this.controllingExtensionInfo.id
) {
await lazy.ExtensionSettingsStore.initialize();
- let { id } = await lazy.ExtensionSettingsStore.getSetting(
+ let { id } = lazy.ExtensionSettingsStore.getSetting(
"prefs",
this.controllingExtensionInfo.storeId
);
@@ -221,6 +352,10 @@ export class Setting extends EventEmitter {
}
}
+ /**
+ * @param {any} _
+ * @param {{ key: string, type: string }} setting ExtensionSettingsStore setting
+ */
_observeExtensionSettingChanged = (_, setting) => {
if (
setting.key == this.config.controllingExtensionInfo.storeId &&
diff --git a/toolkit/content/widgets/lit-utils.mjs b/toolkit/content/widgets/lit-utils.mjs
@@ -263,7 +263,10 @@ export class MozBaseInputElement extends MozLitElement {
ariaLabel: { type: String, mapped: true },
ariaDescription: { type: String, mapped: true },
};
+ /** @type {"inline" | "block"} */
static inputLayout = "inline";
+ /** @type {keyof MozBaseInputElement} */
+ static activatedProperty = null;
constructor() {
super();
@@ -305,7 +308,9 @@ export class MozBaseInputElement extends MozLitElement {
if (changedProperties.has("value")) {
this.setFormValue(this.value);
}
- let activatedProperty = this.constructor.activatedProperty;
+ let activatedProperty = /** @type {typeof MozBaseInputElement} */ (
+ this.constructor
+ ).activatedProperty;
if (
(activatedProperty && changedProperties.has(activatedProperty)) ||
changedProperties.has("disabled") ||
diff --git a/tools/@types/generated/tspaths.json b/tools/@types/generated/tspaths.json
@@ -125,6 +125,9 @@
"chrome://browser/content/preferences/widgets/setting-group.mjs": [
"browser/components/preferences/widgets/setting-group/setting-group.mjs"
],
+ "chrome://browser/content/preferences/widgets/setting-pane.mjs": [
+ "browser/components/preferences/widgets/setting-pane/setting-pane.mjs"
+ ],
"chrome://browser/content/preferences/widgets/sync-device-name.mjs": [
"browser/components/preferences/widgets/sync-device-name/sync-device-name.mjs"
],
@@ -248,6 +251,12 @@
"chrome://global/content/elements/moz-fieldset.mjs": [
"toolkit/content/widgets/moz-fieldset/moz-fieldset.mjs"
],
+ "chrome://global/content/elements/moz-input-folder.mjs": [
+ "toolkit/content/widgets/moz-input-folder/moz-input-folder.mjs"
+ ],
+ "chrome://global/content/elements/moz-input-search.mjs": [
+ "toolkit/content/widgets/moz-input-search/moz-input-search.mjs"
+ ],
"chrome://global/content/elements/moz-input-text.mjs": [
"toolkit/content/widgets/moz-input-text/moz-input-text.mjs"
],
@@ -257,6 +266,9 @@
"chrome://global/content/elements/moz-message-bar.mjs": [
"toolkit/content/widgets/moz-message-bar/moz-message-bar.mjs"
],
+ "chrome://global/content/elements/moz-radio-group.mjs": [
+ "toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs"
+ ],
"chrome://global/content/elements/moz-support-link.mjs": [
"toolkit/content/widgets/moz-support-link/moz-support-link.mjs"
],