Bug 1550669 - Add a second row to autocomplete items for logins that shows origins. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Fri, 10 May 2019 17:58:10 +0000
changeset 532247 a1c80c3d3855ecc1f6281cb8cd5cfaa161326896
parent 532246 06e683412cfbd40b38c9838f0cdd5fe76019744d
child 532248 633722f0bc3d0444d3b5f2227b7d14680897c88b
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1550669
milestone68.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 1550669 - Add a second row to autocomplete items for logins that shows origins. r=jaws Based on a patch by Jared Wein <jwein@mozilla.com> Differential Revision: https://phabricator.services.mozilla.com/D27719
browser/base/content/browser.css
browser/themes/shared/autocomplete.inc.css
modules/libpref/init/all.js
toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/content/widgets/autocomplete-popup.js
toolkit/content/widgets/autocomplete-richlistitem.js
toolkit/content/widgets/autocomplete.xml
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -713,16 +713,17 @@ html|input.urlbar-input {
   min-width: 200px;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
   -moz-binding: none;
   height: auto;
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
   margin-inline-start: 0;
   display: initial;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
   text-overflow: initial;
   white-space: initial;
--- a/browser/themes/shared/autocomplete.inc.css
+++ b/browser/themes/shared/autocomplete.inc.css
@@ -34,28 +34,66 @@
   background-color: var(--arrowpanel-dimmed);
 }
 
 .autocomplete-richlistitem[selected] {
   background-color: Highlight;
   color: HighlightText;
 }
 
-/* Login form autocompletion */
+/* Login form autocompletion with and without origin showing */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="login"] > .ac-site-icon {
   display: initial;
   list-style-image: url(chrome://browser/skin/login.svg);
   -moz-context-properties: fill;
   fill: GrayText;
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"][selected] > .login-wrapper > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="login"] > .ac-site-icon[selected] {
   fill: HighlightText;
 }
 
+/* Login form autocompletion with origin showing */
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] {
+  height: auto;
+  padding: 4px;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper {
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: row;
+  margin: 0;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .ac-site-icon {
+  margin-inline-start: auto;
+  margin-inline-end: 4px;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text {
+  /* The text should flex while the icon should not */
+  flex: 1;
+  /* width/min-width are needed to get the text-overflow: ellipsis to work for the children */
+  min-width: 0;
+  width: 0;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text > .login-row {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="loginWithOrigin"] > .login-wrapper > .login-text > .login-origin {
+  padding-top: 2px !important;
+  opacity: .6;
+}
 
 /* Insecure field warning */
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
   background-color: var(--arrowpanel-dimmed);
   border-bottom: 1px solid var(--panel-separator-color);
   padding-bottom: 4px;
   padding-top: 4px;
 }
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4715,16 +4715,17 @@ pref("signon.formlessCapture.enabled",  
 pref("signon.privateBrowsingCapture.enabled", false);
 pref("signon.storeWhenAutocompleteOff",     true);
 pref("signon.debug",                        false);
 pref("signon.recipes.path",                 "chrome://passwordmgr/content/recipes.json");
 pref("signon.schemeUpgrades",               false);
 // This temporarily prevents the master password to reprompt for autocomplete.
 pref("signon.masterPasswordReprompt.timeout_ms", 900000); // 15 Minutes
 pref("signon.showAutoCompleteFooter", false);
+pref("signon.showAutoCompleteOrigins", false);
 
 // Satchel (Form Manager) prefs
 pref("browser.formfill.debug",            false);
 pref("browser.formfill.enable",           true);
 pref("browser.formfill.expire_days",      180);
 pref("browser.formfill.agedWeight",       2);
 pref("browser.formfill.bucketSize",       1);
 pref("browser.formfill.maxTimeGroupings", 25);
--- a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
+++ b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm
@@ -25,16 +25,18 @@ ChromeUtils.defineModuleGetter(this, "Lo
 ChromeUtils.defineModuleGetter(this, "LoginHelper",
                                "resource://gre/modules/LoginHelper.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginManagerContent",
                                "resource://gre/modules/LoginManagerContent.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "formFillController",
                                    "@mozilla.org/satchel/form-fill-controller;1",
                                    Ci.nsIFormFillController);
+XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_SHOW_ORIGIN",
+                                      "signon.showAutoCompleteOrigins");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   return LoginHelper.createLogger("LoginAutoCompleteResult");
 });
 
 
 // nsIAutoCompleteResult implementation
 function LoginAutoCompleteResult(aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField, hostname}) {
@@ -86,16 +88,17 @@ function LoginAutoCompleteResult(aSearch
       return false;
     }
 
     return true;
   }
 
   this._showInsecureFieldWarning = (!isSecure && LoginHelper.showInsecureFieldWarning) ? 1 : 0;
   this._showAutoCompleteFooter = isFooterEnabled() ? 1 : 0;
+  this._showOrigin = SHOULD_SHOW_ORIGIN ? 1 : 0;
   this.searchString = aSearchString;
   this.logins = matchingLogins.sort(loginSort);
   this.matchCount = matchingLogins.length + this._showInsecureFieldWarning + this._showAutoCompleteFooter;
   this._messageManager = messageManager;
   this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
   this._dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "medium" });
 
   this._isPasswordField = isPasswordField;
