Bug 1300995 - Part 1. Add a footer on formautofill popup to let users open a preferences privacy tab when click on it. r=MattN
authorRay Lin <ralin@mozilla.com>
Thu, 01 Jun 2017 21:53:37 +0800
changeset 415584 329a0ee18186ac3cca6b1b2b429b5e8b02667fdf
parent 415583 c9c74cf6fe4e56c436b893596fa3b6a5f98299ea
child 415585 53bde2fc1fa5400c1b0131dfc487029395153f5d
push id1517
push userjlorenzo@mozilla.com
push dateThu, 14 Sep 2017 16:50:54 +0000
treeherdermozilla-release@3b41fd564418 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1300995
milestone56.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 1300995 - Part 1. Add a footer on formautofill popup to let users open a preferences privacy tab when click on it. r=MattN MozReview-Commit-ID: Izr6IbHlkLY
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillParent.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/content/formautofill.css
browser/extensions/formautofill/content/formautofill.xml
browser/extensions/formautofill/locale/en-US/formautofill.properties
browser/extensions/formautofill/skin/linux/autocomplete-item.css
browser/extensions/formautofill/skin/osx/autocomplete-item.css
browser/extensions/formautofill/skin/shared/autocomplete-item.css
browser/extensions/formautofill/skin/windows/autocomplete-item.css
browser/extensions/formautofill/test/mochitest/formautofill_common.js
browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
toolkit/components/telemetry/Histograms.json
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -191,16 +191,20 @@ let ProfileAutocomplete = {
     this._factory.unregister();
     this._factory = null;
     this._registered = false;
     this._lastAutoCompleteResult = null;
 
     Services.obs.removeObserver(this, "autocomplete-will-enter-text");
   },
 
+  getProfileAutoCompleteResult() {
+    return this._lastAutoCompleteResult;
+  },
+
   setProfileAutoCompleteResult(result) {
     this._lastAutoCompleteResult = result;
     this._lastAutoCompleteFocusedInput = formFillController.focusedInput;
   },
 
   observe(subject, topic, data) {
     switch (topic) {
       case "autocomplete-will-enter-text": {
@@ -485,27 +489,47 @@ var FormAutofillContent = {
       return;
     }
 
     formFillController.markAsAutofillField(field);
   },
 
   _previewProfile(doc) {
     let selectedIndex = ProfileAutocomplete._getSelectedIndex(doc.ownerGlobal);
+    let lastAutoCompleteResult = ProfileAutocomplete.getProfileAutoCompleteResult();
 
-    if (selectedIndex === -1) {
+    if (selectedIndex === -1 ||
+        !lastAutoCompleteResult ||
+        lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
       ProfileAutocomplete._clearProfilePreview();
     } else {
       ProfileAutocomplete._previewSelectedProfile(selectedIndex);
     }
   },
 
   _messageManagerFromWindow(win) {
     return win.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIWebNavigation)
               .QueryInterface(Ci.nsIDocShell)
               .QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIContentFrameMessageManager);
   },
+
+  _onKeyDown(e) {
+    let lastAutoCompleteResult = ProfileAutocomplete.getProfileAutoCompleteResult();
+    let focusedInput = formFillController.focusedInput;
+
+    if (e.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_RETURN || !lastAutoCompleteResult || !focusedInput) {
+      return;
+    }
+
+    let selectedIndex = ProfileAutocomplete._getSelectedIndex(e.target.ownerGlobal);
+    let selectedRowStyle = lastAutoCompleteResult.getStyleAt(selectedIndex);
+    if (selectedRowStyle == "autofill-footer") {
+      focusedInput.addEventListener("DOMAutoComplete", () => {
+        Services.cpmm.sendAsyncMessage("FormAutofill:OpenPreferences");
+      }, {once: true});
+    }
+  },
 };
 
 
 FormAutofillContent.init();
