js: Add keyboard navigation for menus

Once the input focus is in the menu system (for instance by Shift+Tab
from the puzzle), you can move left and right through the menu bar and
up and down within each menu.  Enter selects a menu item.  The current
menu item is tracked by giving it the input focus.
This commit is contained in:
Ben Harris
2022-11-12 23:39:05 +00:00
parent b1b2da9896
commit 52cd58043a
3 changed files with 97 additions and 5 deletions

View File

@ -111,6 +111,7 @@ mergeInto(LibraryManager.library, {
var tick = document.createElement("span"); var tick = document.createElement("span");
tick.className = "tick"; tick.className = "tick";
label.appendChild(tick); label.appendChild(tick);
label.tabIndex = 0;
label.appendChild(document.createTextNode(" " + name)); label.appendChild(document.createTextNode(" " + name));
item.appendChild(label); item.appendChild(label);
var submenu = document.createElement("ul"); var submenu = document.createElement("ul");

View File

@ -416,6 +416,95 @@ function initPuzzle() {
gametypesubmenus.push(gametypelist); gametypesubmenus.push(gametypelist);
menuform = document.getElementById("gamemenu"); menuform = document.getElementById("gamemenu");
// Find the next or previous item in a menu, or null if there
// isn't one. Skip list items that don't have a child (i.e.
// separators) or whose child is disabled.
function isuseful(item) {
return item.querySelector(":scope > :not(:disabled)");
}
function nextmenuitem(item) {
do item = item.nextElementSibling;
while (item !== null && !isuseful(item));
return item;
}
function prevmenuitem(item) {
do item = item.previousElementSibling;
while (item !== null && !isuseful(item));
return item;
}
function firstmenuitem(menu) {
var item = menu && menu.firstElementChild;
while (item !== null && !isuseful(item))
item = item.nextElementSibling;
return item;
}
function lastmenuitem(menu) {
var item = menu && menu.lastElementChild;
while (item !== null && !isuseful(item))
item = item.previousElementSibling;
return item;
}
// Keyboard handlers for the menus.
function menukey(event) {
var thisitem = event.target.closest("li");
var thismenu = thisitem.closest("ul");
var targetitem = null;
var parentitem;
var parentitem_up = null;
var parentitem_sideways = null;
var submenu;
function ishorizontal(menu) {
// Which direction does this menu go in?
var cs = window.getComputedStyle(menu);
return cs.display == "flex" && cs.flexDirection == "row";
}
if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Enter"]
.includes(event.key))
return;
if (ishorizontal(thismenu)) {
// Top-level menu bar.
if (event.key == "ArrowLeft")
targetitem = prevmenuitem(thisitem) || lastmenuitem(thismenu);
else if (event.key == "ArrowRight")
targetitem = nextmenuitem(thisitem) || firstmenuitem(thismenu);
else if (event.key == "ArrowUp")
targetitem = lastmenuitem(thisitem.querySelector("ul"));
else if (event.key == "ArrowDown" || event.key == "Enter")
targetitem = firstmenuitem(thisitem.querySelector("ul"));
} else {
// Ordinary vertical menu.
parentitem = thismenu.closest("li");
if (parentitem) {
if (ishorizontal(parentitem.closest("ul")))
parentitem_up = parentitem;
else
parentitem_sideways = parentitem;
}
if (event.key == "ArrowUp")
targetitem = prevmenuitem(thisitem) || parentitem_up ||
lastmenuitem(thismenu);
else if (event.key == "ArrowDown")
targetitem = nextmenuitem(thisitem) || parentitem_up ||
firstmenuitem(thismenu);
else if (event.key == "ArrowRight")
targetitem = thisitem.querySelector("li") ||
(parentitem_up && nextmenuitem(parentitem_up));
else if (event.key == "Enter")
targetitem = thisitem.querySelector("li");
else if (event.key == "ArrowLeft")
targetitem = parentitem_sideways ||
(parentitem_up && prevmenuitem(parentitem_up));
}
if (targetitem)
targetitem.firstElementChild.focus();
else if (event.key == "Enter")
event.target.click();
// Prevent default even if we didn't do anything, as long as this
// was an interesting key.
event.preventDefault();
}
menuform.addEventListener("keydown", menukey);
// In IE, the canvas doesn't automatically gain focus on a mouse // In IE, the canvas doesn't automatically gain focus on a mouse
// click, so make sure it does // click, so make sure it does
onscreen_canvas.addEventListener("mousedown", function(event) { onscreen_canvas.addEventListener("mousedown", function(event) {

View File

@ -127,7 +127,8 @@ EOF
color: rgba(0,0,0,0.5); color: rgba(0,0,0,0.5);
} }
#gamemenu li > :hover:not(:disabled) { #gamemenu li > :hover:not(:disabled),
#gamemenu li > :focus-within {
/* When the mouse is over a menu item, highlight it */ /* When the mouse is over a menu item, highlight it */
background: rgba(0,0,0,0.3); background: rgba(0,0,0,0.3);
} }
@ -184,7 +185,8 @@ EOF
left: inherit; right: 100%; left: inherit; right: 100%;
} }
#gamemenu :hover > ul { #gamemenu :hover > ul,
#gamemenu :focus-within > ul {
/* Last but by no means least, the all-important line that makes /* Last but by no means least, the all-important line that makes
* submenus be displayed! Any <ul> whose parent <li> is being * submenus be displayed! Any <ul> whose parent <li> is being
* hovered over gets display:flex overriding the display:none * hovered over gets display:flex overriding the display:none
@ -309,13 +311,13 @@ ${unfinishedpara}
<hr> <hr>
<div id="puzzle" style="display: none"> <div id="puzzle" style="display: none">
<form id="gamemenu"><ul> <form id="gamemenu"><ul>
<li><div>Game...<ul> <li><div tabindex="0">Game...<ul>
<li><button type="button" id="specific">Enter game ID</button></li> <li><button type="button" id="specific">Enter game ID</button></li>
<li><button type="button" id="random">Enter random seed</button></li> <li><button type="button" id="random">Enter random seed</button></li>
<li><button type="button" id="save">Download save file</button></li> <li><button type="button" id="save">Download save file</button></li>
<li><button type="button" id="load">Upload save file</button></li> <li><button type="button" id="load">Upload save file</button></li>
</ul></div></li> </ul></div></li>
<li><div>Type...<ul id="gametype"></ul></div></li> <li><div tabindex="0">Type...<ul role="menu" id="gametype"></ul></div></li>
<li role="separator"></li> <li role="separator"></li>
<li><button type="button" id="new"> <li><button type="button" id="new">
New<span class="verbiage"> game</span> New<span class="verbiage"> game</span>
@ -335,7 +337,7 @@ ${unfinishedpara}
</ul></form> </ul></form>
<div align=center> <div align=center>
<div id="resizable"> <div id="resizable">
<canvas id="puzzlecanvas" width="1px" height="1px" tabindex="1"> <canvas id="puzzlecanvas" width="1px" height="1px" tabindex="0">
</canvas> </canvas>
<div id="statusbarholder"> <div id="statusbarholder">
</div> </div>