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.
This commit is contained in:
Storm Dragon
2025-11-27 00:32:44 -05:00
parent 3c58e13c11
commit c0929247e7
+65
View File
@@ -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);
}
/**