зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
79c92e7e4f
Коммит
40cd24ed54
|
@ -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");
|
||||
});
|
Загрузка…
Ссылка в новой задаче