--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -37,16 +37,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillPreferences",
                                   "resource://formautofill/FormAutofillPreferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillDoorhanger",
                                   "resource://formautofill/FormAutofillDoorhanger.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
 
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 const ENABLED_PREF = "extensions.formautofill.addresses.enabled";
 
 function FormAutofillParent() {
   // Lazily load the storage JSM to avoid disk I/O until absolutely needed.
@@ -76,16 +78,17 @@ FormAutofillParent.prototype = {
    * Initializes ProfileStorage and registers the message handler.
    */
   async init() {
     Services.obs.addObserver(this, "advanced-pane-loaded");
     Services.ppmm.addMessageListener("FormAutofill:InitStorage", this);
     Services.ppmm.addMessageListener("FormAutofill:GetAddresses", 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
     Services.prefs.addObserver(ENABLED_PREF, this);
     Services.obs.addObserver(this, "formautofill-storage-changed");
   },
 
   observe(subject, topic, data) {
@@ -191,16 +194,21 @@ FormAutofillParent.prototype = {
         break;
       }
       case "FormAutofill:RemoveAddresses": {
         data.guids.forEach(guid => this.profileStorage.addresses.remove(guid));
         break;
       }
       case "FormAutofill:OnFormSubmit": {
         this._onFormSubmit(data, target);
+        break;
+      }
+      case "FormAutofill:OpenPreferences": {
+        const win = RecentWindow.getMostRecentBrowserWindow();
+        win.openPreferences("panePrivacy", {origin: "autofillFooter"});
       }
     }
   },
 
   /**
    * Uninitializes FormAutofillParent. This is for testing only.
    *
    * @private
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -32,16 +32,22 @@ this.ProfileAutoCompleteResult = functio
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
   } else {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
   }
 
   this._popupLabels = this._generateLabels(this._focusedFieldName,
                                            this._allFieldNames,
                                            this._matchingProfiles);
+  // Add an empty result entry for footer. Its content will come from
+  // the footer binding, so don't assign any value to it.
+  this._popupLabels.push({
+    primary: "",
+    secondary: "",
+  });
 };
 
 ProfileAutoCompleteResult.prototype = {
 
   // The user's query string
   searchString: "",
 
   // The default item that should be entered if none is selected
@@ -174,16 +180,19 @@ ProfileAutoCompleteResult.prototype = {
 
   /**
    * Retrieves a style hint specific to a particular index.
    * @param   {number} index The index of the style hint requested
    * @returns {string} The style hint at the specified index
    */
   getStyleAt(index) {
     this._checkIndexBounds(index);
+    if (index == this.matchCount - 1) {
+      return "autofill-footer";
+    }
     return "autofill-profile";
   },
 
   /**
    * Retrieves an image url.
    * @param   {number} index The index of the image url requested
    * @returns {string} The image url at the specified index
    */
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -22,16 +22,17 @@ Cu.import("resource://formautofill/FormA
  *
  * NOTE: Declares it by "var" to make it accessible in unit tests.
  */
 var FormAutofillFrameScript = {
   init() {
     addEventListener("focusin", this);
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutoComplete:PopupClosed", this);
+    addMessageListener("FormAutoComplete:PopupOpened", this);
   },
 
   handleEvent(evt) {
     if (!evt.isTrusted) {
       return;
     }
 
     if (!Services.prefs.getBoolPref("extensions.formautofill.addresses.enabled")) {
@@ -60,18 +61,31 @@ var FormAutofillFrameScript = {
     }
   },
 
   receiveMessage(message) {
     if (!Services.prefs.getBoolPref("extensions.formautofill.addresses.enabled")) {
       return;
     }
 
+    const doc = content.document;
+    const {chromeEventHandler} = doc.ownerGlobal.getInterface(Ci.nsIDocShell);
+
     switch (message.name) {
-      case "FormAutofill:PreviewProfile":
-      case "FormAutoComplete:PopupClosed":
-        FormAutofillContent._previewProfile(content.document);
+      case "FormAutofill:PreviewProfile": {
+        FormAutofillContent._previewProfile(doc);
         break;
+      }
+      case "FormAutoComplete:PopupClosed": {
+        FormAutofillContent._previewProfile(doc);
+        chromeEventHandler.removeEventListener("keydown", FormAutofillContent._onKeyDown,
+                                               {capturing: true});
+        break;
+      }
+      case "FormAutoComplete:PopupOpened": {
+        chromeEventHandler.addEventListener("keydown", FormAutofillContent._onKeyDown,
+                                            {capturing: true});
+      }
     }
   },
 };
 
 FormAutofillFrameScript.init();
--- a/browser/extensions/formautofill/content/formautofill.css
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -1,23 +1,31 @@
 /* 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/. */
 
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"] {
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
   display: block;
   margin: 0;
   padding: 0;
   height: auto;
   min-height: auto;
+}
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"] {
   -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem");
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
+  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-footer");
+}
+
 /* Treat @collpased="true" as display: none similar to how it is for XUL elements.
  * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"] {
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"] {
   display: none;
 }
 
 #PopupAutoComplete[firstresultstyle="autofill-profile"] {
   min-width: 150px !important;
 }
--- a/browser/extensions/formautofill/content/formautofill.xml
+++ b/browser/extensions/formautofill/content/formautofill.xml
@@ -4,44 +4,102 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <bindings id="formautofillBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
-  <binding id="autocomplete-profile-listitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+  <binding id="autocomplete-profile-listitem-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
     <resources>
       <stylesheet src="chrome://formautofill-shared/skin/autocomplete-item.css"/>
       <stylesheet src="chrome://formautofill/skin/autocomplete-item.css"/>
     </resources>
 
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+      </constructor>
+      <!-- For form autofill, we want to unify the selection no matter by
+      keyboard navigation or mouseover in order not to confuse user which
+      profile preview is being shown. This field is set to true to indicate
+      that selectedIndex of popup should be changed while mouseover item -->
+      <field name="selectedByMouseOver">true</field>
+
+      <property name="_stringBundle">
+        <getter><![CDATA[
+          /* global Services */
+          if (!this.__stringBundle) {
+            this.__stringBundle = Services.strings.createBundle("chrome://formautofill/locale/formautofill.properties");
+          }
+          return this.__stringBundle;
+        ]]></getter>
+      </property>
+
+      <method name="_cleanup">
+        <body>
+        <![CDATA[
+          this.removeAttribute("formautofillattached");
+          if (this._itemBox) {
+            this._itemBox.removeAttribute("size");
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="_onChanged">
+        <body>
+        </body>
+      </method>
+
+      <method name="_onOverflow">
+        <body></body>
+      </method>
+
+      <method name="_onUnderflow">
+        <body></body>
+      </method>
+
+      <method name="_adjustProfileItemLayout">
+        <body>
+        <![CDATA[
+          let outerBoxRect = this.parentNode.getBoundingClientRect();
+
+          // Make item fit in popup as XUL box could not constrain
+          // item's width
+          this._itemBox.style.width = outerBoxRect.width + "px";
+          // Use two-lines layout when width is smaller than 150px
+          if (outerBoxRect.width <= 150) {
+            this._itemBox.setAttribute("size", "small");
+          } else {
+            this._itemBox.removeAttribute("size");
+          }
+        ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+
+  <binding id="autocomplete-profile-listitem" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
     <xbl:content xmlns="http://www.w3.org/1999/xhtml">
-      <div anonid="profile-item-box" class="profile-item-box">
+      <div anonid="autofill-item-box" class="autofill-item-box">
         <div class="profile-label-col profile-item-col">
           <span anonid="profile-label" class="profile-label"></span>
         </div>
         <div class="profile-comment-col profile-item-col">
           <span anonid="profile-comment" class="profile-comment"></span>
         </div>
       </div>
     </xbl:content>
 
     <implementation implements="nsIDOMXULSelectControlItemElement">
-      <!-- For form autofill, we want to unify the selection no matter by
-      keyboard navigation or mouseover in order not to confuse user which
-      profile preview is being shown. This field is set to true to indicate
-      that selectedIndex of popup should be changed while mouseover item -->
-      <field name="selectedByMouseOver">true</field>
-
       <constructor>
         <![CDATA[
           this._itemBox = document.getAnonymousElementByAttribute(
-            this, "anonid", "profile-item-box"
+            this, "anonid", "autofill-item-box"
           );
           this._label = document.getAnonymousElementByAttribute(
             this, "anonid", "profile-label"
           );
           this._comment = document.getAnonymousElementByAttribute(
             this, "anonid", "profile-comment"
           );
 
@@ -61,53 +119,72 @@
           let {AutoCompletePopup} = Cu.import("resource://gre/modules/AutoCompletePopup.jsm", {});
 
           AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");
 
           return val;
         ]]></setter>
       </property>
 
-      <method name="_cleanup">
-        <body>
-        <![CDATA[
-            this._itemBox.removeAttribute("size");
-        ]]>
-        </body>
-      </method>
-
-      <method name="_onChanged">
-        <body></body>
-      </method>
-
-      <method name="_onOverflow">
-        <body></body>
-      </method>
-
-      <method name="_onUnderflow">
-        <body></body>
-      </method>
 
       <method name="_adjustAcItem">
         <body>
         <![CDATA[
-          let outerBoxRect = this.parentNode.getBoundingClientRect();
+          this._adjustProfileItemLayout();
+          this.setAttribute("formautofillattached", "true");
+
           let {primary, secondary} = JSON.parse(this.getAttribute("ac-value"));
 
           this._label.textContent = primary;
           this._comment.textContent = secondary;
+        ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
 
-          // Make item fit in popup as XUL box could not constrain
-          // item's width
-          this._itemBox.style.width =  outerBoxRect.width + "px";
-          // Use two-lines layout when width is smaller than 150px
-          if (outerBoxRect.width <= 150) {
-            this._itemBox.setAttribute("size", "small");
-          } else {
-            this._itemBox.removeAttribute("size");
+  <binding id="autocomplete-profile-listitem-footer" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
+    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
+      <div anonid="autofill-footer" class="autofill-item-box autofill-footer">
+      </div>
+    </xbl:content>
+
+    <handlers>
+      <handler event="click" button="0"><![CDATA[
+        window.openPreferences("panePrivacy", {origin: "autofillFooter"});
+      ]]></handler>
+    </handlers>
+
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+        <![CDATA[
+          this._itemBox = document.getAnonymousElementByAttribute(
+            this, "anonid", "autofill-footer"
+          );
+
+          this._adjustAcItem();
+        ]]>
+      </constructor>
+
+      <method name="_adjustAcItem">
+        <body>
+        <![CDATA[
+          /* global Cu */
+          this._adjustProfileItemLayout();
+          this.setAttribute("formautofillattached", "true");
+
+          let {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
+          let footerTextBundleKey = AppConstants.platform == "macosx" ?
+            "autocompleteFooterOptionOSX" : "autocompleteFooterOption";
+          // If the popup shows up with small layout, we should use short string to
+          // have a better fit in the box.
+          if (this._itemBox.getAttribute("size") == "small") {
+            footerTextBundleKey += "Short";
           }
+          let footerText = this._stringBundle.GetStringFromName(footerTextBundleKey);
+          this._itemBox.textContent = footerText;
         ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
 </bindings>
--- a/browser/extensions/formautofill/locale/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locale/en-US/formautofill.properties
@@ -3,8 +3,12 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 preferenceGroupTitle = Form Autofill
 enableProfileAutofill = Enable Profile Autofill
 savedProfiles = Saved Profiles…
 saveAddressMessage = Firefox now saves your form data to help you fill out forms faster!
 viewAutofillOptions = View Form Autofill options…
 openAutofillMessagePanel = Open Form Autofill message panel
+autocompleteFooterOption = Form Autofill Options
+autocompleteFooterOptionShort = Options
+autocompleteFooterOptionOSX = Form Autofill Preferences
+autocompleteFooterOptionOSXShort = Preferences
--- a/browser/extensions/formautofill/skin/linux/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/linux/autocomplete-item.css
@@ -1,14 +1,18 @@
 /* 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/. */
 
 @namespace url("http://www.w3.org/1999/xhtml");
 
 
-.profile-item-box > .profile-item-col > .profile-label {
+.autofill-item-box > .profile-item-col > .profile-label {
   font-size: .84em;
 }
 
-.profile-item-box > .profile-item-col > .profile-comment {
+.autofill-item-box > .profile-item-col > .profile-comment {
   font-size: .7em;
 }
+
+.autofill-footer {
+  font-size: .77em;
+}
--- a/browser/extensions/formautofill/skin/osx/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/osx/autocomplete-item.css
@@ -1,14 +1,14 @@
 /* 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/. */
 
 @namespace url("http://www.w3.org/1999/xhtml");
 
 
-.profile-item-box > .profile-item-col > .profile-label {
+.autofill-item-box > .profile-item-col > .profile-label {
   font-size: 1.09em;
 }
 
-.profile-item-box > .profile-item-col > .profile-comment {
+.autofill-item-box > .profile-item-col > .profile-comment {
   font-size: .9em;
 }
--- a/browser/extensions/formautofill/skin/shared/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/shared/autocomplete-item.css
@@ -1,71 +1,87 @@
 /* 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/. */
 
 @namespace url("http://www.w3.org/1999/xhtml");
 @namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 
-xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .profile-item-box {
+xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
   background-color: #F2F2F2;
 }
 
-.profile-item-box {
+xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-footer {
+  background-color: #DCDCDE;
+}
+
+.autofill-item-box {
   --item-padding-vertical: 6px;
   --item-padding-horizontal: 10px;
   --col-spacer: 7px;
   --item-width: calc(50% - (var(--col-spacer) / 2));
+  --item-text-color: -moz-FieldText;
 }
 
-.profile-item-box[size="small"] {
+.autofill-item-box[size="small"] {
   --item-padding-vertical: 7px;
   --col-spacer: 0px;
   --row-spacer: 3px;
   --item-width: 100%;
 }
 
-.profile-item-box {
+.autofill-footer {
+  --item-padding-vertical: 0;
+  --item-padding-horizontal: 0;
+}
+
+.autofill-item-box {
   box-sizing: border-box;
   margin: 0;
   border-bottom: 1px solid rgba(38,38,38,.15);
   padding: var(--item-padding-vertical) 0;
   padding-inline-start: var(--item-padding-horizontal);
   padding-inline-end: var(--item-padding-horizontal);
   display: flex;
   flex-direction: row;
   flex-wrap: wrap;
   align-items: center;
   background-color: #FFFFFF;
-  color: -moz-FieldText
+  color: var(--item-text-color);
 }
 
-.profile-item-box:last-child {
+.autofill-item-box:last-child {
   border-bottom: 0;
 }
 
-.profile-item-box > .profile-item-col {
+.autofill-item-box > .profile-item-col {
   box-sizing: border-box;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   width: var(--item-width);
 }
 
-.profile-item-box > .profile-label-col {
+.autofill-item-box > .profile-label-col {
   text-align: start;
 }
 
-.profile-item-box > .profile-comment-col {
+.autofill-item-box > .profile-comment-col {
   margin-inline-start: var(--col-spacer);
   text-align: end;
   color: GrayText;
 }
 
-.profile-item-box[size="small"] {
+.autofill-item-box[size="small"] {
   flex-direction: column;
 }
 
-.profile-item-box[size="small"] > .profile-comment-col {
+.autofill-item-box[size="small"] > .profile-comment-col {
   margin-top: var(--row-spacer);
   text-align: start;
 }
+
+.autofill-footer {
+  height: 41px;
+  background-color: #EDEDED;
+  justify-content: center;
+}
--- a/browser/extensions/formautofill/skin/windows/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/windows/autocomplete-item.css
@@ -1,17 +1,21 @@
 /* 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/. */
 
 @namespace url("http://www.w3.org/1999/xhtml");
 @namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 
-.profile-item-box > .profile-item-col > .profile-comment {
+.autofill-item-box > .profile-item-col > .profile-comment {
   font-size: .83em;
 }
 
+.autofill-footer {
+  font-size: .91em;
+}
+
 @media (-moz-windows-default-theme: 0) {
-  xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .profile-item-box {
+  xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
     background-color: Highlight;
   }
 }
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -38,20 +38,22 @@ async function onAddressChanged(type) {
     formFillChromeScript.addMessageListener("formautofill-storage-changed", function onChanged(data) {
       formFillChromeScript.removeMessageListener("formautofill-storage-changed", onChanged);
       is(data.data, type, `Receive ${type} storage changed event`);
       resolve();
     });
   });
 }
 
-function checkMenuEntries(expectedValues) {
+function checkMenuEntries(expectedValues, isFormAutofillResult = true) {
   let actualValues = getMenuEntries();
+  // Expect one more item would appear at the bottom as the footer if the result is from form autofill.
+  let expectedLength = isFormAutofillResult ? expectedValues.length + 1 : expectedValues.length;
 
-  is(actualValues.length, expectedValues.length, " Checking length of expected menu");
+  is(actualValues.length, expectedLength, " Checking length of expected menu");
   for (let i = 0; i < expectedValues.length; i++) {
     is(actualValues[i], expectedValues[i], " Checking menu entry #" + i);
   }
 }
 
 async function addAddress(address) {
   return new Promise(resolve => {
     formFillChromeScript.sendAsyncMessage("FormAutofillTest:AddAddress", {address});
--- a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
@@ -102,17 +102,17 @@ async function setupFormHistory() {
 
 // Form with history only.
 add_task(async function history_only_menu_checking() {
   await setupFormHistory();
 
   await setInput("#tel", "");
   doKey("down");
   await expectPopup();
-  checkMenuEntries(["1-234-567-890"]);
+  checkMenuEntries(["1-234-567-890"], false);
 });
 
 // Form with both history and address storage.
 add_task(async function check_menu_when_both_existed() {
   await setupAddressStorage();
 
   await setInput("#organization", "");
   doKey("down");
@@ -136,17 +136,17 @@ add_task(async function check_menu_when_
   ));
 });
 
 // Display history search result if no matched data in addresses.
 add_task(async function check_fallback_for_mismatched_field() {
   await setInput("#email", "");
   doKey("down");
   await expectPopup();
-  checkMenuEntries(["foo@mozilla.com"]);
+  checkMenuEntries(["foo@mozilla.com"], false);
 });
 
 // Autofill the address from dropdown menu.
 add_task(async function check_fields_after_form_autofill() {
   await setInput("#organization", "Moz");
   doKey("down");
   await expectPopup();
   checkMenuEntries(MOCK_STORAGE.map(address =>
@@ -156,17 +156,17 @@ add_task(async function check_fields_aft
   await checkFormFilled(MOCK_STORAGE[1]);
 });
 
 // Fallback to history search after autofill address.
 add_task(async function check_fallback_after_form_autofill() {
   await setInput("#tel", "");
   doKey("down");
   await expectPopup();
-  checkMenuEntries(["1-234-567-890"]);
+  checkMenuEntries(["1-234-567-890"], false);
 });
 
 // Resume form autofill once all the autofilled fileds are changed.
 add_task(async function check_form_autofill_resume() {
   document.querySelector("#tel").blur();
   document.querySelector("#form1").reset();
   await setInput("#tel", "");
   doKey("down");
--- a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
+++ b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html
@@ -118,16 +118,21 @@ add_task(async function check_preview() 
   checkFormPreviewFields();
 
   for (let i = 0; i < MOCK_STORAGE.length; i++) {
     doKey("down");
     await notifySelectedIndex(i);
     checkFormPreviewFields(MOCK_STORAGE[i]);
   }
 
+  // Navigate to the footer
+  doKey("down");
+  await notifySelectedIndex(MOCK_STORAGE.length);
+  checkFormPreviewFields();
+
   doKey("down");
   await notifySelectedIndex(-1);
   checkFormPreviewFields();
 
   focusedInput.blur();
 });
 
 add_task(async function check_filled_highlight() {
--- a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
+++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
@@ -165,31 +165,38 @@ add_task(function* test_all_patterns() {
   testCases.forEach(testCase => {
     do_print("Starting testcase: " + testCase.description);
     let actual = new ProfileAutoCompleteResult(testCase.searchString,
                                                testCase.fieldName,
                                                testCase.allFieldNames,
                                                testCase.matchingProfiles,
                                                testCase.options);
     let expectedValue = testCase.expected;
+    let expectedItemLength = expectedValue.items.length;
+    // If the last item shows up as a footer, we expect one more item
+    // than expected.
+    if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") {
+      expectedItemLength++;
+    }
+
     equal(actual.searchResult, expectedValue.searchResult);
     equal(actual.defaultIndex, expectedValue.defaultIndex);
-    equal(actual.matchCount, expectedValue.items.length);
+    equal(actual.matchCount, expectedItemLength);
     expectedValue.items.forEach((item, index) => {
       equal(actual.getValueAt(index), item.value);
       equal(actual.getCommentAt(index), item.comment);
       equal(actual.getLabelAt(index), item.label);
       equal(actual.getStyleAt(index), item.style);
       equal(actual.getImageAt(index), item.image);
     });
 
     if (expectedValue.items.length != 0) {
-      Assert.throws(() => actual.getValueAt(expectedValue.items.length),
+      Assert.throws(() => actual.getValueAt(expectedItemLength),
         /Index out of range\./);
 
-      Assert.throws(() => actual.getLabelAt(expectedValue.items.length),
+      Assert.throws(() => actual.getLabelAt(expectedItemLength),
         /Index out of range\./);
 
-      Assert.throws(() => actual.getCommentAt(expectedValue.items.length),
+      Assert.throws(() => actual.getCommentAt(expectedItemLength),
         /Index out of range\./);
     }
   });
 });
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6428,17 +6428,17 @@
     "description": "Count how often each preference category is opened."
   },
   "FX_PREFERENCES_OPENED_VIA": {
     "record_in_processes": ["main", "content"],
     "bug_numbers": [1330315],
     "alert_emails": ["jaws@mozilla.com"],
     "expires_in_version": "59",
     "kind": "categorical",
-    "labels": ["aboutHome", "aboutTelemetry", "browserMedia", "commandLine", "commandLineLegacy", "ContainersCommand", "contentSearch", "dataReporting", "doorhanger", "devDisconnectedAlert", "experimentsOpenPref", "fxa", "fxaSignedin", "fxaError", "offlineApps", "prefserviceDefaults", "preferencesButton", "paneSync", "storagePressure", "translationInfobar", "UITour", "menubar", "notifOpenSettings", "other"],
+    "labels": ["aboutHome", "aboutTelemetry", "browserMedia", "commandLine", "commandLineLegacy", "ContainersCommand", "contentSearch", "dataReporting", "doorhanger", "devDisconnectedAlert", "experimentsOpenPref", "fxa", "fxaSignedin", "fxaError", "offlineApps", "prefserviceDefaults", "preferencesButton", "paneSync", "storagePressure", "translationInfobar", "UITour", "menubar", "notifOpenSettings", "other", "autofillFooter"],
     "releaseChannelCollection": "opt-out",
     "description":"Count how the Preferences are opened."
   },
   "INPUT_EVENT_RESPONSE_MS": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "bug_numbers": [1235908],
     "expires_in_version": "never",