@@ -185,24 +188,37 @@ LoginAutoCompleteResult.prototype = {
       let time = this._dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
       username = getLocalizedString("loginHostAge", [username, time]);
     }
 
     return username;
   },
 
   getCommentAt(index) {
-    return "";
+    if (this._showInsecureFieldWarning && index === 0) {
+      return "";
+    }
+
+    if (this._showAutoCompleteFooter && index === this.matchCount - 1) {
+      return "";
+    }
+
+    let login = this.logins[index - this._showInsecureFieldWarning];
+    return JSON.stringify({
+      loginOrigin: login.hostname,
+    });
   },
 
   getStyleAt(index) {
     if (index == 0 && this._showInsecureFieldWarning) {
       return "insecureWarning";
     } else if (this._showAutoCompleteFooter && index == this.matchCount - 1) {
       return "loginsFooter";
+    } else if (this._showOrigin) {
+      return "loginWithOrigin";
     }
 
     return "login";
   },
 
   getImageAt(index) {
     return "";
   },
--- a/toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
+++ b/toolkit/components/passwordmgr/test/unit/test_login_autocomplete_result.js
@@ -32,385 +32,385 @@ let expectedResults = [
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: true,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: true,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: true,
     insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "",
       label: "This connection is not secure. Logins entered here could be compromised. Learn More",
       style: "insecureWarning",
     }, {
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: false,
     matchingLogins,
     items: [{
       value: "",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "tempuser1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testuser3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzuser4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: false,
     matchingLogins,
@@ -420,47 +420,49 @@ let expectedResults = [
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: false,
     isSecure: true,
     isPasswordField: true,
     matchingLogins,
     items: [{
       value: "emptypass1",
       label: LABEL_NO_USERNAME,
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "temppass1",
       label: "tempuser1",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass2",
       label: "testuser2",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "testpass3",
       label: "testuser3",
-      style: "login",
+      style: "loginWithOrigin",
     }, {
       value: "zzzpass4",
       label: "zzzuser4",
-      style: "login",
+      style: "loginWithOrigin",
     }],
   },
   {
     insecureFieldWarningEnabled: false,
     insecureAutoFillFormsEnabled: false,
     isSecure: false,
     isPasswordField: true,
     matchingLogins,
     items: [],
   },
 ];
 
 add_task(async function test_all_patterns() {
   LoginHelper.createLogger("LoginAutoCompleteResult");
+  Services.prefs.setBoolPref("signon.showAutoCompleteOrigins", true);
+
   expectedResults.forEach(pattern => {
     Services.prefs.setBoolPref(PREF_INSECURE_FIELD_WARNING_ENABLED,
                                pattern.insecureFieldWarningEnabled);
     Services.prefs.setBoolPref(PREF_INSECURE_AUTOFILLFORMS_ENABLED,
                                pattern.insecureAutoFillFormsEnabled);
     let actual = new LoginAutoCompleteResult("", pattern.matchingLogins, {
       isSecure: pattern.isSecure,
       isPasswordField: pattern.isPasswordField,
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -31,18 +31,19 @@ var AutoCompleteResultView = {
     return this.results[index].value;
   },
 
   getFinalCompleteValueAt(index) {
     return this.results[index].value;
   },
 
   getLabelAt(index) {
-    // Unused by richlist autocomplete - see getCommentAt.
-    return "";
+    // Backwardly-used by richlist autocomplete - see getCommentAt.
+    // The label is used for secondary information.
+    return this.results[index].comment;
   },
 
   getCommentAt(index) {
     // The richlist autocomplete popup uses comment for its main
     // display of an item, which is why we're returning the label
     // here instead.
     return this.results[index].label;
   },
--- a/toolkit/content/widgets/autocomplete-popup.js
+++ b/toolkit/content/widgets/autocomplete-popup.js
@@ -374,16 +374,17 @@ MozElements.MozAutocompleteRichlistboxPo
         // _adjustAcItem() are unreusable.
         const UNREUSEABLE_STYLES = [
           "autofill-profile",
           "autofill-footer",
           "autofill-clear-button",
           "autofill-insecureWarning",
           "insecureWarning",
           "loginsFooter",
+          "loginWithOrigin",
         ];
         // Reuse the item when its style is exactly equal to the previous style or
         // neither of their style are in the UNREUSEABLE_STYLES.
         reusable = originalType === style ||
           !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
       }
 
       // If no reusable item available, then create a new item.
@@ -403,16 +404,19 @@ MozElements.MozAutocompleteRichlistboxPo
             options = { is: "autocomplete-creditcard-insecure-field" };
             break;
           case "insecureWarning":
             options = { is: "autocomplete-richlistitem-insecure-warning" };
             break;
           case "loginsFooter":
             options = { is: "autocomplete-richlistitem-logins-footer" };
             break;
+          case "loginWithOrigin":
+            options = { is: "autocomplete-richlistitem-login-with-origin" };
+            break;
           default:
             options = { is: "autocomplete-richlistitem" };
         }
         item = document.createXULElement("richlistitem", options);
         item.className = "autocomplete-richlistitem";
       }
 
       item.setAttribute("dir", this.style.direction);
--- a/toolkit/content/widgets/autocomplete-richlistitem.js
+++ b/toolkit/content/widgets/autocomplete-richlistitem.js
@@ -998,20 +998,82 @@ class MozAutocompleteRichlistitemLoginsF
     return JSON.parse(this.getAttribute("ac-value"));
   }
 
   _adjustAcItem() {
     this._titleText.textContent = this._data.label;
   }
 }
 
+class MozAutocompleteRichlistitemLoginWithOrigin extends MozElements.MozRichlistitem {
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    this.textContent = "";
+    this.appendChild(MozXULElement.parseXULToFragment(this._markup));
+    this.initializeAttributeInheritance();
+    this._adjustAcItem();
+  }
+
+
+  static get inheritedAttributes() {
+    return {
+      ".login-username": "text=ac-value",
+    };
+  }
+
+  get _markup() {
+    return `
+      <div xmlns="http://www.w3.org/1999/xhtml"
+           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+           class="login-wrapper">
+        <xul:image class="ac-site-icon"></xul:image>
+        <div class="login-text">
+          <div class="login-row login-username"></div>
+          <div class="login-row login-origin"></div>
+        </div>
+      </div>
+    `;
+  }
+
+  _adjustAcItem() {
+    let outerBoxRect = this.parentNode.getBoundingClientRect();
+
+    // Make item fit in popup as XUL box could not constrain
+    // item's width
+    this.firstElementChild.style.width = outerBoxRect.width + "px";
+
+    let data = JSON.parse(this.getAttribute("ac-label"));
+    let originElement = this.querySelector(".login-origin");
+    try {
+      let uri = Services.io.newURI(data.loginOrigin);
+      // Fallback to handle file: URIs
+      originElement.textContent = uri.displayHostPort || data.loginOrigin;
+    } catch (ex) {
+      originElement.textContent = data.loginOrigin;
+    }
+  }
+
+  _onOverflow() {}
+
+  _onUnderflow() {}
+
+  handleOverUnderflow() {}
+}
+
 customElements.define("autocomplete-richlistitem", MozElements.MozAutocompleteRichlistitem, {
   extends: "richlistitem",
 });
 
 customElements.define("autocomplete-richlistitem-insecure-warning", MozAutocompleteRichlistitemInsecureWarning, {
   extends: "richlistitem",
 });
 
 customElements.define("autocomplete-richlistitem-logins-footer", MozAutocompleteRichlistitemLoginsFooter, {
   extends: "richlistitem",
 });
+
+customElements.define("autocomplete-richlistitem-login-with-origin", MozAutocompleteRichlistitemLoginWithOrigin, {
+  extends: "richlistitem",
+});
 }
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1012,16 +1012,17 @@
               // _adjustAcItem() are unreusable.
               const UNREUSEABLE_STYLES = [
                 "autofill-profile",
                 "autofill-footer",
                 "autofill-clear-button",
                 "autofill-insecureWarning",
                 "insecureWarning",
                 "loginsFooter",
+                "loginWithOrigin",
               ];
               // Reuse the item when its style is exactly equal to the previous style or
               // neither of their style are in the UNREUSEABLE_STYLES.
               reusable = originalType === style ||
                 !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
             }
 
             // If no reusable item available, then create a new item.
@@ -1041,16 +1042,19 @@
                   options = { is: "autocomplete-creditcard-insecure-field" };
                   break;
                 case "insecureWarning":
                   options = { is: "autocomplete-richlistitem-insecure-warning" };
                   break;
                 case "loginsFooter":
                   options = { is: "autocomplete-richlistitem-logins-footer" };
                   break;
+                case "loginWithOrigin":
+                  options = { is: "autocomplete-richlistitem-login-with-origin" };
+                  break;
                 default:
                   options = { is: "autocomplete-richlistitem" };
               }
               item = document.createXULElement("richlistitem", options);
               item.className = "autocomplete-richlistitem";
             }
 
             item.setAttribute("dir", this.style.direction);