Bug 1753251 - Treats twisty image in about:processes as a disclosure button. r=Jamie,florian

Summary row includes a Twisty image button to expand/collapse details - it is made focusable with keyboard, programmatic role of a `button` and a descriptive label are assigned, and keyboard events are provided. Related code is refactored and focus styling is updated.

Differential Revision: https://phabricator.services.mozilla.com/D139151
This commit is contained in:
Anna Yeddi 2022-09-14 12:58:47 +00:00
Родитель 79c92e7e4f
Коммит 40cd24ed54
4 изменённых файлов: 228 добавлений и 51 удалений

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

@ -131,7 +131,7 @@ td:not(:hover) > .profiler-icon:not(.profiler-active) {
line-height: 50%;
top: 4px; /* Half the image's height */
inset-inline-start: -16px;
width: 100%;
width: 12px;
-moz-context-properties: fill;
fill: currentColor;
}
@ -141,6 +141,12 @@ td:not(:hover) > .profiler-icon:not(.profiler-active) {
.twisty.open::before {
content: url("chrome://global/skin/icons/arrow-down-12.svg");
}
.twisty:-moz-focusring {
outline: none;
}
.twisty:-moz-focusring::before {
outline: var(--in-content-focus-outline);
}
.indent {
padding-inline: 48px 0;
}

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

@ -587,6 +587,7 @@ var View = {
}
document.l10n.setAttributes(processNameElement, fluentName, fluentArgs);
nameCell.className = ["type", "favicon", ...classNames].join(" ");
nameCell.setAttribute("id", data.pid + "-label");
let image;
switch (data.type) {
@ -758,18 +759,27 @@ var View = {
let span;
if (!nameCell.firstChild) {
nameCell.className = "name indent";
// Create the nodes
let img = document.createElement("span");
img.className = "twisty";
nameCell.appendChild(img);
// Create the nodes:
let imgBtn = document.createElement("span");
// Provide markup for an accessible disclosure button:
imgBtn.className = "twisty";
imgBtn.setAttribute("role", "button");
imgBtn.setAttribute("tabindex", "0");
// Label to include both summary and details texts
imgBtn.setAttribute("aria-labelledby", `${data.pid}-label ${rowId}`);
if (!imgBtn.hasAttribute("aria-expanded")) {
imgBtn.setAttribute("aria-expanded", "false");
}
nameCell.appendChild(imgBtn);
span = document.createElement("span");
span.setAttribute("id", rowId);
nameCell.appendChild(span);
} else {
// The only thing that can change is the thread count.
let img = nameCell.firstChild;
isOpen = img.classList.contains("open");
span = img.nextSibling;
let imgBtn = nameCell.firstChild;
isOpen = imgBtn.classList.contains("open");
span = imgBtn.nextSibling;
}
document.l10n.setAttributes(span, fluentName, fluentArgs);
@ -1055,54 +1065,25 @@ var Control = {
// Single click:
// - show or hide the contents of a twisty;
// - close a process;
// - profile a process;
// - change selection.
tbody.addEventListener("click", event => {
this._updateLastMouseEvent();
// Handle showing or hiding subitems of a row.
let target = event.target;
if (target.classList.contains("twisty")) {
this._handleTwisty(target);
return;
}
if (target.classList.contains("close-icon")) {
this._handleKill(target);
return;
}
this._handleActivate(event.target);
});
if (target.classList.contains("profiler-icon")) {
if (Services.profiler.IsActive()) {
return;
}
Services.profiler.StartProfiler(
10000000,
1,
["default", "ipcmessages", "power"],
["pid:" + target.parentNode.parentNode.process.pid]
);
target.classList.add("profiler-active");
setTimeout(() => {
ProfilerPopupBackground.captureProfile("aboutprofiling");
target.classList.remove("profiler-active");
}, PROFILE_DURATION * 1000);
return;
// Enter or Space keypress:
// - show or hide the contents of a twisty;
// - close a process;
// - profile a process;
// - change selection.
tbody.addEventListener("keypress", event => {
// Handle showing or hiding subitems of a row, when keyboard is used.
if (event.key === "Enter" || event.key === " ") {
this._handleActivate(event.target);
}
// Handle selection changes
let row = target.closest("tr");
if (!row) {
return;
}
if (this.selectedRow) {
this.selectedRow.removeAttribute("selected");
if (this.selectedRow.rowId == row.rowId) {
// Clicking the same row again clears the selection.
this.selectedRow = null;
return;
}
}
row.setAttribute("selected", "true");
this.selectedRow = row;
});
// Double click:
@ -1435,18 +1416,39 @@ var Control = {
}
},
// Handle events on image controls.
_handleActivate(target) {
if (target.classList.contains("twisty")) {
this._handleTwisty(target);
return;
}
if (target.classList.contains("close-icon")) {
this._handleKill(target);
return;
}
if (target.classList.contains("profiler-icon")) {
this._handleProfiling(target);
return;
}
this._handleSelection(target);
},
// Open/close list of threads.
_handleTwisty(target) {
let row = target.parentNode.parentNode;
if (target.classList.toggle("open")) {
target.setAttribute("aria-expanded", "true");
this._showThreads(row, this._maxSlopeCpu);
View.insertAfterRow(row);
} else {
target.setAttribute("aria-expanded", "false");
this._removeSubtree(row);
}
},
// Kill process/close tab/close subframe
// Kill process/close tab/close subframe.
_handleKill(target) {
let row = target.parentNode;
if (row.process) {
@ -1512,6 +1514,42 @@ var Control = {
}
}
},
// Handle profiling of a process.
_handleProfiling(target) {
if (Services.profiler.IsActive()) {
return;
}
Services.profiler.StartProfiler(
10000000,
1,
["default", "ipcmessages", "power"],
["pid:" + target.parentNode.parentNode.process.pid]
);
target.classList.add("profiler-active");
setTimeout(() => {
ProfilerPopupBackground.captureProfile("aboutprofiling");
target.classList.remove("profiler-active");
}, PROFILE_DURATION * 1000);
},
// Handle selection changes.
_handleSelection(target) {
let row = target.closest("tr");
if (!row) {
return;
}
if (this.selectedRow) {
this.selectedRow.removeAttribute("selected");
if (this.selectedRow.rowId == row.rowId) {
// Clicking the same row again clears the selection.
this.selectedRow = null;
return;
}
}
row.setAttribute("selected", "true");
this.selectedRow = row;
},
};
window.onload = async function() {

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

@ -14,3 +14,4 @@ https_first_disabled = true
[browser_aboutprocesses_show_frames_without_threads.js]
https_first_disabled = true
[browser_aboutprocesses_selection.js]
[browser_aboutprocesses_twisty.js]

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

@ -0,0 +1,132 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let doc, tbody, tabAboutProcesses;
const rowTypes = ["process", "window", "thread-summary", "thread"];
function promiseUpdate() {
return promiseAboutProcessesUpdated({
doc,
tbody,
force: true,
tabAboutProcesses,
});
}
add_setup(async function() {
Services.prefs.setBoolPref("toolkit.aboutProcesses.showThreads", true);
info("Setting up about:processes");
tabAboutProcesses = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
opening: "about:processes",
waitForLoad: true,
});
doc = tabAboutProcesses.linkedBrowser.contentDocument;
tbody = doc.getElementById("process-tbody");
await promiseUpdate();
});
add_task(function testTwistyImageButtonSetup() {
let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
let groupRow = twistyBtn.parentNode.parentNode;
info("Verify twisty button is properly set up.");
Assert.ok(
twistyBtn.hasAttribute("aria-labelledby"),
"the Twisty image button has an aria-labelledby"
);
Assert.equal(
twistyBtn.getAttribute("aria-labelledby"),
groupRow.firstChild.children[1].getAttribute("id"),
"the Twisty image button's aria-labelledby refers to a valid 'id' that is the Name of its row"
);
Assert.equal(
twistyBtn.getAttribute("role"),
"button",
"the Twisty image is programmatically a button"
);
Assert.equal(
twistyBtn.getAttribute("tabindex"),
"0",
"the Twisty image button is included in the focus order"
);
Assert.equal(
twistyBtn.getAttribute("aria-expanded"),
"false",
"the Twisty image button is collapsed by default"
);
});
add_task(function testTwistyImageButtonClicking() {
let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
let groupRow = twistyBtn.parentNode.parentNode;
info(
"Verify we can toggle/open a list of threads by clicking the twisty button."
);
twistyBtn.click();
Assert.ok(
groupRow.nextSibling.classList.contains("thread") &&
!groupRow.nextSibling.classList.contains("thread-summary"),
"clicking a collapsed Twisty adds subitems after the row"
);
Assert.equal(
twistyBtn.getAttribute("aria-expanded"),
"true",
"the Twisty image button is expanded after a click"
);
});
add_task(function testTwistyImageButtonKeypressing() {
let twistyBtn = doc.querySelector("tr.thread-summary .twisty");
let groupRow = twistyBtn.parentNode.parentNode;
info(
`Verify we can toggle/close a list of threads by pressing Enter or
Space on the twisty button.`
);
// Verify the twisty button can be focused with a keyboard.
twistyBtn.focus();
Assert.equal(
twistyBtn,
doc.activeElement,
"the Twisty image button can be focused"
);
// Verify we can toggle subitems with a keyboard.
// Twisty is expanded
EventUtils.synthesizeKey("KEY_Enter");
Assert.ok(
!groupRow.nextSibling.classList.contains("thread") ||
groupRow.nextSibling.classList.contains("thread-summary"),
"pressing Enter on expanded Twisty hides a list of threads after the row"
);
Assert.equal(
twistyBtn.getAttribute("aria-expanded"),
"false",
"the Twisty image button is collapsed after an Enter keypress"
);
// Twisty is collapsed
EventUtils.synthesizeKey(" ");
Assert.ok(
groupRow.nextSibling.classList.contains("thread") &&
!groupRow.nextSibling.classList.contains("thread-summary"),
"pressing Space on collapsed Twisty shows a list of threads after the row"
);
Assert.equal(
twistyBtn.getAttribute("aria-expanded"),
"true",
"the Twisty image button is expanded after a Space keypress"
);
});
add_task(function cleanup() {
BrowserTestUtils.removeTab(tabAboutProcesses);
Services.prefs.clearUserPref("toolkit.aboutProcesses.showThreads");
});