ref: 52cd58043ac144eeafb92fd962662420506283c1
parent: b1b2da98961c3ec6561a934834026c117f4366d3
author: Ben Harris <bjh21@bjh21.me.uk>
date: Sat Nov 12 18:39:05 EST 2022
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.
--- a/emcclib.js
+++ b/emcclib.js
@@ -111,6 +111,7 @@
var tick = document.createElement("span");
tick.className = "tick";
label.appendChild(tick);
+ label.tabIndex = 0;
label.appendChild(document.createTextNode(" " + name));
item.appendChild(label);
var submenu = document.createElement("ul");
--- a/emccpre.js
+++ b/emccpre.js
@@ -416,6 +416,95 @@
gametypesubmenus.push(gametypelist);
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
// click, so make sure it does
onscreen_canvas.addEventListener("mousedown", function(event) {
--- a/html/jspage.pl
+++ b/html/jspage.pl
@@ -127,7 +127,8 @@
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 */
background: rgba(0,0,0,0.3);
}
@@ -184,7 +185,8 @@
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
* submenus be displayed! Any <ul> whose parent <li> is being
* hovered over gets display:flex overriding the display:none
@@ -309,13 +311,13 @@
<hr>
<div id="puzzle" style="display: none">
<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="random">Enter random seed</button></li>
<li><button type="button" id="save">Download save file</button></li>
<li><button type="button" id="load">Upload save file</button></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><button type="button" id="new">
New<span class="verbiage"> game</span>
@@ -335,7 +337,7 @@
</ul></form>
<div align=center>
<div id="resizable">
- <canvas id="puzzlecanvas" width="1px" height="1px" tabindex="1">
+ <canvas id="puzzlecanvas" width="1px" height="1px" tabindex="0">
</canvas>
<div id="statusbarholder">
</div>