shithub: puzzles

Download patch

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>