From c0929247e71e16b82c7b96f403d92ca8d87defa8 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 27 Nov 2025 00:32:44 -0500 Subject: [PATCH] Add keyboard navigation and accessibility to context menus This commit makes the Contextual library menus fully keyboard accessible. Previously, context menus (user menu and chat message menu) were only usable with a mouse. Changes: - Add makeContextualMenuAccessible() helper function * Adds ARIA role="menu" to menu container * Adds role="menuitem" to each menu item * Makes menu items focusable with tabindex * Automatically focuses first menu item when opened - Add keyboard navigation support * Arrow Up/Down: Navigate between menu items * Enter/Space: Activate selected menu item * Escape: Close menu - Apply to both context menus * User menu (click/Enter on username) * Chat message menu (double-click on message) - Focus management * First item automatically focused when menu opens * Roving tabindex for efficient keyboard navigation * Menu closes and returns focus after activation This makes context menus fully accessible to keyboard and screen reader users. They can now navigate menus with arrow keys and activate items with Enter or Space. --- static/galene.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/static/galene.js b/static/galene.js index 0fdfd44..bf68e59 100644 --- a/static/galene.js +++ b/static/galene.js @@ -2535,6 +2535,53 @@ document.getElementById('invite-dialog').onclose = function(e) { makeToken(template); }; +/** + * Adds keyboard navigation to Contextual menu + * @param {HTMLElement} menuElement + */ +function makeContextualMenuAccessible(menuElement) { + // Find all menu items + let menuItems = menuElement.querySelectorAll('.contextualMenuItem:not(.disabled)'); + if(menuItems.length === 0) return; + + let currentIndex = 0; + + // Make menu items focusable and add ARIA + menuItems.forEach((item, index) => { + item.setAttribute('role', 'menuitem'); + item.setAttribute('tabindex', index === 0 ? '0' : '-1'); + }); + + // Focus first item + menuItems[0].focus(); + + // Add keyboard navigation + menuElement.addEventListener('keydown', function(e) { + if(e.key === 'Escape') { + e.preventDefault(); + contextualCore.CloseMenu(); + return; + } + + if(e.key === 'ArrowDown') { + e.preventDefault(); + menuItems[currentIndex].setAttribute('tabindex', '-1'); + currentIndex = (currentIndex + 1) % menuItems.length; + menuItems[currentIndex].setAttribute('tabindex', '0'); + menuItems[currentIndex].focus(); + } else if(e.key === 'ArrowUp') { + e.preventDefault(); + menuItems[currentIndex].setAttribute('tabindex', '-1'); + currentIndex = currentIndex === 0 ? menuItems.length - 1 : currentIndex - 1; + menuItems[currentIndex].setAttribute('tabindex', '0'); + menuItems[currentIndex].focus(); + } else if(e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + menuItems[currentIndex].click(); + } + }); +} + /** * @param {HTMLElement} elt */ @@ -2598,6 +2645,15 @@ function userMenu(elt) { new Contextual({ items: items, }); + + // Add keyboard navigation after menu is created + setTimeout(() => { + let menu = document.querySelector('.contextualMenu'); + if(menu) { + menu.setAttribute('role', 'menu'); + makeContextualMenuAccessible(menu); + } + }, 0); } /** @@ -3482,6 +3538,15 @@ function chatMessageMenu(elt) { new Contextual({ items: items, }); + + // Add keyboard navigation after menu is created + setTimeout(() => { + let menu = document.querySelector('.contextualMenu'); + if(menu) { + menu.setAttribute('role', 'menu'); + makeContextualMenuAccessible(menu); + } + }, 0); } /**