Bug 1520350 - Lazily load about:preferences markups from hidden panes r=jaws
authorTimothy Guan-tin Chien <timdream@gmail.com>
Tue, 29 Jan 2019 00:27:29 +0000
changeset 455791 41e11bb52568
parent 455790 43a795c02325
child 455792 adcc2b05c708
push id76979
push usertchien@mozilla.com
push dateTue, 29 Jan 2019 03:40:18 +0000
treeherderautoland@41e11bb52568 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1520350
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1520350 - Lazily load about:preferences markups from hidden panes r=jaws Because custom elements will be constructed when DOM is constructed, construct the DOM in the hidden panels will be expensive as we move more and more widgets to custom elements from XBL. This patch attempts to counter that by moving all the pane markups into comment nodes, and use MozXULElement.parseXULToFragment() to insert it when it is being asked. They will be loaded lazily from an requestIdleCallback() in findInPage.js. Differential Revision: https://phabricator.services.mozilla.com/D16787
browser/components/enterprisepolicies/tests/browser/browser_policy_disable_popup_blocker.js
browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
browser/components/preferences/in-content/home.xul
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/privacy.js
browser/components/preferences/in-content/privacy.xul
browser/components/preferences/in-content/search.xul
browser/components/preferences/in-content/sync.js
browser/components/preferences/in-content/sync.xul
browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
browser/extensions/formautofill/FormAutofillParent.jsm
toolkit/content/preferencesBindings.js
toolkit/content/widgets/radio.xml
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_disable_popup_blocker.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_disable_popup_blocker.js
@@ -19,17 +19,17 @@ registerCleanupFunction(async function c
   if (ORIGINAL_PREF_VALUE === undefined) {
     Services.prefs.clearUserPref("dom.disable_open_during_load");
   } else {
     Services.prefs.setBoolPref("dom.disable_open_during_load", ORIGINAL_PREF_VALUE);
   }
 });
 
 async function test_popup_blocker_disabled({disabled, locked}) {
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences#privacy");
   // eslint-disable-next-line no-shadow
   await ContentTask.spawn(tab.linkedBrowser, {disabled, locked}, async function({disabled, locked}) {
     let checkbox = content.document.getElementById("popupPolicy");
     is(checkbox.checked, !disabled,
        "Checkbox checked state should match policy's Block status");
     is(checkbox.disabled, locked,
        "Checkbox disabled state should match policy's Locked status");
   });
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_search_engine.js
@@ -130,17 +130,17 @@ add_task(async function setup_prevent_in
       },
     },
   });
 });
 
 add_task(async function test_prevent_install_ui() {
   // Check that about:preferences does not prompt user to install search engines
   // if that feature is disabled
-  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences#search");
   await ContentTask.spawn(tab.linkedBrowser, null, async function() {
     let linkContainer = content.document.getElementById("addEnginesBox");
     if (!linkContainer.hidden) {
       await new Promise(resolve => {
         let mut = new linkContainer.ownerGlobal.MutationObserver(mutations => {
           mut.disconnect();
           resolve();
         });
--- a/browser/components/preferences/in-content/home.xul
+++ b/browser/components/preferences/in-content/home.xul
@@ -1,17 +1,17 @@
 # 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/.
 
 <!-- Home panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/home.js"/>
-
+<box id="template-paneHome" hidden="true"><![CDATA[
 <hbox id="firefoxHomeCategory"
       class="subcategory"
       hidden="true"
       data-category="paneHome">
   <html:h1 style="-moz-box-flex: 1;" data-l10n-id="pane-home-title"/>
   <button id="restoreDefaultHomePageBtn"
           class="homepage-button check-home-page-controlled"
           data-preference-related="browser.startup.homepage"
@@ -93,8 +93,9 @@
   <hbox id="browserNewTabExtensionContent"
         align="center" hidden="true" class="extension-controlled">
     <description control="disableNewTabExtension" flex="1" />
     <button id="disableNewTabExtension"
             class="extension-controlled-button accessory-button"
             data-l10n-id="disable-extension" />
   </hbox>
 </groupbox>
+]]></box>
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -394,16 +394,30 @@ var gMainPane = {
       row.removeAttribute("hidden");
       // Showing attribution only for Bing Translator.
       ChromeUtils.import("resource:///modules/translation/Translation.jsm");
       if (Translation.translationEngine == "Bing") {
         document.getElementById("bingAttribution").removeAttribute("hidden");
       }
     }
 
+    let drmInfoURL =
+      Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
+    document.getElementById("playDRMContentLink").setAttribute("href", drmInfoURL);
+    let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
+    // Force-disable/hide on WinXP:
+    if (navigator.platform.toLowerCase().startsWith("win")) {
+      emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
+    }
+    if (!emeUIEnabled) {
+      // Don't want to rely on .hidden for the toplevel groupbox because
+      // of the pane hiding/showing code potentially interfering:
+      document.getElementById("drmGroup").setAttribute("style", "display: none !important");
+    }
+
     if (AppConstants.MOZ_DEV_EDITION) {
       let uAppData = OS.Constants.Path.userApplicationDataDir;
       let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
 
       setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
       let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
       setEventListener("getStarted", "click", gMainPane.onGetStarted);
 
@@ -472,17 +486,23 @@ var gMainPane = {
       if (distroAbout) {
         let distroField = document.getElementById("distribution");
         distroField.value = distroAbout;
         distroField.hidden = false;
       }
     }
 
     if (AppConstants.MOZ_UPDATER) {
-      gAppUpdater = new appUpdater();
+      // XXX Workaround bug 1523453 -- changing selectIndex of a <deck> before
+      // frame construction could confuse nsDeckFrame::RemoveFrame().
+      window.requestAnimationFrame(() => {
+        window.requestAnimationFrame(() => {
+          gAppUpdater = new appUpdater();
+        });
+      });
       setEventListener("showUpdateHistory", "command",
         gMainPane.showUpdates);
 
       if (Services.policies && !Services.policies.isAllowed("appUpdate")) {
         document.getElementById("updateAllowDescription").hidden = true;
         document.getElementById("updateRadioGroup").hidden = true;
         if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
           document.getElementById("useService").hidden = true;
@@ -567,24 +587,27 @@ var gMainPane = {
       browserBundle.getString("userContextPersonal.label"),
       browserBundle.getString("userContextWork.label"),
       browserBundle.getString("userContextBanking.label"),
       browserBundle.getString("userContextShopping.label"),
     ]);
 
     // Notify observers that the UI is now ready
     Services.obs.notifyObservers(window, "main-pane-loaded");
+
+    this.setInitialized();
   },
 
   preInit() {
     promiseLoadHandlersList = new Promise((resolve, reject) => {
       // Load the data and build the list of handlers for applications pane.
       // By doing this after pageshow, we ensure it doesn't delay painting
       // of the preferences page.
       window.addEventListener("pageshow", async () => {
+        await this.initialized;
         try {
           this._initListEventHandlers();
           this._loadData();
           await this._rebuildVisibleTypes();
           this._sortVisibleTypes();
           this._rebuildView();
           resolve();
         } catch (ex) {
@@ -2479,16 +2502,20 @@ var gMainPane = {
       case 1:
         return this._getDownloadsFolder("Downloads");
     }
     var currentDirPref = Preferences.get("browser.download.dir");
     return currentDirPref.value;
   },
 };
 
+gMainPane.initialized = new Promise(res => {
+  gMainPane.setInitialized = res;
+});
+
 // Utilities
 
 function getFileDisplayName(file) {
   if (AppConstants.platform == "win") {
     if (file instanceof Ci.nsILocalFileWin) {
       try {
         return file.getVersionInfoField("FileDescription");
       } catch (e) { }
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -11,16 +11,17 @@
   <script type="application/javascript" src="chrome://browser/content/aboutDialog-appUpdater.js"/>
 #endif
 
 <script type="application/javascript"
         src="chrome://mozapps/content/preferences/fontbuilder.js"/>
 
 <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences.properties"/>
 
+<box id="template-paneGeneral" hidden="true"><![CDATA[
 <hbox id="generalCategory"
       class="subcategory"
       hidden="true"
       data-category="paneGeneral">
   <html:h1 data-l10n-id="pane-general-title"/>
 </hbox>
 
 <!-- Startup -->
@@ -707,8 +708,9 @@
                 connection-proxy-socks-remote-dns.label,
                 connection-dns-over-https,
                 connection-dns-over-https-url-custom,
                 connection-dns-over-https-url-default,
             " />
     </hbox>
   </hbox>
 </groupbox>
+]]></box>
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -8,26 +8,28 @@
 /* import-globals-from home.js */
 /* import-globals-from search.js */
 /* import-globals-from containers.js */
 /* import-globals-from privacy.js */
 /* import-globals-from sync.js */
 /* import-globals-from findInPage.js */
 /* import-globals-from ../../../base/content/utilityOverlay.js */
 /* import-globals-from ../../../../toolkit/content/preferencesBindings.js */
+/* global MozXULElement */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
 ChromeUtils.defineModuleGetter(this, "formAutofillParent",
                                "resource://formautofill/FormAutofillParent.jsm");
 
 var gLastHash = "";
+const gXULDOMParser = new DOMParser();
 
 var gCategoryInits = new Map();
 function init_category_if_required(category) {
   let categoryInfo = gCategoryInits.get(category);
   if (!categoryInfo) {
     throw "Unknown in-content prefs category! Can't init " + category;
   }
   if (categoryInfo.inited) {
@@ -35,16 +37,55 @@ function init_category_if_required(categ
   }
   categoryInfo.init();
 }
 
 function register_module(categoryName, categoryObject) {
   gCategoryInits.set(categoryName, {
     inited: false,
     init() {
+      let template = document.getElementById("template-" + categoryName);
+      if (template) {
+        // Replace the template element with the nodes from the parsed comment
+        // string.
+        let frag = MozXULElement.parseXULToFragment(template.firstChild.data);
+
+        // Gather the to-be-translated elements so that we could pass them to
+        // l10n.translateElements() and get a translated promise.
+        // Here we loop through the first level elements (<hbox>/<groupbox>/<deck>/etc)
+        // because we know that they are not implemented by XBL bindings,
+        // so it's ok to get a reference of them before inserting the node
+        // to the DOM.
+        //
+        // If we don't have to worry about XBL, this can simply be
+        // let l10nUpdatedElements = Array.from(frag.querySelectorAll("[data-l10n-id]"))
+        //
+        // If we can get a translated promise after insertion, this can all be
+        // removed (see bug 1520659.)
+        let firstLevelElements = Array.from(frag.children);
+
+        // Actually insert them into the DOM.
+        template.replaceWith(frag);
+
+        let l10nUpdatedElements = [];
+        // Collect the elements from the newly inserted first level elements.
+        for (let el of firstLevelElements) {
+          l10nUpdatedElements = l10nUpdatedElements.concat(
+            Array.from(el.querySelectorAll("[data-l10n-id]")));
+        }
+
+        // Set a promise on the categoryInfo object that the highlight code can await on.
+        this.translated = document.l10n.translateElements(l10nUpdatedElements)
+          .then(() => this.translated = undefined);
+
+        // Asks Preferences to update the attribute value of the entire
+        // document again (this can be simplified if we could seperate the
+        // preferences of each pane.)
+        Preferences.updateAllElements();
+      }
       categoryObject.init();
       this.inited = true;
     },
   });
 }
 
 document.addEventListener("DOMContentLoaded", init_all, {once: true});
 
@@ -54,17 +95,16 @@ function init_all() {
   gSubDialog.init();
   register_module("paneGeneral", gMainPane);
   register_module("paneHome", gHomePane);
   register_module("paneSearch", gSearchPane);
   register_module("panePrivacy", gPrivacyPane);
   register_module("paneContainers", gContainersPane);
   if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
     document.getElementById("category-sync").hidden = false;
-    document.getElementById("weavePrefsDeck").removeAttribute("data-hidden-from-search");
     register_module("paneSync", gSyncPane);
   }
   register_module("paneSearchResults", gSearchResultsPane);
   gSearchResultsPane.init();
   gMainPane.preInit();
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
@@ -182,17 +222,17 @@ function gotoPref(aCategory) {
     categories.clearSelection();
   }
   window.history.replaceState(category, document.title);
   search(category, "data-category");
 
   let mainContent = document.querySelector(".main-content");
   mainContent.scrollTop = 0;
 
-  spotlight(subcategory);
+  spotlight(subcategory, category);
 }
 
 function search(aQuery, aAttribute) {
   let mainPrefPane = document.getElementById("mainPrefPane");
   let elements = mainPrefPane.children;
   for (let element of elements) {
     // If the "data-hidden-from-search" is "true", the
     // element will not get considered during search.
@@ -216,102 +256,51 @@ function search(aQuery, aAttribute) {
     let attributeValue = element.getAttribute(aAttribute);
     if (attributeValue == aQuery)
       element.removeAttribute("disabled");
     else
       element.setAttribute("disabled", true);
   }
 }
 
-async function spotlight(subcategory) {
+async function spotlight(subcategory, category) {
   let highlightedElements = document.querySelectorAll(".spotlight");
   if (highlightedElements.length) {
     for (let element of highlightedElements) {
       element.classList.remove("spotlight");
     }
   }
   if (subcategory) {
-    if (!gSearchResultsPane.categoriesInitialized) {
-      await waitForSystemAddonInjectionsFinished([{
-        isGoingToInject: formAutofillParent.initialized,
-        elementId: "formAutofillGroup",
-      }]);
-    }
-    scrollAndHighlight(subcategory);
-  }
-
-  /**
-   * Wait for system addons finished their dom injections.
-   * @param {Array} addons - The system addon information array.
-   * For example, the element is looked like
-   * { isGoingToInject: true, elementId: "formAutofillGroup" }.
-   * The `isGoingToInject` means the system addon will be visible or not,
-   * and the `elementId` means the id of the element will be injected into the dom
-   * if the `isGoingToInject` is true.
-   * @returns {Promise} Will resolve once all injections are finished.
-   */
-  function waitForSystemAddonInjectionsFinished(addons) {
-    return new Promise(resolve => {
-      let elementIdSet = new Set();
-      for (let addon of addons) {
-        if (addon.isGoingToInject) {
-          elementIdSet.add(addon.elementId);
-        }
-      }
-      if (elementIdSet.size) {
-        let observer = new MutationObserver(mutations => {
-          for (let mutation of mutations) {
-            for (let node of mutation.addedNodes) {
-              elementIdSet.delete(node.id);
-              if (elementIdSet.size === 0) {
-                observer.disconnect();
-                resolve();
-              }
-            }
-          }
-        });
-        let mainContent = document.querySelector(".main-content");
-        observer.observe(mainContent, {childList: true, subtree: true});
-        // Disconnect the mutation observer once there is any user input.
-        mainContent.addEventListener("scroll", disconnectMutationObserver);
-        window.addEventListener("mousedown", disconnectMutationObserver);
-        window.addEventListener("keydown", disconnectMutationObserver);
-        function disconnectMutationObserver() {
-          mainContent.removeEventListener("scroll", disconnectMutationObserver);
-          window.removeEventListener("mousedown", disconnectMutationObserver);
-          window.removeEventListener("keydown", disconnectMutationObserver);
-          observer.disconnect();
-        }
-      } else {
-        resolve();
-      }
-    });
+    scrollAndHighlight(subcategory, category);
   }
 }
 
-function scrollAndHighlight(subcategory) {
+async function scrollAndHighlight(subcategory, category) {
   let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
-  if (element) {
-    let header = getClosestDisplayedHeader(element);
-    scrollContentTo(header);
-    element.classList.add("spotlight");
+  if (!element) {
+    return;
   }
+  let header = getClosestDisplayedHeader(element);
+  await gCategoryInits.get(category).translated;
+
+  scrollContentTo(header);
+  element.classList.add("spotlight");
 }
 
 /**
  * If there is no visible second level header it will return first level header,
  * otherwise return second level header.
  * @returns {Element} - The closest displayed header.
  */
 function getClosestDisplayedHeader(element) {
   let header = element.closest("groupbox");
   let searchHeader = header.querySelector(".search-header");
   if (searchHeader && searchHeader.hidden &&
-      header.previousSibling.classList.contains("subcategory")) {
-    header = header.previousSibling;
+      header.previousElementSibling.classList.contains("subcategory")) {
+    header = header.previousElementSibling;
   }
   return header;
 }
 
 function scrollContentTo(element) {
   const STICKY_CONTAINER_HEIGHT = document.querySelector(".sticky-container").clientHeight;
   let mainContent = document.querySelector(".main-content");
   let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
--- a/browser/components/preferences/in-content/privacy.js
+++ b/browser/components/preferences/in-content/privacy.js
@@ -382,29 +382,16 @@ var gPrivacyPane = {
       gPrivacyPane.showSiteDataSettings);
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "storage-permissions";
     document.getElementById("siteDataLearnMoreLink").setAttribute("href", url);
 
     let notificationInfoURL =
       Services.urlFormatter.formatURLPref("app.support.baseURL") + "push";
     document.getElementById("notificationPermissionsLearnMore").setAttribute("href",
       notificationInfoURL);
-    let drmInfoURL =
-      Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
-    document.getElementById("playDRMContentLink").setAttribute("href", drmInfoURL);
-    let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled");
-    // Force-disable/hide on WinXP:
-    if (navigator.platform.toLowerCase().startsWith("win")) {
-      emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6;
-    }
-    if (!emeUIEnabled) {
-      // Don't want to rely on .hidden for the toplevel groupbox because
-      // of the pane hiding/showing code potentially interfering:
-      document.getElementById("drmGroup").setAttribute("style", "display: none !important");
-    }
 
     if (AppConstants.MOZ_DATA_REPORTING) {
       this.initDataCollection();
       if (AppConstants.MOZ_CRASHREPORTER) {
         this.initSubmitCrashes();
       }
       this.initSubmitHealthReport();
       setEventListener("submitHealthReportBox", "command",
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -3,17 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 <!-- Privacy panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/privacy.js"/>
 <stringbundle id="bundlePreferences" src="chrome://browser/locale/preferences/preferences.properties"/>
 <stringbundle id="signonBundle" src="chrome://passwordmgr/locale/passwordmgr.properties"/>
-
+<box id="template-panePrivacy" hidden="true"><![CDATA[
 <hbox id="browserPrivacyCategory"
       class="subcategory"
       hidden="true"
       data-category="panePrivacy">
   <html:h1 data-l10n-id="privacy-header"/>
 </hbox>
 
 <!-- Tracking / Content Blocking -->
@@ -317,17 +317,17 @@
     </hbox>
   </hbox>
 </groupbox>
 
 <!-- The form autofill section is inserted in to this box
      after the form autofill extension has initialized. -->
 <groupbox id="formAutofillGroupBox"
           data-category="panePrivacy"
-          data-subcategory="form-autofill"></groupbox>
+          data-subcategory="form-autofill" hidden="true"></groupbox>
 
 <!-- History -->
 <groupbox id="historyGroup" data-category="panePrivacy" hidden="true">
   <label><html:h2 data-l10n-id="history-header"/></label>
   <hbox align="center">
     <label id="historyModeLabel"
            control="historyMode"
            data-l10n-id="history-remember-label"/>
@@ -790,8 +790,9 @@
                   devmgr-button-changepw.label,
                   devmgr-button-load.label,
                   devmgr-button-unload.label
                 "/>
       </hbox>
     </vbox>
   </hbox>
 </groupbox>
+]]></box>
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -1,30 +1,30 @@
     <script type="application/javascript"
             src="chrome://browser/content/preferences/in-content/search.js"/>
-
+    <box id="template-paneSearch" hidden="true"><![CDATA[
     <hbox id="searchCategory"
           class="subcategory"
           hidden="true"
           data-category="paneSearch">
       <html:h1 data-l10n-id="pane-search-title"/>
     </hbox>
 
-    <groupbox id="searchbarGroup" data-category="paneSearch">
+    <groupbox id="searchbarGroup" data-category="paneSearch" hidden="true">
       <label control="searchBarVisibleGroup"><html:h2 data-l10n-id="search-bar-header"/></label>
       <radiogroup id="searchBarVisibleGroup" preference="browser.search.widget.inNavBar">
         <radio id="searchBarHiddenRadio" value="false" data-l10n-id="search-bar-hidden"/>
         <image class="searchBarImage searchBarHiddenImage" role="presentation"/>
         <radio id="searchBarShownRadio" value="true" data-l10n-id="search-bar-shown"/>
         <image class="searchBarImage searchBarShownImage" role="presentation"/>
       </radiogroup>
     </groupbox>
 
     <!-- Default Search Engine -->
-    <groupbox id="defaultEngineGroup" data-category="paneSearch">
+    <groupbox id="defaultEngineGroup" data-category="paneSearch" hidden="true">
       <label><html:h2 data-l10n-id="search-engine-default-header" /></label>
       <description data-l10n-id="search-engine-default-desc" />
 
       <hbox id="browserDefaultSearchExtensionContent"
             align="center" hidden="true" class="extension-controlled">
         <description control="disableDefaultSearchExtension" flex="1"/>
       </hbox>
 
@@ -46,17 +46,17 @@
                   data-l10n-id="search-show-suggestions-above-history-option"/>
         <hbox id="urlBarSuggestionPermanentPBLabel"
               align="center" class="indent">
           <label flex="1" data-l10n-id="search-suggestions-cant-show" />
         </hbox>
       </vbox>
     </groupbox>
 
-    <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch">
+    <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch" hidden="true">
       <label><html:h2 data-l10n-id="search-one-click-header" /></label>
       <description data-l10n-id="search-one-click-desc" />
 
       <tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true"
             seltype="single" allowunderflowscroll="true">
         <treechildren id="engineChildren" flex="1"/>
         <treecols>
           <treecol id="engineShown" type="checkbox" editable="true" sortable="false"/>
@@ -76,8 +76,9 @@
                 data-l10n-id="search-remove-engine"
                 disabled="true"
                 />
       </hbox>
       <hbox id="addEnginesBox" pack="start">
         <label id="addEngines" class="text-link" data-l10n-id="search-find-more-link"></label>
       </hbox>
     </groupbox>
+    ]]></box>
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -47,16 +47,18 @@ var gSyncPane = {
   set page(val) {
     document.getElementById("weavePrefsDeck").selectedIndex = val;
   },
 
   init() {
     this._setupEventListeners();
     this._adjustForPrefs();
 
+    document.getElementById("weavePrefsDeck").removeAttribute("data-hidden-from-search");
+
     // If the Service hasn't finished initializing, wait for it.
     let xps = Cc["@mozilla.org/weave/service;1"]
       .getService(Ci.nsISupports)
       .wrappedJSObject;
 
     if (xps.ready) {
       this._init();
       return;
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -1,17 +1,17 @@
 # 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/.
 
 <!-- Sync panel -->
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/sync.js"/>
-
+<box id="template-paneSync" hidden="true"><![CDATA[
 <hbox id="firefoxAccountCategory"
       class="subcategory"
       hidden="true"
       data-category="paneSync">
   <html:h1 data-l10n-id="pane-sync-title"/>
 </hbox>
 
 <deck id="weavePrefsDeck" data-category="paneSync" hidden="true"
@@ -191,8 +191,9 @@
              class="text-link fxaMobilePromo" data-l10n-id="sync-mobilepromo-multi"/>
     </vbox>
     <vbox id="tosPP-small" align="start">
       <label id="tosPP-small-ToS" class="text-link" data-l10n-id="sync-tos-link"/>
       <label id="tosPP-small-PP" class="text-link" data-l10n-id="sync-fxa-privacy-notice"/>
     </vbox>
   </vbox>
 </deck>
+]]></box>
--- a/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
+++ b/browser/components/preferences/in-content/tests/browser_search_within_preferences_2.js
@@ -11,17 +11,17 @@ add_task(async function() {
 });
 
 /**
  * Test that we only search the selected child of a XUL deck.
  * When we search "Remove Account",
  * it should not show the "Remove Account" button if the Firefox account is not logged in yet.
  */
 add_task(async function() {
-  await openPreferencesViaOpenPreferencesAPI("paneGeneral", {leaveOpen: true});
+  await openPreferencesViaOpenPreferencesAPI("paneSync", {leaveOpen: true});
 
   // Ensure the "Sign Up" button in the hidden child of the <xul:deck>
   // is selected and displayed on the screen.
   let weavePrefsDeck = gBrowser.contentDocument.getElementById("weavePrefsDeck");
   is(weavePrefsDeck.selectedIndex, 0, "Should select the #noFxaAccount child node");
   let noFxaSignUp = weavePrefsDeck.children[0].querySelector("#noFxaSignUp");
   is(noFxaSignUp.textContent, "Don\u2019t have an account? Get started", "The Sign Up button should exist");
 
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -101,17 +101,17 @@ FormAutofillParent.prototype = {
    * Initializes FormAutofillStorage and registers the message handler.
    */
   async init() {
     if (this._initialized) {
       return;
     }
     this._initialized = true;
 
-    Services.obs.addObserver(this, "sync-pane-loaded");
+    Services.obs.addObserver(this, "privacy-pane-loaded");
     Services.ppmm.addMessageListener("FormAutofill:InitStorage", this);
     Services.ppmm.addMessageListener("FormAutofill:GetRecords", this);
     Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this);
     Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this);
     Services.ppmm.addMessageListener("FormAutofill:OpenPreferences", this);
     Services.mm.addMessageListener("FormAutofill:OnFormSubmit", this);
 
     // Observing the pref and storage changes
@@ -125,17 +125,17 @@ FormAutofillParent.prototype = {
       Services.ppmm.addMessageListener("FormAutofill:GetDecryptedString", this);
       Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
     }
   },
 
   observe(subject, topic, data) {
     log.debug("observe:", topic, "with data:", data);
     switch (topic) {
-      case "sync-pane-loaded": {
+      case "privacy-pane-loaded": {
         let formAutofillPreferences = new FormAutofillPreferences();
         let document = subject.document;
         let prefFragment = formAutofillPreferences.init(document);
         let formAutofillGroupBox = document.getElementById("formAutofillGroupBox");
         formAutofillGroupBox.appendChild(prefFragment);
         break;
       }
 
@@ -273,17 +273,17 @@ FormAutofillParent.prototype = {
    */
   _uninit() {
     this.formAutofillStorage._saveImmediately();
 
     Services.ppmm.removeMessageListener("FormAutofill:InitStorage", this);
     Services.ppmm.removeMessageListener("FormAutofill:GetRecords", this);
     Services.ppmm.removeMessageListener("FormAutofill:SaveAddress", this);
     Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this);
-    Services.obs.removeObserver(this, "sync-pane-loaded");
+    Services.obs.removeObserver(this, "privacy-pane-loaded");
     Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
 
     if (FormAutofill.isAutofillCreditCardsAvailable) {
       Services.ppmm.removeMessageListener("FormAutofill:SaveCreditCard", this);
       Services.ppmm.removeMessageListener("FormAutofill:RemoveCreditCards", this);
       Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
       Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
     }
--- a/toolkit/content/preferencesBindings.js
+++ b/toolkit/content/preferencesBindings.js
@@ -110,16 +110,34 @@ const Preferences = window.Preferences =
         const id = element.getAttribute("preference");
         const pref = this.get(id);
         if (!pref) {
           console.error(`Missing preference for ID ${id}`);
         }
       }
     },
 
+    updateQueued: false,
+
+    updateAllElements() {
+      if (this.updateQueued) {
+        return;
+      }
+
+      this.updateQueued = true;
+
+      Promise.resolve().then(() => {
+        const preferences = Preferences.getAll();
+        for (const preference of preferences) {
+          preference.updateElements();
+        }
+        this.updateQueued = false;
+      });
+    },
+
     onUnload() {
       Services.prefs.removeObserver("", this);
     },
 
     QueryInterface: ChromeUtils.generateQI([
       Ci.nsITimerCallback,
       Ci.nsIObserver,
     ]),
--- a/toolkit/content/widgets/radio.xml
+++ b/toolkit/content/widgets/radio.xml
@@ -19,16 +19,17 @@
       </xul:hbox>
     </content>
 
     <implementation implements="nsIDOMXULSelectControlItemElement">
       <constructor>
         <![CDATA[
           // Just clear out the parent's cached list of radio children
           var control = this.control;
+          window.customElements.upgrade(control);
           if (control)
             control.radioChildConstructed(this);
         ]]>
       </constructor>
       <destructor>
         <![CDATA[
           if (!this.control)
             return;