Bug 1563769 - Create login-list-item as a DOM fragment instead of a custom element to reduce overhead. r=MattN

Each custom element had its own shadowRoot, duplicated instances of the style sheet, and localization root. This patch also moves to a single 'click' event listener on the list instead of one for each item.

Differential Revision: https://phabricator.services.mozilla.com/D37143

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2019-07-08 17:39:54 +00:00
Родитель 423635cba5
Коммит 091478789a
12 изменённых файлов: 134 добавлений и 169 удалений

Просмотреть файл

@ -73,11 +73,10 @@
</template>
<template id="login-list-item-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list-item.css">
<li class="login-list-item">
<span class="title"></span>
<span class="username"></span>
</li>
</template>
<template id="login-item-template">

Просмотреть файл

@ -1,42 +0,0 @@
/* 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/. */
:host {
display: block;
padding: 10px;
padding-inline-end: 18px;
padding-inline-start: 14px;
border-inline-start: 4px solid transparent;
border-bottom: 1px solid var(--in-content-box-border-color);
}
:host(:hover) {
background-color: var(--in-content-box-background-hover);
}
:host(:hover:active) {
background-color: var(--in-content-box-background-active);
}
:host(.keyboard-selected) {
border-inline-start-color: var(--in-content-border-active-shadow);
background-color: var(--in-content-box-background-odd);
}
:host(.selected) {
border-inline-start-color: var(--in-content-border-highlight);
background-color: var(--in-content-box-background-hover);
}
.title {
font-weight: bold;
}
.title,
.username {
display: block;
max-width: 50ch;
text-overflow: ellipsis;
overflow: hidden;
}

Просмотреть файл

