forked from mirrors/gecko-dev
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:
parent
897cc21b64
commit
b9661755d7
3 changed files with 132 additions and 63 deletions
|
|
@ -15,7 +15,7 @@ body {
|
||||||
--sidebar-width: 320px;
|
--sidebar-width: 320px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--sidebar-width) 1fr;
|
grid-template-columns: var(--sidebar-width) 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 830px) {
|
@media (max-width: 830px) {
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,14 @@
|
||||||
<link rel="icon" href="chrome://branding/content/icon32.png">
|
<link rel="icon" href="chrome://branding/content/icon32.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
|
<login-filter></login-filter>
|
||||||
|
<fxaccounts-button hidden></fxaccounts-button>
|
||||||
|
<menu-button></menu-button>
|
||||||
|
</header>
|
||||||
<login-list></login-list>
|
<login-list></login-list>
|
||||||
<section>
|
<login-item></login-item>
|
||||||
<header>
|
<login-intro></login-intro>
|
||||||
<login-filter></login-filter>
|
|
||||||
<fxaccounts-button hidden></fxaccounts-button>
|
|
||||||
<menu-button></menu-button>
|
|
||||||
</header>
|
|
||||||
<login-item></login-item>
|
|
||||||
<login-intro></login-intro>
|
|
||||||
</section>
|
|
||||||
<confirmation-dialog hidden></confirmation-dialog>
|
<confirmation-dialog hidden></confirmation-dialog>
|
||||||
<remove-logins-dialog hidden></remove-logins-dialog>
|
<remove-logins-dialog hidden></remove-logins-dialog>
|
||||||
<import-summary-dialog hidden></import-summary-dialog>
|
<import-summary-dialog hidden></import-summary-dialog>
|
||||||
|
|
|
||||||
|
|
@ -30,70 +30,137 @@ add_task(async function test_tab_key_nav() {
|
||||||
let browser = gBrowser.selectedBrowser;
|
let browser = gBrowser.selectedBrowser;
|
||||||
await SpecialPowers.spawn(browser, [], async () => {
|
await SpecialPowers.spawn(browser, [], async () => {
|
||||||
const EventUtils = ContentTaskUtils.getEventUtils(content);
|
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() {
|
async function tab() {
|
||||||
EventUtils.synthesizeKey("KEY_Tab", {}, content);
|
EventUtils.synthesizeKey("KEY_Tab", {}, content);
|
||||||
await new Promise(resolve => content.requestAnimationFrame(resolve));
|
await new Promise(resolve => content.requestAnimationFrame(resolve));
|
||||||
// The following line can help with focus trap debugging:
|
// 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,
|
// Getting focused shadow DOM element itself instead of shadowRoot,
|
||||||
// using recursion for any component-nesting level, as in:
|
// using recursion for any component-nesting level, as in:
|
||||||
// document.activeElement.shadowRoot.activeElement.shadowRoot.activeElement
|
// document.activeElement.shadowRoot.activeElement.shadowRoot.activeElement
|
||||||
function getFocusedEl() {
|
function getFocusedElement() {
|
||||||
let el = content.document.activeElement;
|
let element = content.document.activeElement;
|
||||||
const getShadowRootFocus = e => {
|
const getShadowRootFocus = e => {
|
||||||
if (e.shadowRoot) {
|
if (e.shadowRoot) {
|
||||||
return getShadowRootFocus(e.shadowRoot.activeElement);
|
return getShadowRootFocus(e.shadowRoot.activeElement);
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
return getShadowRootFocus(el);
|
return getShadowRootFocus(element);
|
||||||
}
|
}
|
||||||
|
// Helper function for getting the DOM element given an entry in the ordered list
|
||||||
// We’re making two passes with focused and focusedAgain sets of DOM elements,
|
function getElementFromOrderedArray(combinedSelectors) {
|
||||||
// rather than just checking we get back to the first focused element,
|
let [selector, shadowRootSelector] = combinedSelectors;
|
||||||
// so we can cover more edge-cases involving issues with focus tree.
|
return content.document
|
||||||
const focused = new Set();
|
.querySelector(selector)
|
||||||
const focusedAgain = new Set();
|
.shadowRoot.querySelector(shadowRootSelector);
|
||||||
|
}
|
||||||
// Initializing: some element is supposed to be automatically focused
|
// Ensure the test starts in a valid state
|
||||||
const firstEl = getFocusedEl();
|
firstElement.focus();
|
||||||
focused.add(firstEl);
|
info(`what is our navigator platform ${content.window.navigator.platform}`);
|
||||||
|
// Assert that we tab navigate correctly
|
||||||
// Setting a maximum number of keypresses to escape the loop,
|
for (let expectedSelector of expectedElementsInOrder) {
|
||||||
// should we fall into a focus trap.
|
let expectedElement = getElementFromOrderedArray(expectedSelector);
|
||||||
// Fixed amount of keypresses also helps assess consistency with keyboard navigation.
|
// By default, MacOS will skip over certain text controls, such as links.
|
||||||
const maxKeypresses = content.document.getElementsByTagName("*").length * 2;
|
if (
|
||||||
let keypresses = 1;
|
content.window.navigator.platform.toLowerCase().includes("mac") &&
|
||||||
|
expectedElement.tagName === "A"
|
||||||
while (focusedAgain.size < focused.size && keypresses <= maxKeypresses) {
|
) {
|
||||||
|
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();
|
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 we’re fixing below.
|
|
||||||
if (el.tagName === "BODY" && el !== firstEl) {
|
|
||||||
firstEl.focus();
|
|
||||||
await new Promise(resolve => content.requestAnimationFrame(resolve));
|
|
||||||
el = getFocusedEl();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focused.has(el)) {
|
|
||||||
focusedAgain.add(el);
|
|
||||||
} else {
|
|
||||||
focused.add(el);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is(
|
// The following block is needed to get back to document
|
||||||
focusedAgain.size,
|
// after tabbing to browser chrome.
|
||||||
focused.size,
|
// This focus trap in browser chrome is a testing artifact we’re fixing below.
|
||||||
"All focusable elements should be kept accessible with TAB key (no focus trap)."
|
if (getFocusedElement().tagName === "BODY") {
|
||||||
);
|
firstElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
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 we’re 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(
|
let createButton = loginList.shadowRoot.querySelector(
|
||||||
".create-login-button"
|
".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();
|
loginSort.focus();
|
||||||
await waitForAnimationFrame();
|
await waitForAnimationFrame();
|
||||||
is(getFocusedEl(), loginSort, "login sort is focused");
|
is(getFocusedElement(), loginSort, "login sort is focused");
|
||||||
|
|
||||||
await tab();
|
await tab();
|
||||||
is(getFocusedEl(), loginListbox, "listbox is focused next");
|
is(getFocusedElement(), loginListbox, "listbox is focused next");
|
||||||
|
|
||||||
await tab();
|
await tab();
|
||||||
is(getFocusedEl(), createButton, "create button is after");
|
is(getFocusedElement(), createButton, "create button is after");
|
||||||
|
|
||||||
await tab();
|
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 loginList = content.document.querySelector("login-list");
|
||||||
let loginItem = content.document.querySelector("login-item");
|
let loginItem = content.document.querySelector("login-item");
|
||||||
|
let loginFilter = content.document.querySelector("login-filter");
|
||||||
let createButton = loginList.shadowRoot.querySelector(
|
let createButton = loginList.shadowRoot.querySelector(
|
||||||
".create-login-button"
|
".create-login-button"
|
||||||
);
|
);
|
||||||
let editButton = loginItem.shadowRoot.querySelector(".edit-button");
|
let editButton = loginItem.shadowRoot.querySelector(".edit-button");
|
||||||
let breachAlert = loginItem.shadowRoot.querySelector(".breach-alert");
|
let breachAlert = loginItem.shadowRoot.querySelector(".breach-alert");
|
||||||
let getFocusedEl = () => {
|
let getFocusedElement = () => {
|
||||||
if (content.document.activeElement == loginList) {
|
if (content.document.activeElement == loginList) {
|
||||||
return loginList.shadowRoot.activeElement;
|
return loginList.shadowRoot.activeElement;
|
||||||
}
|
}
|
||||||
if (content.document.activeElement == loginItem) {
|
if (content.document.activeElement == loginItem) {
|
||||||
return loginItem.shadowRoot.activeElement;
|
return loginItem.shadowRoot.activeElement;
|
||||||
}
|
}
|
||||||
|
if (content.document.activeElement == loginFilter) {
|
||||||
|
return loginFilter.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
ok(
|
ok(
|
||||||
false,
|
false,
|
||||||
"not expecting a different element to get focused in this test: " +
|
"not expecting a different element to get focused in this test: " +
|
||||||
|
|
@ -198,11 +269,11 @@ add_task(async function testTabToEditButton() {
|
||||||
|
|
||||||
createButton.focus();
|
createButton.focus();
|
||||||
await waitForAnimationFrame();
|
await waitForAnimationFrame();
|
||||||
is(getFocusedEl(), createButton, "create button is focused");
|
is(getFocusedElement(), createButton, "create button is focused");
|
||||||
|
|
||||||
await tab();
|
await tab();
|
||||||
await waitForAnimationFrame();
|
await waitForAnimationFrame();
|
||||||
is(getFocusedEl(), editButton, "edit button is focused");
|
is(getFocusedElement(), editButton, "edit button is focused");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue