Bug 1560359 - Add keyboard support for the menu on about:logins. r=sfoster,yzen

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2019-07-03 03:34:39 +00:00
Родитель 192e63f9dd
Коммит a4cce13895
3 изменённых файлов: 87 добавлений и 25 удалений

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

@ -149,9 +149,7 @@
<ul class="menu" role="menu" hidden>
<button role="menuitem" class="menuitem-button menuitem-import alternate-button" hidden data-supported-platforms="Win32" data-event-name="AboutLoginsImport"></button>
<button role="menuitem" class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences"></button>
</li>
<li role="menuitem" class="menuitem">
<button class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
<button role="menuitem" class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
</ul>
</template>

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

@ -24,7 +24,9 @@ export default class MenuButton extends ReflectedFluentElement {
this._menu = this.shadowRoot.querySelector(".menu");
this._menuButton = this.shadowRoot.querySelector(".menu-button");
this.addEventListener("blur", this);
this._menuButton.addEventListener("click", this);
this.addEventListener("keydown", this, true);
super.connectedCallback();
}
@ -54,6 +56,15 @@ export default class MenuButton extends ReflectedFluentElement {
handleEvent(event) {
switch (event.type) {
case "blur": {
if (event.relatedTarget &&
event.relatedTarget.closest(".menu") == this._menu) {
// Only hide the menu if focus has left the menu-button.
return;
}
this._hideMenu();
break;
}
case "click": {
// Skip the catch-all event listener if it was the menu-button
// that was clicked on.
@ -76,9 +87,40 @@ export default class MenuButton extends ReflectedFluentElement {
this._toggleMenu();
break;
}
case "keydown": {
this._handleKeyDown(event);
}
}
}
_handleKeyDown(event) {
if (event.key == "Enter") {
event.preventDefault();
this._toggleMenu();
} else if (event.key == "Escape") {
this._hideMenu();
this._menuButton.focus();
}
if (!event.key.startsWith("Arrow")) {
return;
}
let activeMenuitem = this.shadowRoot.activeElement ||
this._menu.querySelector(".menuitem-button:not([hidden])");
let newlyFocusedItem = null;
if (event.key == "ArrowDown") {
newlyFocusedItem = activeMenuitem.nextElementSibling;
} else if (event.key == "ArrowUp") {
newlyFocusedItem = activeMenuitem.previousElementSibling;
}
if (!newlyFocusedItem) {
return;
}
newlyFocusedItem.focus();
}
_hideMenu() {
this._menu.hidden = true;
document.documentElement.removeEventListener("click", this, true);

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

@ -48,36 +48,58 @@ add_task(async function test_menu_open_close() {
is(true, menu.hidden, "menu should be hidden before pressing 'space'");
sendKey("SPACE");
await new Promise(resolve => requestAnimationFrame(resolve));
is(false, menu.hidden, "menu should be visible after pressing 'space'");
ok(!menu.hidden, "menu should be visible after pressing 'space'");
sendKey("ESCAPE");
await new Promise(resolve => requestAnimationFrame(resolve));
ok(menu.hidden, "menu should be hidden after pressing 'escape'");
is(gMenuButton.shadowRoot.activeElement, gMenuButton.shadowRoot.querySelector(".menu-button"),
"the .menu-button should be focused after closing the menu via keyboard");
sendKey("RETURN");
await new Promise(resolve => requestAnimationFrame(resolve));
ok(!menu.hidden, "menu should be visible after pressing 'return'");
let firstVisibleItem = gMenuButton.shadowRoot.querySelector(".menuitem-button:not([hidden])");
ok(!firstVisibleItem.matches(":focus"), "the first item should not be focused before tabbing to it");
sendKey("TAB");
await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
"waiting for firstVisibleItem to get focus");
ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it");
synthesizeKey("VK_TAB", { shiftKey: true });
await SimpleTest.promiseWaitForCondition(() => !firstVisibleItem.matches(":focus"),
"waiting for firstVisibleItem to lose focus");
ok(!firstVisibleItem.matches(":focus"), "firstVisibleItem should lose focus after tabbing away from it");
sendKey("TAB");
await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
"waiting for firstVisibleItem to get focus again");
ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it again");
if (navigator.platform == "Win32") {
// The Import menuitem is only visible on Windows, where we will need another Tab
// press to get to the Preferences item.
let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
sendKey("DOWN");
await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
"waiting for preferencesItem to gain focus");
ok(preferencesItem.matches(":focus"), `.menuitem-preferences should be now be focused (DOWN)`);
sendKey("UP");
await SimpleTest.promiseWaitForCondition(() => !preferencesItem.matches(":focus"),
`waiting for preferencesItem to lose focus (UP)`);
ok(!preferencesItem.matches(":focus"), `.menuitem-preferences should lose focus after pressing up`);
let feedbackItem = gMenuButton.shadowRoot.querySelector(".menuitem-feedback");
ok(!feedbackItem.matches(":focus"), ".menuitem-feedback should not be focused before tabbing to it");
// The Import menuitem is only visible on Windows, where we will need a second Tab
// press to get to the Feedback item.
let tabs = navigator.platform == "Win32" ? 2 : 1;
while (tabs--) {
sendKey("TAB");
await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
"waiting for preferencesItem to get focus");
ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
}
await SimpleTest.promiseWaitForCondition(() => feedbackItem.matches(":focus"),
"waiting for feedbackItem to get focus");
ok(feedbackItem.matches(":focus"), ".menuitem-feedback should be focused after tabbing to it");
let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
ok(!preferencesItem.matches(":focus"), ".menuitem-preferences should not be focused before tabbing to it");
// We will need a third Tab press to get to the Preferences item.
sendKey("TAB");
await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
"waiting for preferencesItem to get focus");
ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
let openPreferencesEvent = null;
is(false, menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
ok(!menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
window.addEventListener("AboutLoginsOpenPreferences", event => openPreferencesEvent = event);
sendKey("SPACE");
ok(openPreferencesEvent, "AboutLoginsOpenPreferences event should be dispatched after pressing 'space' on .menuitem-preferences");
is(true, menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
ok(menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
});
</script>