@ -2,96 +2,49 @@
* 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/. */
import { recordTelemetryEvent } from "../aboutLoginsUtils.js";
export default class LoginListItem extends HTMLElement {
constructor(login) {
super();
this._login = login;
this.id = login.guid
? // Prepend the ID with a string since IDs must not begin with a number.
"lli-" + this._login.guid
: "new-login-list-item";
}
connectedCallback() {
if (this.shadowRoot) {
this.render();
return;
}
/**
* LoginListItemFactory is used instead of the "login-list-item" custom element
* since there may be 100s of login-list-items on about:logins and each
* custom element carries with it significant overhead when used in large
* numbers.
*/
export default class LoginListItemFactory {
static create(login) {
let loginListItemTemplate = document.querySelector(
"#login-list-item-template"
);
let shadowRoot = this.attachShadow({ mode: "open" });
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(loginListItemTemplate.content.cloneNode(true));
let loginListItem = loginListItemTemplate.content.cloneNode(true);
let listItem = loginListItem.querySelector("li");
let title = loginListItem.querySelector(".title");
let username = loginListItem.querySelector(".username");
this._title = this.shadowRoot.querySelector(".title");
this._username = this.shadowRoot.querySelector(".username");
this.setAttribute("role", "option");
listItem.setAttribute("role", "option");
this.addEventListener("click", this);
this.render();
}
render() {
if (!this._login.guid) {
delete this.dataset.guid;
if (!login.guid) {
listItem.id = "new-login-list-item";
document.l10n.setAttributes(title, "login-list-item-title-new-login");
document.l10n.setAttributes(
this._title,
"login-list-item-title-new-login"
);
document.l10n.setAttributes(
this._username,
username,
"login-list-item-subtitle-new-login"
);
return;
return listItem;
}
this.dataset.guid = this._login.guid;
this._title.textContent = this._login.title;
if (this._login.username.trim()) {
this._username.removeAttribute("data-l10n-id");
this._username.textContent = this._login.username.trim();
// Prepend the ID with a string since IDs must not begin with a number.
listItem.id = "lli-" + login.guid;
listItem.dataset.guid = login.guid;
listItem._login = login;
title.textContent = login.title;
if (login.username.trim()) {
username.removeAttribute("data-l10n-id");
username.textContent = login.username.trim();
} else {
document.l10n.setAttributes(
this._username,
username,
"login-list-item-subtitle-missing-username"
);
}
}
handleEvent(event) {
switch (event.type) {
case "click": {
if (!this._login.guid) {
return;
}
this.dispatchEvent(
new CustomEvent("AboutLoginsLoginSelected", {
bubbles: true,
composed: true,
detail: this._login,
})
);
recordTelemetryEvent({ object: "existing_login", method: "select" });
}
}
}
/**
* Updates the cached login object with new values.
*
* @param {login} login The login object to display. The login object is
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
*/
update(login) {
this._login = login;
this.render();
return listItem;
}
}
customElements.define("login-list-item", LoginListItem);

Просмотреть файл

@ -37,3 +37,42 @@ ol {
.create-login-button {
margin: 18px;
}
.login-list-item {
display: block;
padding: 10px;
padding-inline-end: 18px;
padding-inline-start: 14px;
border-inline-start: 4px solid transparent;
border-bottom: 1px solid var(--in-content-box-border-color);
}
.login-list-item:hover {
background-color: var(--in-content-box-background-hover);
}
.login-list-item:hover:active {
background-color: var(--in-content-box-background-active);
}
.login-list-item.keyboard-selected {
border-inline-start-color: var(--in-content-border-active-shadow);
background-color: var(--in-content-box-background-odd);
}
.login-list-item.selected {
border-inline-start-color: var(--in-content-border-highlight);
background-color: var(--in-content-box-background-hover);
}
.title {
font-weight: bold;
}
.title,
.username {
display: block;
max-width: 50ch;
text-overflow: ellipsis;
overflow: hidden;
}

Просмотреть файл

@ -2,7 +2,7 @@
* 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/. */
import LoginListItem from "./login-list-item.js";
import LoginListItemFactory from "./login-list-item.js";
import { recordTelemetryEvent } from "../aboutLoginsUtils.js";
const collator = new Intl.Collator();
@ -18,7 +18,7 @@ export default class LoginList extends HTMLElement {
this._logins = [];
this._filter = "";
this._selectedGuid = null;
this._blankLoginListItem = new LoginListItem({});
this._blankLoginListItem = LoginListItemFactory.create({});
}
connectedCallback() {
@ -44,6 +44,7 @@ export default class LoginList extends HTMLElement {
window.addEventListener("AboutLoginsCreateLogin", this);
window.addEventListener("AboutLoginsLoginSelected", this);
window.addEventListener("AboutLoginsFilterLogins", this);
this._list.addEventListener("click", this);
this.addEventListener("keydown", this);
this._createLoginButton.addEventListener("click", this);
}
@ -85,7 +86,7 @@ export default class LoginList extends HTMLElement {
let chunkSize = 5;
for (let i = 0; i < this._logins.length; i++) {
let login = this._logins[i];
let listItem = new LoginListItem(login);
let listItem = LoginListItemFactory.create(login);
if (login.guid == this._selectedGuid) {
this._setListItemAsSelected(listItem);
}
@ -109,17 +110,35 @@ export default class LoginList extends HTMLElement {
handleEvent(event) {
switch (event.type) {
case "click": {
if (event.originalTarget == this._createLoginButton) {
window.dispatchEvent(new CustomEvent("AboutLoginsCreateLogin"));
recordTelemetryEvent({ object: "new_login", method: "new" });
return;
}
let loginListItem = event.originalTarget.closest(".login-list-item");
if (!loginListItem || !loginListItem.dataset.guid) {
return;
}
this.dispatchEvent(
new CustomEvent("AboutLoginsLoginSelected", {
bubbles: true,
composed: true,
detail: loginListItem._login,
})
);
recordTelemetryEvent({ object: "existing_login", method: "select" });
break;
}
case "change": {
const sort = this._sortSelect.value;
this._logins = this._logins.sort((a, b) => sortFnOptions[sort](a, b));
this.render();
break;
}
case "click": {
window.dispatchEvent(new CustomEvent("AboutLoginsCreateLogin"));
recordTelemetryEvent({ object: "new_login", method: "new" });
break;
}
case "AboutLoginsClearSelection": {
if (!this._logins.length) {
return;
@ -147,7 +166,7 @@ export default class LoginList extends HTMLElement {
}
let listItem = this._list.querySelector(
`login-list-item[data-guid="${event.detail.guid}"]`
`.login-list-item[data-guid="${event.detail.guid}"]`
);
if (listItem) {
this._setListItemAsSelected(listItem);
@ -179,7 +198,7 @@ export default class LoginList extends HTMLElement {
) {
// Select the first visible login after any possible filter is applied.
let firstVisibleListItem = this._list.querySelector(
"login-list-item[data-guid]:not([hidden])"
".login-list-item[data-guid]:not([hidden])"
);
if (firstVisibleListItem) {
this._selectedGuid = firstVisibleListItem.dataset.guid;

Просмотреть файл

@ -11,7 +11,6 @@ browser.jar:
content/browser/aboutlogins/components/login-item.js (content/components/login-item.js)
content/browser/aboutlogins/components/login-list.css (content/components/login-list.css)
content/browser/aboutlogins/components/login-list.js (content/components/login-list.js)
content/browser/aboutlogins/components/login-list-item.css (content/components/login-list-item.css)
content/browser/aboutlogins/components/login-list-item.js (content/components/login-list-item.js)
content/browser/aboutlogins/components/menu-button.css (content/components/menu-button.css)
content/browser/aboutlogins/components/menu-button.js (content/components/menu-button.js)

Просмотреть файл

@ -51,7 +51,7 @@ add_task(async function test_telemetry_events() {
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
let loginList = content.document.querySelector("login-list");
let loginListItem = loginList.shadowRoot.querySelector(
"login-list-item:nth-child(2)"
".login-list-item:nth-child(2)"
);
loginListItem.click();
});
@ -128,7 +128,7 @@ add_task(async function test_telemetry_events() {
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
let loginList = content.document.querySelector("login-list");
let loginListItem = loginList.shadowRoot.querySelector(
"login-list-item[data-guid]"
".login-list-item[data-guid]"
);
loginListItem.click();
});

Просмотреть файл

@ -86,7 +86,7 @@ add_task(async function test_create_login() {
ok(loginFound, "Expected number of logins found in login-list");
let loginListItem = [
...loginList.shadowRoot.querySelectorAll("login-list-item"),
...loginList.shadowRoot.querySelectorAll(".login-list-item"),
].find(l => l._login.origin == aOriginTuple[1]);
ok(
!!loginListItem,

Просмотреть файл

@ -45,7 +45,7 @@ add_task(async function test_login_item() {
await ContentTask.spawn(browser, [TEST_LOGIN1, TEST_LOGIN2], async logins => {
let loginList = content.document.querySelector("login-list");
let loginListItem = loginList.shadowRoot.querySelector(
"login-list-item[data-guid]"
".login-list-item[data-guid]"
);
info("Clicking on the first login");
loginListItem.click();

Просмотреть файл

@ -71,10 +71,10 @@ add_task(async function test_query_parameter_filter() {
);
let hiddenLoginListItems = loginList.shadowRoot.querySelectorAll(
"login-list-item[hidden]"
".login-list-item[hidden]"
);
let visibleLoginListItems = loginList.shadowRoot.querySelectorAll(
"login-list-item:not([hidden])"
".login-list-item:not([hidden])"
);
is(visibleLoginListItems.length, 1, "The one login should be visible");
is(

Просмотреть файл

@ -34,7 +34,7 @@ add_task(async function test_login_item() {
async login => {
let loginList = content.document.querySelector("login-list");
let loginListItem = Cu.waiveXrays(
loginList.shadowRoot.querySelector("login-list-item[data-guid]")
loginList.shadowRoot.querySelector(".login-list-item[data-guid]")
);
loginListItem.click();
@ -103,7 +103,7 @@ add_task(async function test_login_item() {
);
await ContentTaskUtils.waitForCondition(() => {
loginListItem = Cu.waiveXrays(
loginList.shadowRoot.querySelector("login-list-item")
loginList.shadowRoot.querySelector(".login-list-item")
);
return (
loginListItem._login.username == usernameInput.value &&
@ -132,7 +132,7 @@ add_task(async function test_login_item() {
await ContentTaskUtils.waitForCondition(() => {
loginListItem = Cu.waiveXrays(
loginList.shadowRoot.querySelector("login-list-item")
loginList.shadowRoot.querySelector(".login-list-item")
);
return !loginListItem;
}, "Waiting for login to be removed from list");

Просмотреть файл

@ -124,35 +124,33 @@ add_task(async function test_empty_login_username_in_list() {
}));
gLoginList.setLogins([TEST_LOGIN_3]);
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 1, "The one stored login should be displayed");
is(loginListItems[0].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
loginListItems[0].render();
let loginUsername = loginListItems[0].shadowRoot.querySelector(".username");
let loginUsername = loginListItems[0].querySelector(".username");
is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
});
add_task(async function test_populated_list() {
gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 2, "The two stored logins should be displayed");
is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
is(loginListItems[0].shadowRoot.querySelector(".title").textContent, TEST_LOGIN_1.title,
is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
"login-list-item origin should match");
is(loginListItems[0].shadowRoot.querySelector(".username").textContent, TEST_LOGIN_1.username,
is(loginListItems[0].querySelector(".username").textContent, TEST_LOGIN_1.username,
"login-list-item username should match");
ok(loginListItems[0].classList.contains("selected"), "The first item should be selected by default");
ok(!loginListItems[1].classList.contains("selected"), "The second item should not be selected by default");
loginListItems[0].click();
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 2, "After selecting one, only the two stored logins should be displayed");
ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
});
add_task(async function test_filtered_list() {
is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not([hidden])").length, 2, "Both logins should be visible");
is(gLoginList.shadowRoot.querySelectorAll(".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", {
@ -160,8 +158,8 @@ add_task(async function test_filtered_list() {
detail: "user1",
}));
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
is(loginListItems[0].shadowRoot.querySelector(".username").textContent, "user1", "user1 is expected first");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
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", {
@ -169,7 +167,7 @@ add_task(async function test_filtered_list() {
detail: "user2",
}));
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
ok(loginListItems[0].hidden, "user1 should be hidden");
ok(!loginListItems[1].hidden, "user2 should be visible");
window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
@ -177,7 +175,7 @@ add_task(async function test_filtered_list() {
detail: "user",
}));
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
ok(!loginListItems[0].hidden, "user1 should be visible");
ok(!loginListItems[1].hidden, "user2 should be visible");
window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
@ -185,7 +183,7 @@ add_task(async function test_filtered_list() {
detail: "foo",
}));
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 0, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
ok(loginListItems[0].hidden, "user1 should be hidden");
ok(loginListItems[1].hidden, "user2 should be hidden");
window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
@ -193,7 +191,7 @@ add_task(async function test_filtered_list() {
detail: "",
}));
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");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
ok(!loginListItems[0].hidden, "user1 should be visible");
ok(!loginListItems[1].hidden, "user2 should be visible");
});
@ -202,14 +200,14 @@ add_task(async function test_login_modified() {
let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
gLoginList.loginModified(modifiedLogin);
await asyncElementRendered();
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 2, "Both logins should be displayed");
is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
is(loginListItems[0].shadowRoot.querySelector(".title").textContent, TEST_LOGIN_1.title,
is(loginListItems[0].querySelector(".title").textContent, TEST_LOGIN_1.title,
"login-list-item origin should match");
is(loginListItems[0].shadowRoot.querySelector(".username").textContent, modifiedLogin.username,
is(loginListItems[0].querySelector(".username").textContent, modifiedLogin.username,
"login-list-item username should have been updated");
is(loginListItems[1].shadowRoot.querySelector(".username").textContent, TEST_LOGIN_2.username,
is(loginListItems[1].querySelector(".username").textContent, TEST_LOGIN_2.username,
"login-list-item2 username should remain unchanged");
});
@ -217,21 +215,21 @@ add_task(async function test_login_added() {
let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
gLoginList.loginAdded(newLogin);
await asyncElementRendered();
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 3, "New login should be added to the list");
is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
is(loginListItems[2].shadowRoot.querySelector(".title").textContent, newLogin.title,
is(loginListItems[2].querySelector(".title").textContent, newLogin.title,
"login-list-item origin should match");
is(loginListItems[2].shadowRoot.querySelector(".username").textContent, newLogin.username,
is(loginListItems[2].querySelector(".username").textContent, newLogin.username,
"login-list-item username should have been updated");
});
add_task(async function test_login_removed() {
gLoginList.loginRemoved({guid: "111222"});
await asyncElementRendered();
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 2, "New login should be removed from the list");
is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
@ -250,7 +248,7 @@ add_task(async function test_login_added_filtered() {
let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
gLoginList.loginAdded(newLogin);
await asyncElementRendered();
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 3, "New login should be added to the list");
is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
@ -269,7 +267,7 @@ add_task(async function test_sorted_list() {
// sort by last used
gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 1;
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
let loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
is(loginListItems.length, 3, "The list should contain the three stored logins");
let timeUsed = loginListItems[0]._login.timeLastUsed;
let timeUsed2 = loginListItems[1]._login.timeLastUsed;
@ -277,14 +275,14 @@ add_task(async function test_sorted_list() {
// sort by name
gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 0;
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
let title = loginListItems[0]._login.title;
let title2 = loginListItems[1]._login.title;
is(title.localeCompare(title2), -1, "Logins should be sorted alphabetically by hostname");
// sort by last changed
gLoginList.shadowRoot.getElementById("login-sort").selectedIndex = 2;
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
loginListItems = gLoginList.shadowRoot.querySelectorAll(".login-list-item");
let pwChanged = loginListItems[0]._login.timePasswordChanged;
let pwChanged2 = loginListItems[1]._login.timePasswordChanged;
is(pwChanged2 > pwChanged, true, "Login with most recently changed password should be displayed at top of list");