Bug 1407568 - Add a spotlight indicator to a specific section and UI component.
MozReview-Commit-ID: 4AgAFq2r418
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -22,16 +22,18 @@ var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSettingsStore",
"resource://gre/modules/ExtensionSettingsStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "formAutofillParent",
+ "resource://formautofill/FormAutofillParent.jsm");
var gLastHash = "";
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;
@@ -169,46 +171,41 @@ function gotoPref(aCategory) {
// the categories 'select' event will re-enter the gotoPref codepath.
gLastHash = category;
if (item) {
categories.selectedItem = item;
} else {
categories.clearSelection();
}
window.history.replaceState(category, document.title);
- search(category, "data-category", subcategory, "data-subcategory");
+ search(category, "data-category");
let mainContent = document.querySelector(".main-content");
mainContent.scrollTop = 0;
+ spotlight(subcategory);
+
Services.telemetry
.getHistogramById("FX_PREFERENCES_CATEGORY_OPENED_V2")
.add(telemetryBucketForCategory(friendlyName));
}
-function search(aQuery, aAttribute, aSubquery, aSubAttribute) {
+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. This
// should only be used when an element is still under
// development and should not be shown for any reason.
if (element.getAttribute("data-hidden-from-search") != "true" ||
element.getAttribute("data-subpanel") == "true") {
let attributeValue = element.getAttribute(aAttribute);
if (attributeValue == aQuery) {
- if (!element.classList.contains("header") &&
- element.localName !== "preferences" &&
- aSubquery && aSubAttribute) {
- let subAttributeValue = element.getAttribute(aSubAttribute);
- element.hidden = subAttributeValue != aSubquery;
- } else {
- element.hidden = false;
- }
+ element.hidden = false;
} else {
element.hidden = true;
}
}
element.classList.remove("visually-hidden");
}
let keysets = mainPrefPane.getElementsByTagName("keyset");
@@ -216,16 +213,116 @@ function search(aQuery, aAttribute, aSub
let attributeValue = element.getAttribute(aAttribute);
if (attributeValue == aQuery)
element.removeAttribute("disabled");
else
element.setAttribute("disabled", true);
}
}
+async function spotlight(subcategory) {
+ 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();
+ }
+ });
+ }
+}
+
+function scrollAndHighlight(subcategory) {
+ let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
+ if (element) {
+ let header = getClosestDisplayedHeader(element);
+ 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("caption.search-header");
+ if (searchHeader && searchHeader.hidden &&
+ header.previousSibling.classList.contains("subcategory")) {
+ header = header.previousSibling;
+ }
+ return header;
+}
+
+function scrollContentTo(element) {
+ const SEARCH_CONTAINER_HEIGHT = document.querySelector(".search-container").clientHeight;
+ let mainContent = document.querySelector(".main-content");
+ let top = element.getBoundingClientRect().top - SEARCH_CONTAINER_HEIGHT;
+ mainContent.scroll({
+ top,
+ behavior: "smooth",
+ });
+}
+
function helpButtonCommand() {
let pane = history.state;
let categories = document.getElementById("categories");
let helpTopic = categories.querySelector(".category[value=" + pane + "]")
.getAttribute("helpTopic");
openHelpLink(helpTopic);
}
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -702,52 +702,52 @@
value="&a11yPrivacy.learnmore.label;"></label>
</hbox>
</vbox>
</groupbox>
<hbox id="dataCollectionCategory"
class="subcategory"
hidden="true"
- data-category="panePrivacy"
- data-subcategory="reports">
+ data-category="panePrivacy">
<label class="header-name" flex="1">&dataCollection.label;</label>
</hbox>
<!-- Firefox Data Collection and Use -->
#ifdef MOZ_DATA_REPORTING
-<groupbox id="dataCollectionGroup" data-category="panePrivacy" data-subcategory="reports" hidden="true">
+<groupbox id="dataCollectionGroup" data-category="panePrivacy" hidden="true">
<caption class="search-header" hidden="true"><label>&dataCollection.label;</label></caption>
- <vbox>
- <description>
- <label class="tail-with-learn-more">&dataCollectionDesc.label;</label><label id="dataCollectionPrivacyNotice" class="learnMore text-link">&dataCollectionPrivacyNotice.label;</label>
- </description>
+ <description>
+ <label class="tail-with-learn-more">&dataCollectionDesc.label;</label><label id="dataCollectionPrivacyNotice" class="learnMore text-link">&dataCollectionPrivacyNotice.label;</label>
+ </description>
+ <vbox data-subcategory="reports">
<description flex="1">
<checkbox id="submitHealthReportBox" label="&enableHealthReport2.label;"
class="tail-with-learn-more"
accesskey="&enableHealthReport2.accesskey;"/>
<label id="FHRLearnMore"
class="learnMore text-link">&healthReportLearnMore.label;</label>
</description>
#ifndef MOZ_TELEMETRY_REPORTING
<description id="TelemetryDisabledDesc" class="indent tip-caption" control="telemetryGroup">&healthReportingDisabled.label;</description>
#endif
- </vbox>
+
#ifdef MOZ_CRASHREPORTER
- <hbox align="center">
- <checkbox id="automaticallySubmitCrashesBox"
- class="tail-with-learn-more"
- preference="browser.crashReports.unsubmittedCheck.autoSubmit"
- label="&alwaysSubmitCrashReports1.label;"
- accesskey="&alwaysSubmitCrashReports1.accesskey;"/>
- <label id="crashReporterLearnMore"
- class="learnMore text-link">&crashReporterLearnMore.label;</label>
- </hbox>
+ <hbox align="center">
+ <checkbox id="automaticallySubmitCrashesBox"
+ class="tail-with-learn-more"
+ preference="browser.crashReports.unsubmittedCheck.autoSubmit"
+ label="&alwaysSubmitCrashReports1.label;"
+ accesskey="&alwaysSubmitCrashReports1.accesskey;"/>
+ <label id="crashReporterLearnMore"
+ class="learnMore text-link">&crashReporterLearnMore.label;</label>
+ </hbox>
#endif
+ </vbox>
</groupbox>
#endif
<hbox id="securityCategory"
class="subcategory"
hidden="true"
data-category="panePrivacy">
<label class="header-name" flex="1">&security.label;</label>
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -68,16 +68,17 @@ skip-if = e10s
[browser_privacypane_8.js]
[browser_sanitizeOnShutdown_prefLocked.js]
[browser_searchsuggestions.js]
[browser_security-1.js]
[browser_security-2.js]
[browser_siteData.js]
[browser_siteData2.js]
[browser_siteData3.js]
+[browser_spotlight.js]
[browser_site_login_exceptions.js]
[browser_permissions_dialog.js]
[browser_cookies_dialog.js]
[browser_subdialogs.js]
support-files =
subdialog.xul
subdialog2.xul
[browser_telemetry.js]
--- a/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
+++ b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
@@ -20,17 +20,18 @@ add_task(async function() {
prefs = await openPreferencesViaHash("nonexistant-category");
is(prefs.selectedPane, "paneGeneral", "General pane is selected when hash is a nonexistant-category");
prefs = await openPreferencesViaHash();
is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
prefs = await openPreferencesViaOpenPreferencesAPI("privacy-reports", {leaveOpen: true});
is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default");
let doc = gBrowser.contentDocument;
is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
- ok(doc.querySelector("#locationBarGroup").hidden, "Location Bar prefs should be hidden when only Reports are requested");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"), "Wait for the reports section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "reports", "The reports section is spotlighted.");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475.
add_task(async function() {
let prefs = await openPreferencesViaOpenPreferencesAPI("general", {leaveOpen: true});
is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
let doc = gBrowser.contentDocument;
@@ -38,17 +39,18 @@ add_task(async function() {
// The reasons that here just call the `openPreferences` API without the helping function are
// - already opened one about:preferences tab up there and
// - the goal is to test on the existing tab and
// - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event
openPreferences("privacy-reports");
let selectedPane = gBrowser.contentWindow.history.state;
is(selectedPane, "panePrivacy", "Privacy pane should be selected");
is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
- ok(doc.querySelector("#locationBarGroup").hidden, "Location Bar prefs should be hidden when only Reports are requested");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"), "Wait for the reports section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "reports", "The reports section is spotlighted.");
await BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
// Test opening to a subcategory displays the correct values for preferences
add_task(async function() {
// Skip if crash reporting isn't enabled since the checkbox will be missing.
if (!AppConstants.MOZ_CRASHREPORTER) {
return;
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_spotlight.js
@@ -0,0 +1,37 @@
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+add_task(async function test_reports_section() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("privacy-reports", {leaveOpen: true});
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"),
+ "Wait for the reports section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "reports",
+ "The reports section is spotlighted.");
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_address_autofill_section() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("privacy-address-autofill", {leaveOpen: true});
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"),
+ "Wait for the ddress-autofill section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "address-autofill",
+ "The ddress-autofill section is spotlighted.");
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_credit_card_autofill_section() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("privacy-credit-card-autofill", {leaveOpen: true});
+ is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected by default");
+ let doc = gBrowser.contentDocument;
+ is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"),
+ "Wait for the credit-card-autofill section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "credit-card-autofill",
+ "The credit-card-autofill section is spotlighted.");
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
--- a/browser/components/uitour/test/browser_openPreferences.js
+++ b/browser/components/uitour/test/browser_openPreferences.js
@@ -40,13 +40,14 @@ add_UITour_task(async function test_open
!(AppConstants.MOZ_DATA_REPORTING && AppConstants.MOZ_CRASHREPORTER)) {
return;
}
let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, "about:preferences#privacy-reports");
await gContentAPI.openPreferences("privacy-reports");
let tab = await promiseTabOpened;
await BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "Initialized");
let doc = gBrowser.selectedBrowser.contentDocument;
- let reports = doc.querySelector("groupbox[data-subcategory='reports']");
is(doc.location.hash, "#privacy", "Should not display the reports subcategory in the location hash.");
- is(reports.hidden, false, "Should open to the reports subcategory in the privacy pane in the new Preferences.");
+ await TestUtils.waitForCondition(() => doc.querySelector(".spotlight"),
+ "Wait for the reports section is spotlighted.");
+ is(doc.querySelector(".spotlight").getAttribute("data-subcategory"), "reports", "The reports section is spotlighted.");
await BrowserTestUtils.removeTab(tab);
});
--- a/browser/extensions/formautofill/FormAutofillPreferences.jsm
+++ b/browser/extensions/formautofill/FormAutofillPreferences.jsm
@@ -82,16 +82,17 @@ FormAutofillPreferences.prototype = {
savedAddressesBtn.className = "accessory-button";
addressAutofillCheckbox.className = "tail-with-learn-more";
addressAutofillLearnMore.className = "learnMore text-link";
formAutofillGroup.id = "formAutofillGroup";
addressAutofill.id = "addressAutofill";
addressAutofillLearnMore.id = "addressAutofillLearnMore";
+ addressAutofill.setAttribute("data-subcategory", "address-autofill");
addressAutofillLearnMore.setAttribute("value", this.bundle.GetStringFromName("learnMoreLabel"));
addressAutofillCheckbox.setAttribute("label", this.bundle.GetStringFromName("autofillAddressesCheckbox"));
savedAddressesBtn.setAttribute("label", this.bundle.GetStringFromName("savedAddressesBtnLabel"));
addressAutofillLearnMore.setAttribute("href", learnMoreURL);
// Add preferences search support
savedAddressesBtn.setAttribute("searchkeywords", MANAGE_ADDRESSES_KEYWORDS.concat(EDIT_ADDRESS_KEYWORDS)
@@ -125,16 +126,17 @@ FormAutofillPreferences.prototype = {
let savedCreditCardsBtn = document.createElementNS(XUL_NS, "button");
savedCreditCardsBtn.className = "accessory-button";
creditCardAutofillCheckbox.className = "tail-with-learn-more";
creditCardAutofillLearnMore.className = "learnMore text-link";
creditCardAutofill.id = "creditCardAutofill";
creditCardAutofillLearnMore.id = "creditCardAutofillLearnMore";
+ creditCardAutofill.setAttribute("data-subcategory", "credit-card-autofill");
creditCardAutofillLearnMore.setAttribute("value", this.bundle.GetStringFromName("learnMoreLabel"));
creditCardAutofillCheckbox.setAttribute("label", this.bundle.GetStringFromName("autofillCreditCardsCheckbox"));
savedCreditCardsBtn.setAttribute("label", this.bundle.GetStringFromName("savedCreditCardsBtnLabel"));
creditCardAutofillLearnMore.setAttribute("href", learnMoreURL);
// Add preferences search support
savedCreditCardsBtn.setAttribute("searchkeywords", MANAGE_CREDITCARDS_KEYWORDS.concat(EDIT_CREDITCARD_KEYWORDS)
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -90,16 +90,34 @@ button > hbox > label {
}
.accessory-button {
height: 30px;
min-width: 150px;
margin: 4px 0;
}
+.spotlight {
+ background-color: rgba(0,200,215,0.3);
+ /* Show the border to spotlight the components in high-contrast mode. */
+ border: 1px solid transparent;
+ border-radius: 2px;
+}
+
+[data-subcategory] {
+ margin-left: -4px;
+ margin-right: -4px;
+ padding-left: 4px;
+ padding-right: 4px;
+}
+
+[data-subcategory] > .groupbox-title {
+ padding-inline-start: 4px;
+}
+
#searchInput {
border-radius: 0;
}
/* Subcategory title */
/**
* The first subcategory title for each category should not have margin-top.
@@ -720,19 +738,24 @@ button > hbox > label {
margin-inline-end: 8px;
margin-top: 5px;
margin-bottom: 5px;
}
.search-container {
position: sticky;
background-color: var(--in-content-page-background);
- width: 100%;
top: 0;
z-index: 1;
+ /* The search-container should have the capability to cover all spotlight area. */
+ width: calc(100% + 8px);
+ margin-left: -4px;
+ margin-right: -4px;
+ padding-left: 4px;
+ padding-right: 4px;
}
#searchInput {
margin: 20px 0 30px 0;
}
#searchInput .textbox-search-icons:not([selectedIndex="1"]) {
display: none;