From 52cd58043ac144eeafb92fd962662420506283c1 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Sat, 12 Nov 2022 23:39:05 +0000 Subject: [PATCH] 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. --- emcclib.js | 1 + emccpre.js | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ html/jspage.pl | 12 ++++--- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/emcclib.js b/emcclib.js index 47963bb..5c1d039 100644 --- a/emcclib.js +++ b/emcclib.js @@ -111,6 +111,7 @@ mergeInto(LibraryManager.library, { 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"); diff --git a/emccpre.js b/emccpre.js index 89954b6..2e1508c 100644 --- a/emccpre.js +++ b/emccpre.js @@ -416,6 +416,95 @@ function initPuzzle() { 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) { diff --git a/html/jspage.pl b/html/jspage.pl index f3b0df5..ec67a4d 100755 --- a/html/jspage.pl +++ b/html/jspage.pl @@ -127,7 +127,8 @@ EOF 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 @@ EOF 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