Bug 1571444 - Show a message in the sidebar when the search returns 0 results. r=MattN,fluent-reviewers,flod
authorJared Wein <jwein@mozilla.com>
Thu, 29 Aug 2019 00:02:10 +0000
changeset 490492 7004b8779a36084c898634e5e31d8f406815d68a
parent 490491 e61f7df8ebf205e6e061e66045514154d7c3d87c
child 490493 d07fa3716e8ea4ea083ddeb1c74bbb05ff4112bd
child 490553 d08cf6ecb122743ab3eb567c52f37df956721f92
push id36504
push userccoroiu@mozilla.com
push dateThu, 29 Aug 2019 04:08:39 +0000
treeherdermozilla-central@7004b8779a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, fluent-reviewers, flod
bugs1571444
milestone70.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 1571444 - Show a message in the sidebar when the search returns 0 results. r=MattN,fluent-reviewers,flod Differential Revision: https://phabricator.services.mozilla.com/D43312
browser/components/aboutlogins/content/aboutLogins.html
browser/components/aboutlogins/content/components/login-list.css
browser/components/aboutlogins/content/components/login-list.js
browser/components/aboutlogins/tests/chrome/test_login_list.html
browser/locales/en-US/browser/aboutLogins.ftl
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -93,16 +93,20 @@
       </div>
       <!-- This container is to work around bug 1569292 -->
       <div class="container">
         <ol role="listbox" tabindex="0" data-l10n-id="login-list"></ol>
         <div class="intro">
           <p data-l10n-id="login-list-intro-title"></p>
           <span data-l10n-id="login-list-intro-description"></span>
         </div>
+        <div class="empty-search-message">
+          <p data-l10n-id="about-logins-login-list-empty-search-title"></p>
+          <span data-l10n-id="about-logins-login-list-empty-search-description"></span>
+        </div>
       </div>
       <button class="create-login-button" data-l10n-id="create-login-button"></button>
     </template>
 
     <template id="login-list-item-template">
       <li class="login-list-item" role="option">
         <div class="favicon-wrapper">
           <img class="favicon" src="" alt=""/>
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -40,31 +40,37 @@
   text-align: end;
   margin-inline-start: 18px;
 }
 
 .container {
   display: contents;
 }
 
+:host(.no-logins) .empty-search-message,
+:host(:not(.empty-search)) .empty-search-message,
+:host(.empty-search:not(.create-login-selected)) ol,
 :host(.no-logins:not(.create-login-selected)) ol,
 :host(:not(.no-logins)) .intro,
 :host(.create-login-selected) .intro,
+:host(.create-login-selected) .empty-search-message,
 :host(:not(.create-login-selected)) #new-login-list-item {
   display: none;
 }
 
+.empty-search-message,
 .intro {
   text-align: center;
   padding: 1em;
   max-width: 50ch; /* This should be kept in sync with login-list-item username and title max-width */
   flex-grow: 1;
   border-bottom: 1px solid var(--in-content-box-border-color);
 }
 
