Bug 1770852 - Re-implement placeholders and fix tests for the tree list view. r=darktrojan

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

--HG--
extra : amend_source : 7254cbb44be671b8b3ffa32800e2435bd39d96cd
This commit is contained in:
Alessandro Castellani 2022-12-01 21:33:31 +11:00
Родитель 0d5f4bd539
Коммит 3401ee7dca
15 изменённых файлов: 499 добавлений и 344 удалений

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

@ -21,6 +21,10 @@
}
this.hasConnected = true;
// Prevent this element from being part of the roving tab focus since we
// handle that independently for the TreeViewListbox and we don't want any
// interference from this.
this.tabIndex = -1;
this.classList.add("tree-view-scrollable-container");
this.table = document.createElement("table", { is: "tree-view-table" });
@ -606,6 +610,9 @@
this.setAttribute("aria-multiselectable", "true");
this.scrollable = this.closest(".tree-view-scrollable-container");
this.placeholder = this.scrollable.querySelector(
`slot[name="placeholders"]`
);
this.addEventListener("focus", event => {
if (this._preventFocusHandler) {
@ -972,7 +979,10 @@
* here is important.
*/
_ensureVisibleRowsAreDisplayed() {
if (!this.view || this.view.rowCount == 0) {
let hasRows = !this.view || this.view.rowCount == 0;
this.placeholder?.classList.toggle("show", hasRows);
if (hasRows) {
return;
}
@ -1064,7 +1074,12 @@
return;
}
const bottomIndex = topIndex + this._rowElementClass.ROW_HEIGHT * 3;
// Account for the table header height in a sticky position above the
// listbox. If the list is not in a table layout, the thead height is 0.
const bottomIndex =
topIndex +
this._rowElementClass.ROW_HEIGHT +
this.closest("table").header.clientHeight;
if (bottomIndex > scrollTop + clientHeight) {
this.scrollable.scrollTo(0, bottomIndex - clientHeight);
}
@ -1367,6 +1382,17 @@
return selected;
}
/**
* Loop through all available child elements of the placeholder slot and
* show those that are needed.
* @param {array} idsToShow - Array of ids to show.
*/
updatePlaceholders(idsToShow) {
for (let element of this.placeholder.children) {
element.hidden = !idsToShow.includes(element.id);
}
}
}
customElements.define("tree-view-listbox", TreeViewListbox, {
extends: "tbody",

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

@ -27,12 +27,12 @@ add_task(async function testKeyboardAndMouse() {
async function subtestKeyboardAndMouse() {
let doc = content.document;
let list = doc.querySelector("tree-view-listbox");
let list = doc.querySelector(`[is="tree-view-listbox"]`);
Assert.ok(!!list, "the list exists");
let listRect = list.getBoundingClientRect();
let listRect = list.scrollable.getBoundingClientRect();
let rows = list.querySelectorAll("test-listrow");
let rows = list.querySelectorAll(`tr[is="test-listrow"]`);
// Count is calculated from the height of `list` divided by
// TestCardRow.ROW_HEIGHT, plus TreeViewListbox.OVERFLOW_BUFFER.
Assert.equal(rows.length, 23, "the list has the right number of rows");
@ -85,6 +85,23 @@ async function subtestKeyboardAndMouse() {
},
};
/**
* Check if the spacerTop TBODY of the TreeViewTable is properly allocating
* the height of non existing rows.
*
* @param {int} rows - The number of rows that the spacerTop should be
* simulating their height allocation.
*/
function checkTopSpacerHeight(rows) {
let table = doc.querySelector(`[is="tree-view-table"]`);
// -10 to account for the OVERFLOW_BUFFER.
Assert.equal(
table.spacerTop.clientHeight,
list.getRowAtIndex(rows).clientHeight * (rows - 10),
"The top spacer has the correct height"
);
}
function checkCurrent(expectedIndex) {
Assert.equal(list.currentIndex, expectedIndex, "currentIndex is correct");
if (selectHandler.currentAtEvent !== null) {
@ -357,6 +374,7 @@ async function subtestKeyboardAndMouse() {
38,
"scrolled to the correct place"
);
checkTopSpacerHeight(37);
// Does nothing.
await pressKey("VK_DOWN", {}, false);
@ -367,6 +385,7 @@ async function subtestKeyboardAndMouse() {
38,
"scrolled to the correct place"
);
checkTopSpacerHeight(37);
await pressKey("VK_PAGE_UP");
await scrollingDelay();
@ -387,6 +406,7 @@ async function subtestKeyboardAndMouse() {
38,
"scrolled to the correct place"
);
checkTopSpacerHeight(37);
await pressKey("VK_HOME");
await scrollingDelay();
@ -398,7 +418,7 @@ async function subtestKeyboardAndMouse() {
// even if the row element itself disappears.
selectHandler.reset();
list.scrollTo(0, 125);
list.scrollable.scrollTo(0, 125);
await scrollingDelay();
checkCurrent(0);
checkSelected(0);
@ -408,7 +428,7 @@ async function subtestKeyboardAndMouse() {
"getFirstVisibleIndex is correct"
);
list.scrollTo(0, 1025);
list.scrollable.scrollTo(0, 1025);
await scrollingDelay();
Assert.equal(list.currentIndex, 0, "currentIndex is still set");
Assert.ok(
@ -429,6 +449,7 @@ async function subtestKeyboardAndMouse() {
!selectHandler.seenEvent,
"'select' event did not fire as expected"
);
checkTopSpacerHeight(20);
await pressKey("VK_DOWN");
await scrollingDelay();
@ -437,7 +458,7 @@ async function subtestKeyboardAndMouse() {
Assert.equal(list.getFirstVisibleIndex(), 1, "scrolled to the correct place");
selectHandler.reset();
list.scrollTo(0, 0);
list.scrollable.scrollTo(0, 0);
await scrollingDelay();
checkCurrent(1);
checkSelected(1);
@ -459,30 +480,58 @@ async function subtestKeyboardAndMouse() {
// Some literal edge cases. Clicking on a partially visible row should
// scroll it into view.
rows = list.querySelectorAll("test-listrow");
rows = list.querySelectorAll(`tr[is="test-listrow"]`);
let bcr = rows[12].getBoundingClientRect();
Assert.less(bcr.top, listRect.bottom, "top of row 12 is visible");
Assert.less(
Math.round(bcr.top),
Math.round(listRect.bottom),
"top of row 12 is visible"
);
Assert.greater(
bcr.bottom,
listRect.bottom,
Math.round(bcr.bottom),
Math.round(listRect.bottom),
"bottom of row 12 is not visible"
);
await clickOnRow(12);
await scrollingDelay();
rows = list.querySelectorAll("test-listrow");
rows = list.querySelectorAll(`tr[is="test-listrow"]`);
bcr = rows[12].getBoundingClientRect();
Assert.less(bcr.top, listRect.bottom, "top of row 12 is visible");
Assert.equal(bcr.bottom, listRect.bottom, "bottom of row 12 is visible");
Assert.less(
Math.round(bcr.top),
Math.round(listRect.bottom),
"top of row 12 is visible"
);
Assert.equal(
Math.round(bcr.bottom),
Math.round(listRect.bottom),
"bottom of row 12 is visible"
);
bcr = rows[0].getBoundingClientRect();
Assert.less(bcr.top, listRect.top, "top of row 0 is not visible");
Assert.greater(bcr.bottom, listRect.top, "bottom of row 0 is visible");
Assert.less(
Math.round(bcr.top),
Math.round(listRect.top),
"top of row 0 is not visible"
);
Assert.greater(
Math.round(bcr.bottom),
Math.round(listRect.top),
"bottom of row 0 is visible"
);
await clickOnRow(0);
await scrollingDelay();
rows = list.querySelectorAll("test-listrow");
rows = list.querySelectorAll(`tr[is="test-listrow"]`);
bcr = rows[0].getBoundingClientRect();
Assert.equal(bcr.top, listRect.top, "top of row 0 is visible");
Assert.greater(bcr.bottom, listRect.top, "bottom of row 0 is visible");
Assert.equal(
Math.round(bcr.top),
Math.round(listRect.top),
"top of row 0 is visible"
);
Assert.greater(
Math.round(bcr.bottom),
Math.round(listRect.top),
"bottom of row 0 is visible"
);
}
/**
@ -506,7 +555,7 @@ async function subtestRowCountChange() {
let doc = content.document;
let ROW_HEIGHT = 50;
let list = doc.querySelector("tree-view-listbox");
let list = doc.querySelector(`[is="tree-view-listbox"]`);
let view = list.view;
let rows;
@ -517,7 +566,7 @@ async function subtestRowCountChange() {
for (let i = first; i <= last; i++) {
expectedIndices.push(i);
}
rows = list.querySelectorAll("test-listrow");
rows = list.querySelectorAll(`tr[is="test-listrow"]`);
Assert.deepEqual(
Array.from(rows, r => r.index),
expectedIndices,
@ -532,7 +581,7 @@ async function subtestRowCountChange() {
function checkSelected(indices, existingIndices) {
Assert.deepEqual(list.selectedIndices, indices);
let selectedRows = list.querySelectorAll("test-listrow.selected");
let selectedRows = list.querySelectorAll(`tr[is="test-listrow"].selected`);
Assert.deepEqual(
Array.from(selectedRows, r => r.index),
existingIndices
@ -559,7 +608,7 @@ async function subtestRowCountChange() {
list.rowCountChanged(index, values.length);
Assert.equal(
list.scrollHeight,
list.scrollable.scrollHeight,
expectedCount * ROW_HEIGHT,
"space for all rows is allocated"
);
@ -581,7 +630,7 @@ async function subtestRowCountChange() {
list.rowCountChanged(index, -count);
Assert.equal(
list.scrollHeight,
list.scrollable.scrollHeight,
expectedCount * ROW_HEIGHT,
"space for all rows is allocated"
);
@ -592,9 +641,9 @@ async function subtestRowCountChange() {
expectedCount,
"the view has the right number of rows"
);
Assert.equal(list.scrollTop, 0, "the list is scrolled to the top");
Assert.equal(list.scrollable.scrollTop, 0, "the list is scrolled to the top");
Assert.equal(
list.scrollHeight,
list.scrollable.scrollHeight,
expectedCount * ROW_HEIGHT,
"space for all rows is allocated"
);
@ -664,7 +713,11 @@ async function subtestRowCountChange() {
Assert.equal(rows[22].dataset.value, "17");
checkSelected([7, 19, 32, 42, 54], [7, 19]);
Assert.equal(list.scrollTop, 0, "the list is still scrolled to the top");
Assert.equal(
list.scrollable.scrollTop,
0,
"the list is still scrolled to the top"
);
// Remove values in the order we added them.
@ -715,11 +768,15 @@ async function subtestRowCountChange() {
Assert.equal(rows[22].dataset.value, "22");
checkSelected([4, 14, 24, 34, 44], [4, 14]);
Assert.equal(list.scrollTop, 0, "the list is still scrolled to the top");
Assert.equal(
list.scrollable.scrollTop,
0,
"the list is still scrolled to the top"
);
// Now scroll to the middle and repeat.
list.scrollTo(0, 935);
list.scrollable.scrollTo(0, 935);
await new Promise(r => content.setTimeout(r, 100));
checkRows(8, 41);
Assert.equal(rows[0].dataset.value, "8");
@ -765,7 +822,11 @@ async function subtestRowCountChange() {
Assert.equal(rows[33].dataset.value, "37a");
checkSelected([5, 16, 26, 37, 48], [16, 26, 37]);
Assert.equal(list.scrollTop, 935, "the list is still scrolled to the middle");
Assert.equal(
list.scrollable.scrollTop,
935,
"the list is still scrolled to the middle"
);
removeValues(54, 1, [50]);
checkRows(8, 41);
@ -797,11 +858,15 @@ async function subtestRowCountChange() {
Assert.equal(rows[33].dataset.value, "41");
checkSelected([4, 14, 24, 34, 44], [14, 24, 34]);
Assert.equal(list.scrollTop, 935, "the list is still scrolled to the middle");
Assert.equal(
list.scrollable.scrollTop,
935,
"the list is still scrolled to the middle"
);
// Now scroll to the bottom and repeat.
list.scrollTo(0, 1870);
list.scrollable.scrollTo(0, 1870);
await new Promise(r => content.setTimeout(r, 100));
checkRows(27, 49);
Assert.equal(rows[0].dataset.value, "27");
@ -840,7 +905,7 @@ async function subtestRowCountChange() {
checkSelected([5, 15, 25, 36, 46], [36, 46]);
Assert.equal(
list.scrollTop,
list.scrollable.scrollTop,
1870,
"the list is still scrolled to the bottom"
);
@ -873,14 +938,14 @@ async function subtestRowCountChange() {
checkSelected([4, 14, 24, 34, 44], [34, 44]);
Assert.equal(
list.scrollTop,
list.scrollable.scrollTop,
1870,
"the list is still scrolled to the bottom"
);
// Remove a selected row and check the selection changes.
list.scrollTo(0, 0);
list.scrollable.scrollTo(0, 0);
await new Promise(r => content.setTimeout(r, 100));
checkSelected([4, 14, 24, 34, 44], [4, 14]);
@ -935,7 +1000,7 @@ add_task(async function testExpandCollapse() {
async function subtestExpandCollapse() {
let doc = content.document;
let list = doc.querySelector("tree-view-listbox");
let list = doc.querySelector(`[is="tree-view-listbox"]`);
let allIds = [
"row-1",
"row-2",
@ -1461,12 +1526,12 @@ add_task(async function testRowClassChange() {
async function subtestRowClassChange() {
let doc = content.document;
let list = doc.querySelector("tree-view-listbox");
let list = doc.querySelector(`[is="tree-view-listbox"]`);
let indices = (list.selectedIndices = [1, 2, 3, 5, 8, 13, 21, 34]);
list.currentIndex = 5;
for (let row of list.children) {
Assert.equal(row.localName, "test-listrow");
Assert.equal(row.getAttribute("is"), "test-listrow");
Assert.equal(row.clientHeight, 50);
Assert.equal(
row.classList.contains("selected"),
@ -1481,7 +1546,7 @@ async function subtestRowClassChange() {
Assert.equal(list.currentIndex, 5);
for (let row of list.children) {
Assert.equal(row.localName, "alternative-listrow");
Assert.equal(row.getAttribute("is"), "alternative-listrow");
Assert.equal(row.clientHeight, 80);
Assert.equal(
row.classList.contains("selected"),
@ -1500,7 +1565,7 @@ async function subtestRowClassChange() {
Assert.equal(list.currentIndex, -1);
for (let row of list.children) {
Assert.equal(row.localName, "test-listrow");
Assert.equal(row.getAttribute("is"), "test-listrow");
Assert.equal(row.clientHeight, 50);
Assert.ok(!row.classList.contains("selected"));
Assert.ok(!row.classList.contains("current"));

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

@ -1,96 +1,118 @@
class TestCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 50;
connectedCallback() {
if (this.hasConnected) {
return;
}
super.connectedCallback();
this.d1 = this.appendChild(document.createElement("div"));
this.d1.classList.add("d1");
this.d2 = this.d1.appendChild(document.createElement("div"));
this.d2.classList.add("d2");
this.d3 = this.d1.appendChild(document.createElement("div"));
this.d3.classList.add("d3");
}
get index() {
return super.index;
}
set index(index) {
super.index = index;
this.d2.textContent = this.view.getCellText(index, {
id: "GeneratedName",
});
this.d3.textContent = this.view.getCellText(index, {
id: "PrimaryEmail",
});
this.dataset.value = this.view.values[index];
}
}
customElements.define("test-listrow", TestCardRow);
class AlternativeCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 80;
connectedCallback() {
if (this.hasConnected) {
return;
}
super.connectedCallback();
}
get index() {
return super.index;
}
set index(index) {
super.index = index;
this.textContent = this.view.getCellText(index, { id: "GeneratedName" });
}
}
customElements.define("alternative-listrow", AlternativeCardRow);
class TestView {
values = [];
constructor(start, count) {
for (let i = start; i < start + count; i++) {
this.values.push(i);
}
}
get rowCount() {
return this.values.length;
}
getCellText(index, column) {
return `${column.id} ${this.values[index]}`;
}
isContainer() {
return false;
}
isContainerOpen() {
return false;
}
selectionChanged() {}
setTree() {}
}
/* 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/. */
// FIXME: Wrap the whole method around the document load listener to prevent the
// undefined state of the "tree-view-listrow" element. This is due to the .mjs
// nature of the class file.
window.addEventListener("load", () => {
let list = document.getElementById("testList");
list.addEventListener("select", event => {
console.log("select event, selected indices:", list.selectedIndices);
class TestCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 50;
connectedCallback() {
if (this.hasConnected) {
return;
}
super.connectedCallback();
this.cell = this.appendChild(document.createElement("td"));
let container = this.cell.appendChild(document.createElement("div"));
this.d1 = container.appendChild(document.createElement("div"));
this.d1.classList.add("d1");
this.d2 = this.d1.appendChild(document.createElement("div"));
this.d2.classList.add("d2");
this.d3 = this.d1.appendChild(document.createElement("div"));
this.d3.classList.add("d3");
}
get index() {
return super.index;
}
set index(index) {
super.index = index;
this.d2.textContent = this.view.getCellText(index, {
id: "GeneratedName",
});
this.d3.textContent = this.view.getCellText(index, {
id: "PrimaryEmail",
});
this.dataset.value = this.view.values[index];
}
}
customElements.define("test-listrow", TestCardRow, { extends: "tr" });
class AlternativeCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 80;
connectedCallback() {
if (this.hasConnected) {
return;
}
super.connectedCallback();
this.cell = this.appendChild(document.createElement("td"));
}
get index() {
return super.index;
}
set index(index) {
super.index = index;
this.cell.textContent = this.view.getCellText(index, {
id: "GeneratedName",
});
}
}
customElements.define("alternative-listrow", AlternativeCardRow, {
extends: "tr",
});
list.view = new TestView(0, 50);
class TestView {
values = [];
constructor(start, count) {
for (let i = start; i < start + count; i++) {
this.values.push(i);
}
}
get rowCount() {
return this.values.length;
}
getCellText(index, column) {
return `${column.id} ${this.values[index]}`;
}
isContainer() {
return false;
}
isContainerOpen() {
return false;
}
selectionChanged() {}
setTree() {}
}
let tree = document.getElementById("testTree");
let table = tree.table;
table.setListBoxID("testList");
table.listbox.setAttribute("rows", "test-listrow");
table.listbox.addEventListener("select", () => {
console.log(
"select event, selected indices:",
table.listbox.selectedIndices
);
});
table.listbox.view = new TestView(0, 50);
});

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

@ -1,3 +1,8 @@
<?xml version="1.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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
@ -5,46 +10,36 @@
<title>Test for the tree-view-listbox custom element</title>
<link rel="stylesheet" href="chrome://messenger/skin/shared/tree-listbox.css" />
<style>
#testList {
.tree-view-scrollable-container {
height: 630px;
scroll-behavior: unset;
--in-content-button-background: rgba(12, 12, 13, 0.1);
--in-content-focus-outline-color: #45a1ff;
--in-content-item-selected-text: highlighttext;
--in-content-item-selected: highlight;
}
test-listrow {
height: 36px;
padding: 7px;
tr[is="test-listrow"] td > div {
display: flex;
align-items: center;
box-sizing: border-box;
}
test-listrow div.d1 {
tr[is="test-listrow"] td div.d1 {
flex: 1;
}
test-listrow div.d1 > div.d2 {
line-height: 18px;
tr[is="test-listrow"] td div.d1 > div.d2 {
line-height: 1.2;
}
test-listrow div.d1 > div.d3 {
line-height: 18px;
font-size: 13.333px;
}
alternative-listrow {
height: 80px;
tr[is="test-listrow"] td div.d1 > div.d3 {
line-height: 1.2;
font-size: 13px;
}
</style>
<script type="module" defer="defer" src="chrome://messenger/content/tree-view-listbox.mjs"></script>
<script defer="defer" src="treeViewListbox.js"></script>
<script type="module" src="chrome://messenger/content/tree-view-listbox.mjs"></script>
<script src="treeViewListbox.js"></script>
</head>
<body>
<input id="before" placeholder="something to focus on" />
<tree-view-listbox id="testList" tabindex="0" rows="test-listrow"></tree-view-listbox>
<tree-view id="testTree"/>
<input id="after" placeholder="something to focus on" />
</body>
</html>

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

@ -1,94 +1,110 @@
/* 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/. */
/* globals PROTO_TREE_VIEW */
class TestCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 30;
// FIXME: Wrap the whole method around the document load listener to prevent the
// undefined state of the "tree-view-listrow" element. This is due to the .mjs
// nature of the class file.
window.addEventListener("load", () => {
class TestCardRow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 30;
connectedCallback() {
if (this.hasConnected) {
return;
connectedCallback() {
if (this.hasConnected) {
return;
}
super.connectedCallback();
this.cell = this.appendChild(document.createElement("td"));
let container = this.cell.appendChild(document.createElement("div"));
this.twisty = container.appendChild(document.createElement("div"));
this.twisty.classList.add("twisty");
this.d2 = container.appendChild(document.createElement("div"));
this.d2.classList.add("d2");
}
super.connectedCallback();
get index() {
return super.index;
}
this.twisty = this.appendChild(document.createElement("div"));
this.twisty.classList.add("twisty");
set index(index) {
super.index = index;
this.id = this.view.getRowProperties(index);
this.classList.remove("level0", "level1", "level2");
this.classList.add(`level${this.view.getLevel(index)}`);
this.d2.textContent = this.view.getCellText(index, { id: "text" });
}
}
customElements.define("test-listrow", TestCardRow, { extends: "tr" });
this.d2 = this.appendChild(document.createElement("div"));
this.d2.classList.add("d2");
class TreeItem {
_children = [];
constructor(id, text, open = false, level = 0) {
this._id = id;
this._text = text;
this._open = open;
this._level = level;
}
getText() {
return this._text;
}
get open() {
return this._open;
}
get level() {
return this._level;
}
get children() {
return this._children;
}
getProperties() {
return this._id;
}
addChild(treeItem) {
treeItem._parent = this;
treeItem._level = this._level + 1;
this.children.push(treeItem);
}
}
get index() {
return super.index;
}
let testView = new PROTO_TREE_VIEW();
testView._rowMap.push(new TreeItem("row-1", "Item with no children"));
testView._rowMap.push(new TreeItem("row-2", "Item with children"));
testView._rowMap.push(new TreeItem("row-3", "Item with grandchildren"));
testView._rowMap[1].addChild(new TreeItem("row-2-1", "First child"));
testView._rowMap[1].addChild(new TreeItem("row-2-2", "Second child"));
testView._rowMap[2].addChild(new TreeItem("row-3-1", "First child"));
testView._rowMap[2].children[0].addChild(
new TreeItem("row-3-1-1", "First grandchild")
);
testView._rowMap[2].children[0].addChild(
new TreeItem("row-3-1-2", "Second grandchild")
);
testView.toggleOpenState(1);
testView.toggleOpenState(4);
testView.toggleOpenState(5);
set index(index) {
super.index = index;
this.id = this.view.getRowProperties(index);
this.classList.remove("level0", "level1", "level2");
this.classList.add(`level${this.view.getLevel(index)}`);
this.d2.textContent = this.view.getCellText(index, { id: "text" });
}
}
customElements.define("test-listrow", TestCardRow);
class TreeItem {
_children = [];
constructor(id, text, open = false, level = 0) {
this._id = id;
this._text = text;
this._open = open;
this._level = level;
}
getText() {
return this._text;
}
get open() {
return this._open;
}
get level() {
return this._level;
}
get children() {
return this._children;
}
getProperties() {
return this._id;
}
addChild(treeItem) {
treeItem._parent = this;
treeItem._level = this._level + 1;
this.children.push(treeItem);
}
}
let testView = new PROTO_TREE_VIEW();
testView._rowMap.push(new TreeItem("row-1", "Item with no children"));
testView._rowMap.push(new TreeItem("row-2", "Item with children"));
testView._rowMap.push(new TreeItem("row-3", "Item with grandchildren"));
testView._rowMap[1].addChild(new TreeItem("row-2-1", "First child"));
testView._rowMap[1].addChild(new TreeItem("row-2-2", "Second child"));
testView._rowMap[2].addChild(new TreeItem("row-3-1", "First child"));
testView._rowMap[2].children[0].addChild(
new TreeItem("row-3-1-1", "First grandchild")
);
testView._rowMap[2].children[0].addChild(
new TreeItem("row-3-1-2", "Second grandchild")
);
testView.toggleOpenState(1);
testView.toggleOpenState(4);
testView.toggleOpenState(5);
window.addEventListener("load", () => {
let list = document.getElementById("testList");
list.addEventListener("select", event => {
console.log("select event, selected indices:", list.selectedIndices);
let tree = document.getElementById("testTree");
let table = tree.table;
table.setListBoxID("testList");
table.listbox.setAttribute("rows", "test-listrow");
table.listbox.addEventListener("select", () => {
console.log(
"select event, selected indices:",
table.listbox.selectedIndices
);
});
list.view = testView;
table.listbox.view = testView;
});

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

@ -5,18 +5,12 @@
<title>Test for the tree-view-listbox custom element</title>
<link rel="stylesheet" href="chrome://messenger/skin/shared/tree-listbox.css" />
<style>
#testList {
.tree-view-scrollable-container {
height: 630px;
scroll-behavior: unset;
--in-content-button-background: rgba(12, 12, 13, 0.1);
--in-content-focus-outline-color: #45a1ff;
--in-content-item-selected-text: highlighttext;
--in-content-item-selected: highlight;
}
test-listrow {
height: 20px;
padding: 5px;
tr[is="test-listrow"] td > div {
display: flex;
align-items: center;
}
@ -26,19 +20,19 @@
height: 1em;
}
test-listrow.children div.twisty {
tr[is="test-listrow"].children div.twisty {
background-color: green;
}
test-listrow.children.collapsed div.twisty {
tr[is="test-listrow"].children.collapsed div.twisty {
background-color: red;
}
test-listrow.level1 .d2 {
tr[is="test-listrow"].level1 .d2 {
padding-inline-start: 1em;
}
test-listrow.level2 .d2 {
tr[is="test-listrow"].level2 .d2 {
padding-inline-start: 2em;
}
</style>
@ -47,6 +41,6 @@
<script defer="defer" src="treeViewListbox2.js"></script>
</head>
<body>
<tree-view-listbox id="testList" tabindex="0" rows="test-listrow"></tree-view-listbox>
<tree-view id="testTree"/>
</body>
</html>

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

@ -1350,6 +1350,11 @@ class AbCardListrow extends customElements.get("tree-view-listrow") {
}
customElements.define("ab-card-listrow", AbCardListrow, { extends: "tr" });
/**
* A row in the table list of cards.
*
* @augments {TreeViewListrow}
*/
class AbTableCardListrow extends customElements.get("tree-view-listrow") {
static ROW_HEIGHT = 22;
@ -1861,9 +1866,7 @@ var cardsPane = {
break;
}
for (let element of document.getElementById("cardsPlaceholder").children) {
element.hidden = !idsToShow.includes(element.id);
}
this.cardsList.updatePlaceholders(idsToShow);
},
/**
@ -2114,7 +2117,9 @@ var cardsPane = {
if (event.target == this.cardsList) {
row = this.cardsList.getRowAtIndex(this.cardsList.currentIndex);
} else {
row = event.target.closest("ab-card-listrow, ab-table-card-listrow");
row = event.target.closest(
`tr[is="ab-card-listrow"], tr[is="ab-table-card-listrow"]`
);
}
if (!row) {
return;
@ -2320,7 +2325,9 @@ var cardsPane = {
) {
return;
}
let row = event.target.closest("ab-card-listrow, ab-table-card-listrow");
let row = event.target.closest(
`tr[is="ab-card-listrow"], tr[is="ab-table-card-listrow"]`
);
if (row) {
this._activateRow(row.index);
}
@ -2362,7 +2369,9 @@ var cardsPane = {
return MailServices.headerParser.makeMimeAddress(card.displayName, email);
}
let row = event.target.closest("ab-card-listrow, ab-table-card-listrow");
let row = event.target.closest(
`tr[is="ab-card-listrow"], tr[is="ab-table-card-listrow"]`
);
if (!row) {
event.preventDefault();
return;

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

@ -119,26 +119,26 @@
</button>
</div>
<tree-view id="addressBookTree"/>
<div id="cardsPlaceholder">
<div id="placeholderEmptyBook"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-empty-book"></div>
<button id="placeholderCreateContact"
class="icon-button"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-new-contact"></button>
<div id="placeholderSearchOnly"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-search-only"></div>
<div id="placeholderSearching"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-searching"></div>
<div id="placeholderNoSearchResults"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-no-search-results"></div>
</div>
<tree-view id="addressBookTree">
<slot name="placeholders">
<div id="placeholderEmptyBook"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-empty-book"></div>
<button id="placeholderCreateContact"
class="icon-button"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-new-contact"></button>
<div id="placeholderSearchOnly"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-search-only"></div>
<div id="placeholderSearching"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-searching"></div>
<div id="placeholderNoSearchResults"
hidden="hidden"
data-l10n-id="about-addressbook-placeholder-no-search-results"></div>
</slot>
</tree-view>
</div>
<!-- We will dynamically switch this splitter to be horizontal or vertical and
affect the cardsPane or detailsPane based on the required layout. -->

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

@ -40,7 +40,7 @@ add_task(async function test_additions_and_removals() {
let contactB1 = bookB.addCard(createContact("contact", "B1"));
let abWindow = await openAddressBookWindow();
let cardsList = abWindow.document.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
await openAllAddressBooks();
info("Performing check #1");
@ -248,7 +248,7 @@ add_task(async function test_name_column() {
book.addCard(createContact("echo", "november", "uniform"));
let abWindow = await openAddressBookWindow();
let cardsList = abWindow.document.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
// Check the format is display name, ascending.
Assert.equal(
@ -818,32 +818,32 @@ add_task(async function test_context_menu_delete() {
add_task(async function test_layout() {
function checkColumns(visibleColumns, sortColumn, sortDirection) {
let visibleHeaders = cardsHeader.querySelectorAll("button:not([hidden])");
let visibleHeaders = cardsHeader.querySelectorAll(
`th[is="tree-view-table-header-cell"]:not([hidden])`
);
Assert.deepEqual(
Array.from(visibleHeaders, b => b.value),
Array.from(visibleHeaders, h => h.id),
visibleColumns,
"visible columns are correct"
);
for (let header of visibleHeaders) {
let button = header.querySelector("button");
Assert.equal(
header.classList.contains("ascending"),
header.value == sortColumn && sortDirection == "ascending",
`${header.value} header is ascending`
button.classList.contains("ascending"),
header.id == sortColumn && sortDirection == "ascending",
`${header.id} header is ascending`
);
Assert.equal(
header.classList.contains("descending"),
header.value == sortColumn && sortDirection == "descending",
`${header.value} header is descending`
button.classList.contains("descending"),
header.id == sortColumn && sortDirection == "descending",
`${header.id} header is descending`
);
}
}
function checkRowHeight(height) {
Assert.equal(cardsList.getRowAtIndex(0).clientHeight, height);
Assert.equal(cardsList.getRowAtIndex(0).style.top, "0px");
Assert.equal(cardsList.getRowAtIndex(1).clientHeight, height);
Assert.equal(cardsList.getRowAtIndex(1).style.top, `${height}px`);
}
Services.prefs.setIntPref("mail.uidensity", 0);
@ -862,8 +862,8 @@ add_task(async function test_layout() {
let abWindow = await openAddressBookWindow();
let abDocument = abWindow.document;
let cardsHeader = abDocument.getElementById("cardsHeader");
let cardsList = abDocument.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
let cardsHeader = abWindow.cardsPane.table.header;
let sharedSplitter = abDocument.getElementById("sharedSplitter");
// Sanity check.
@ -928,7 +928,7 @@ add_task(async function test_layout() {
// Click the email addresses header to sort.
EventUtils.synthesizeMouseAtCenter(
cardsHeader.querySelector(`[value="EmailAddresses"]`),
cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
{},
abWindow
);
@ -947,7 +947,7 @@ add_task(async function test_layout() {
// Click the email addresses header again to flip the sort.
EventUtils.synthesizeMouseAtCenter(
cardsHeader.querySelector(`[value="EmailAddresses"]`),
cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
{},
abWindow
);
@ -965,9 +965,9 @@ add_task(async function test_layout() {
// Add a column.
await showSortMenu("toggle", "Title");
await showPickerMenu("toggle", "Title");
await TestUtils.waitForCondition(
() => !cardsHeader.querySelector(`[value="Title"]`).hidden
() => !cardsHeader.querySelector(`[id="Title"]`).hidden
);
checkColumns(
["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses", "Title"],
@ -977,9 +977,9 @@ add_task(async function test_layout() {
// Remove a column.
await showSortMenu("toggle", "Addresses");
await showPickerMenu("toggle", "Addresses");
await TestUtils.waitForCondition(
() => cardsHeader.querySelector(`[value="Addresses"]`).hidden
() => cardsHeader.querySelector(`[id="Addresses"]`).hidden
);
checkColumns(
["GeneratedName", "EmailAddresses", "PhoneNumbers", "Title"],
@ -1001,8 +1001,8 @@ add_task(async function test_layout() {
abWindow = await openAddressBookWindow();
abDocument = abWindow.document;
cardsHeader = abDocument.getElementById("cardsHeader");
cardsList = abDocument.getElementById("cards");
cardsList = abWindow.cardsPane.cardsList;
cardsHeader = abWindow.cardsPane.table.header;
sharedSplitter = abDocument.getElementById("sharedSplitter");
Assert.ok(
@ -1149,17 +1149,16 @@ add_task(async function test_list_table_layout() {
book.addMailList(list);
let abWindow = await openAddressBookWindow();
let abDocument = abWindow.document;
let cardsList = abWindow.cardsPane.cardsList;
let cardsHeader = abDocument.getElementById("cardsHeader");
let cardsHeader = abWindow.cardsPane.table.header;
// Switch layout to table.
await toggleLayout();
await showSortMenu("toggle", "addrbook");
await showPickerMenu("toggle", "addrbook");
await TestUtils.waitForCondition(
() => !cardsHeader.querySelector(`[value="addrbook"]`).hidden
() => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
);
// Check for the contact that the column is shown.
@ -1202,9 +1201,8 @@ add_task(async function test_list_all_address_book() {
secondBook.addMailList(list);
let abWindow = await openAddressBookWindow();
let abDocument = abWindow.document;
let cardsList = abWindow.cardsPane.cardsList;
let cardsHeader = abDocument.getElementById("cardsHeader");
let cardsHeader = abWindow.cardsPane.table.header;
info("Check that no address book suffix is present.");
Assert.ok(
@ -1223,7 +1221,7 @@ add_task(async function test_list_all_address_book() {
info("Toggle the option to show address books.");
await showSortMenu("toggle", "addrbook");
await TestUtils.waitForCondition(
() => !cardsHeader.querySelector(`[value="addrbook"]`).hidden
() => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
);
Assert.ok(

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

@ -968,7 +968,7 @@ add_task(async function test_total_address_book_count() {
// Delete a contact an check that the count updates.
let promptPromise = BrowserTestUtils.promiseAlertDialog("accept");
let deletedPromise = TestUtils.topicObserved("addrbook-contact-deleted");
let cards = abDocument.getElementById("cards");
let cards = abWindow.cardsPane.cardsList;
EventUtils.synthesizeMouseAtCenter(cards.getRowAtIndex(0), {}, abWindow);
EventUtils.synthesizeKey("VK_DELETE", {}, abWindow);
await promptPromise;

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

@ -52,7 +52,7 @@ add_task(async () => {
let abDocument = abWindow.document;
let searchBox = abDocument.getElementById("searchInput");
let cardsList = abDocument.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
let noSearchResults = abDocument.getElementById("placeholderNoSearchResults");
let detailsPane = abDocument.getElementById("detailsPane");

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

@ -60,7 +60,7 @@ add_task(async () => {
let abDocument = abWindow.document;
let searchBox = abDocument.getElementById("searchInput");
let cardsList = abDocument.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
Assert.equal(
abDocument.activeElement,

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

@ -206,7 +206,7 @@ async function createMailingListWithUI(mlParent, mlName) {
function checkDirectoryDisplayed(directory) {
let abWindow = getAddressBookWindow();
let booksList = abWindow.document.getElementById("books");
let cardsList = abWindow.document.getElementById("cards");
let cardsList = abWindow.cardsPane.cardsList;
if (directory) {
Assert.equal(
@ -249,7 +249,7 @@ function checkNamesListed(...expectedNames) {
function checkPlaceholders(expectedVisible = []) {
let abWindow = getAddressBookWindow();
let placeholder = abWindow.document.getElementById("cardsPlaceholder");
let placeholder = abWindow.cardsPane.cardsList.placeholder;
if (!expectedVisible.length) {
Assert.ok(
@ -288,6 +288,28 @@ async function showSortMenu(name, value) {
await hiddenPromise;
}
async function showPickerMenu(name, value) {
let abWindow = getAddressBookWindow();
let cardsHeader = abWindow.cardsPane.table.header;
let pickerButton = cardsHeader.querySelector(
`th[is="tree-view-table-column-picker"] button`
);
let menupopup = cardsHeader.querySelector(
`th[is="tree-view-table-column-picker"] menupopup`
);
let shownPromise = BrowserTestUtils.waitForEvent(menupopup, "popupshown");
EventUtils.synthesizeMouseAtCenter(pickerButton, {}, abWindow);
await shownPromise;
let hiddenPromise = BrowserTestUtils.waitForEvent(menupopup, "popuphidden");
menupopup.activateItem(
menupopup.querySelector(`[name="${name}"][value="${value}"]`)
);
if (name == "toggle") {
menupopup.hidePopup();
}
await hiddenPromise;
}
async function toggleLayout() {
let abWindow = getAddressBookWindow();
let abDocument = abWindow.document;

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

@ -397,40 +397,6 @@ body:not(.layout-table).all-ab-selected ~ menupopup#sortContext >
fill: currentColor;
}
#cardsPlaceholder {
position: absolute;
inset: 120px 0 auto;
display: none;
padding: 0 40px;
box-sizing: border-box;
color: var(--in-content-deemphasized-text);
text-align: center;
}
#cards:empty + #cardsPlaceholder {
display: block;
}
#cardsPlaceholder > div {
margin-block-end: 0.25em;
font-size: 1.3rem;
line-height: 1.2;
}
#cardsPlaceholder > div::before {
content: "";
display: block;
height: 32px;
margin-block-end: 9px;
background-position: center top;
background-size: contain;
background-repeat: no-repeat;
-moz-context-properties: fill, stroke, fill-opacity;
fill: color-mix(in srgb, currentColor 20%, transparent);
stroke: currentColor;
fill-opacity: var(--toolbarbutton-icon-fill-opacity);
}
#placeholderEmptyBook::before,
#placeholderSearchOnly::before {
background-image: var(--addressbook-tree-list);
@ -456,6 +422,8 @@ body:not(.layout-table).all-ab-selected ~ menupopup#sortContext >
tr[is="ab-card-listrow"] td > .card-container {
display: flex;
align-items: center;
max-height: inherit;
box-sizing: border-box;
}
.selected-card {

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

@ -92,6 +92,8 @@ table[is="tree-view-table"] {
table-layout: fixed;
flex: 1 0 100%;
border-spacing: 0;
line-height: 1;
font-size: 1.05rem;
}
table[is="tree-view-table"] td {
@ -230,6 +232,44 @@ table[is="tree-view-table"] td div {
text-overflow: ellipsis;
}
/* Placeholder */
slot[name="placeholders"] {
position: absolute;
display: none;
box-sizing: border-box;
inset: 120px 0 auto;
padding: 0 40px;
color: var(--in-content-deemphasized-text);
text-align: center;
}
slot[name="placeholders"].show {
display: block;
}
slot[name="placeholders"] > div{
font-size: 1.5rem;
line-height: 1.2;
font-weight: 600;
margin-block-end: 12px;
text-shadow: 0 1px 0px var(--in-content-page-background);
}
slot[name="placeholders"] div::before {
content: "";
display: block;
height: 32px;
margin-block-end: 9px;
background-position: center top;
background-size: contain;
background-repeat: no-repeat;
-moz-context-properties: fill, stroke, fill-opacity;
fill: color-mix(in srgb, currentColor 20%, transparent);
stroke: currentColor;
fill-opacity: var(--toolbarbutton-icon-fill-opacity);
}
/* Transitions and animations */
@media (prefers-reduced-motion: no-preference) {