Bug 1756387 - Fix keyboard navigation issues on about:logins. r=sgalich,dimi

Differential Revision: https://phabricator.services.mozilla.com/D139848
This commit is contained in:
Tim Giles 2022-03-11 15:19:53 +00:00
parent 897cc21b64
commit b9661755d7
3 changed files with 132 additions and 63 deletions

View file

@ -15,7 +15,7 @@ body {
--sidebar-width: 320px;
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: 1fr;
grid-template-rows: auto 1fr;
}
@media (max-width: 830px) {

View file

@ -33,16 +33,14 @@
<link rel="icon" href="chrome://branding/content/icon32.png">
</head>
<body>
<login-list></login-list>
<section>
<header>
<login-filter></login-filter>
<fxaccounts-button hidden></fxaccounts-button>
<menu-button></menu-button>
</header>
<login-list></login-list>
<login-item></login-item>
<login-intro></login-intro>
</section>
<confirmation-dialog hidden></confirmation-dialog>
<remove-logins-dialog hidden></remove-logins-dialog>
<import-summary-dialog hidden></import-summary-dialog>

View file

@ -30,70 +30,137 @@ add_task(async function test_tab_key_nav() {
let browser = gBrowser.selectedBrowser;
await SpecialPowers.spawn(browser, [], async () => {
const EventUtils = ContentTaskUtils.getEventUtils(content);
// list [selector, shadow root selector] for each element
// in the order we expect them to be navigated.
let expectedElementsInOrder = [
["login-filter", "input"],
["fxaccounts-button", "button"],
["menu-button", "button"],
["login-list", "select"],
["login-list", "ol"],
["login-list", "button"],
["login-item", "button.edit-button"],
["login-item", "button.delete-button"],
["login-item", "a.origin-input"],
["login-item", "button.copy-username-button"],
["login-item", "input.reveal-password-checkbox"],
["login-item", "button.copy-password-button"],
];
let firstElement = content.document
.querySelector(expectedElementsInOrder.at(0).at(0))
.shadowRoot.querySelector(expectedElementsInOrder.at(0).at(1));
let lastElement = content.document
.querySelector(expectedElementsInOrder.at(-1).at(0))
.shadowRoot.querySelector(expectedElementsInOrder.at(-1).at(1));
async function tab() {
EventUtils.synthesizeKey("KEY_Tab", {}, content);
await new Promise(resolve => content.requestAnimationFrame(resolve));
// The following line can help with focus trap debugging:
// await new Promise(resolve => content.window.setTimeout(resolve, 100));
// await new Promise(resolve => content.window.setTimeout(resolve, 2000));
}
async function shiftTab() {
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, content);
await new Promise(resolve => content.requestAnimationFrame(resolve));
}
// Getting focused shadow DOM element itself instead of shadowRoot,
// using recursion for any component-nesting level, as in:
// document.activeElement.shadowRoot.activeElement.shadowRoot.activeElement
function getFocusedEl() {
let el = content.document.activeElement;
function getFocusedElement() {
let element = content.document.activeElement;
const getShadowRootFocus = e => {
if (e.shadowRoot) {
return getShadowRootFocus(e.shadowRoot.activeElement);
}
return e;
};
return getShadowRootFocus(el);
return getShadowRootFocus(element);
}
// Were making two passes with focused and focusedAgain sets of DOM elements,
// rather than just checking we get back to the first focused element,
// so we can cover more edge-cases involving issues with focus tree.
const focused = new Set();
const focusedAgain = new Set();
// Initializing: some element is supposed to be automatically focused
const firstEl = getFocusedEl();
focused.add(firstEl);
// Setting a maximum number of keypresses to escape the loop,
// should we fall into a focus trap.
// Fixed amount of keypresses also helps assess consistency with keyboard navigation.
const maxKeypresses = content.document.getElementsByTagName("*").length * 2;
let keypresses = 1;
while (focusedAgain.size < focused.size && keypresses <= maxKeypresses) {
// Helper function for getting the DOM element given an entry in the ordered list
function getElementFromOrderedArray(combinedSelectors) {
let [selector, shadowRootSelector] = combinedSelectors;
return content.document
.querySelector(selector)
.shadowRoot.querySelector(shadowRootSelector);
}
// Ensure the test starts in a valid state
firstElement.focus();
info(`what is our navigator platform ${content.window.navigator.platform}`);
// Assert that we tab navigate correctly
for (let expectedSelector of expectedElementsInOrder) {
let expectedElement = getElementFromOrderedArray(expectedSelector);
// By default, MacOS will skip over certain text controls, such as links.
if (
content.window.navigator.platform.toLowerCase().includes("mac") &&
expectedElement.tagName === "A"
) {
continue;
}
expectedElement = Cu.waiveXrays(expectedElement);
info(`expectedElement className ${expectedElement.className}`);
let actualElem = getFocusedElement();
actualElem = Cu.waiveXrays(actualElem);
info(`actualElement className ${actualElem.className}`);
is(
actualElem,
expectedElement,
"Actual focused element should equal the expected focused element"
);
await tab();
keypresses++;
let el = getFocusedEl();
}
// The following block is needed to get back to document
// after tabbing to browser chrome.
// This focus trap in browser chrome is a testing artifact were fixing below.
if (el.tagName === "BODY" && el !== firstEl) {
firstEl.focus();
await new Promise(resolve => content.requestAnimationFrame(resolve));
el = getFocusedEl();
if (getFocusedElement().tagName === "BODY") {
firstElement.focus();
}
if (focused.has(el)) {
focusedAgain.add(el);
} else {
focused.add(el);
// Assert that we tab navigate correctly after looping through chrome elements
for (let expectedSelector of expectedElementsInOrder) {
let expectedElement = getElementFromOrderedArray(expectedSelector);
// By default, MacOS will skip over certain text controls, such as links.
if (
content.window.navigator.platform.toLowerCase().includes("mac") &&
expectedElement.tagName === "A"
) {
continue;
}
}
let actualElement = getFocusedElement();
is(
focusedAgain.size,
focused.size,
"All focusable elements should be kept accessible with TAB key (no focus trap)."
actualElement,
expectedElement,
"Actual focused element should equal the expected focused element"
);
await tab();
}
// The following block is needed to get back to document
// after tabbing to browser chrome.
// This focus trap in browser chrome is a testing artifact were fixing below.
if (getFocusedElement().tagName === "BODY") {
lastElement.focus();
}
// Assert that we shift + tab navigate correctly starting from the last ordered element
for (let expectedSelector of expectedElementsInOrder.reverse()) {
let expectedElement = getElementFromOrderedArray(expectedSelector);
// By default, MacOS will skip over certain text controls, such as links.
if (
content.window.navigator.platform.toLowerCase().includes("mac") &&
expectedElement.tagName === "A"
) {
continue;
}
let actualElement = getFocusedElement();
is(
actualElement,
expectedElement,
"Actual focused element should equal the expected focused element"
);
await shiftTab();
}
});
});
@ -117,22 +184,22 @@ add_task(async function testTabToCreateButton() {
let createButton = loginList.shadowRoot.querySelector(
".create-login-button"
);
let getFocusedEl = () => loginList.shadowRoot.activeElement;
let getFocusedElement = () => loginList.shadowRoot.activeElement;
is(getFocusedEl(), null, "login-list isn't focused");
is(getFocusedElement(), null, "login-list isn't focused");
loginSort.focus();
await waitForAnimationFrame();
is(getFocusedEl(), loginSort, "login sort is focused");
is(getFocusedElement(), loginSort, "login sort is focused");
await tab();
is(getFocusedEl(), loginListbox, "listbox is focused next");
is(getFocusedElement(), loginListbox, "listbox is focused next");
await tab();
is(getFocusedEl(), createButton, "create button is after");
is(getFocusedElement(), createButton, "create button is after");
await tab();
is(getFocusedEl(), null, "login-list isn't focused again");
is(getFocusedElement(), null, "login-list isn't focused again");
});
});
@ -156,18 +223,22 @@ add_task(async function testTabToEditButton() {
let loginList = content.document.querySelector("login-list");
let loginItem = content.document.querySelector("login-item");
let loginFilter = content.document.querySelector("login-filter");
let createButton = loginList.shadowRoot.querySelector(
".create-login-button"
);
let editButton = loginItem.shadowRoot.querySelector(".edit-button");
let breachAlert = loginItem.shadowRoot.querySelector(".breach-alert");
let getFocusedEl = () => {
let getFocusedElement = () => {
if (content.document.activeElement == loginList) {
return loginList.shadowRoot.activeElement;
}
if (content.document.activeElement == loginItem) {
return loginItem.shadowRoot.activeElement;
}
if (content.document.activeElement == loginFilter) {
return loginFilter.shadowRoot.activeElement;
}
ok(
false,
"not expecting a different element to get focused in this test: " +
@ -198,11 +269,11 @@ add_task(async function testTabToEditButton() {
createButton.focus();
await waitForAnimationFrame();
is(getFocusedEl(), createButton, "create button is focused");
is(getFocusedElement(), createButton, "create button is focused");
await tab();
await waitForAnimationFrame();
is(getFocusedEl(), editButton, "edit button is focused");
is(getFocusedElement(), editButton, "edit button is focused");
}
}
);