+.empty-search-message span,
 .intro span {
   font-size: 0.85em;
 }
 
 ol {
   margin-top: 0;
   margin-bottom: 0;
   padding-inline-start: 0;
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -66,16 +66,17 @@ export default class LoginList extends H
     this._list.addEventListener("click", this);
     this.addEventListener("keydown", this);
     this._createLoginButton.addEventListener("click", this);
   }
 
   async render() {
     let visibleLoginGuids = this._applyFilter();
     this._updateVisibleLoginCount(visibleLoginGuids.size);
+    this.classList.toggle("empty-search", visibleLoginGuids.size == 0);
 
     // Add all of the logins that are not in the DOM yet.
     let fragment = document.createDocumentFragment();
     for (let guid of this._loginGuidsSortedOrder) {
       if (this._logins[guid].listItem) {
         continue;
       }
       let login = this._logins[guid].login;
--- a/browser/components/aboutlogins/tests/chrome/test_login_list.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_list.html
@@ -96,16 +96,44 @@ add_task(async function setup() {
 
   gLoginList = document.createElement("login-list");
   displayEl.appendChild(gLoginList);
 });
 
 add_task(async function test_empty_list() {
   ok(gLoginList, "loginList exists");
   is(gLoginList.textContent, "", "Initially empty");
+  gLoginList.classList.add("no-logins");
+  let loginListBox = gLoginList.shadowRoot.querySelector("ol");
+  let introText = gLoginList.shadowRoot.querySelector(".intro");
+  let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
+  ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
+  ok(!isHidden(introText), "The intro text should be visible when the list is empty");
+  ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins");
+
+  gLoginList.classList.add("create-login-selected");
+  ok(!isHidden(loginListBox), "The login-list ol should be visible when the create-login mode is active");
+  ok(isHidden(introText), "The intro text should be hidden when the create-login mode is active");
+  ok(isHidden(emptySearchText), "The empty-search text should be hidden when the create-login mode is active");
+  gLoginList.classList.remove("create-login-selected");
+
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    detail: "foo",
+  }));
+  ok(isHidden(loginListBox), "The login-list ol should be hidden when there are no logins");
+  ok(!isHidden(introText), "The intro text should be visible when the list is empty");
+  ok(isHidden(emptySearchText), "The empty-search text should be hidden when there are no logins even if a filter is applied");
+
+  // Clean up state for next test
+  gLoginList.classList.remove("no-logins");
+  window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
+    bubbles: true,
+    detail: "",
+  }));
 });
 
 add_task(async function test_keyboard_navigation() {
   gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, TEST_LOGIN_3]);
 
   while (document.activeElement != gLoginList &&
          gLoginList.shadowRoot.querySelector("#login-sort") != gLoginList.shadowRoot.activeElement) {
     sendKey("TAB");
@@ -185,56 +213,63 @@ add_task(async function test_populated_l
 add_task(async function test_breach_indicator() {
   gLoginList.updateBreaches(TEST_BREACHES_MAP);
   let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item):not([hidden])");
   ok(loginListItems[0].classList.contains("breached"), "The first login should have the .breached class.");
   ok(!loginListItems[1].classList.contains("breached"), "The second login should not have the .breached class");
 });
 
 add_task(async function test_filtered_list() {
+  let emptySearchText = gLoginList.shadowRoot.querySelector(".empty-search-message");
+  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
   is(gLoginList.shadowRoot.querySelectorAll(".login-list-item:not(#new-login-list-item):not([hidden])").length, 2, "Both logins should be visible");
   let countSpan = gLoginList.shadowRoot.querySelector(".count");
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user1",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
+  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
   let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   is(loginListItems[0].querySelector(".username").textContent, "user1", "user1 is expected first");
   ok(!loginListItems[0].hidden, "user1 should remain visible");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user2",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
+  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
   loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match result amount");
+  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
   loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "foo",
   }));
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 0, "Count should match result amount");
+  ok(!isHidden(emptySearchText), "The empty search text should be visible when there are no results in the list");
   loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "",
   }));
+  ok(isHidden(emptySearchText), "The empty search text should be hidden when there are results in the list");
   is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should be reset to full list length");
   loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item[data-guid]");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
 
   info("Add an HTTP Auth login");
   gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, TEST_HTTP_AUTH_LOGIN_1]);
   await asyncElementRendered();
--- a/browser/locales/en-US/browser/aboutLogins.ftl
+++ b/browser/locales/en-US/browser/aboutLogins.ftl
@@ -56,16 +56,18 @@ login-list-count =
   }
 login-list-sort-label-text = Sort by:
 login-list-name-option = Name (A-Z)
 login-list-breached-option = Breached Websites
 login-list-last-changed-option = Last Modified
 login-list-last-used-option = Last Used
 login-list-intro-title = No logins found
 login-list-intro-description = When you save a password in { -brand-product-name }, it will show up here.
+about-logins-login-list-empty-search-title = No logins found
+about-logins-login-list-empty-search-description = There are no results matching your search.
 login-list-item-title-new-login = New Login
 login-list-item-subtitle-new-login = Enter your login credentials
 login-list-item-subtitle-missing-username = (no username)
 
 ## Introduction screen
 
 login-intro-heading = Looking for your saved logins? Set up { -sync-brand-short-name }.
 login-intro-description = If you saved your logins to { -brand-product-name } on a different device, here’s how to get them here: