Skip to content
Snippets Groups Projects
Commit 8bc88f75 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms
Browse files

rework keyboard focus management for action menues, fixes #4641

Closes #4641

Merge request studip/studip!3457
parent de136ea2
No related branches found
No related tags found
No related merge requests found
......@@ -34,19 +34,4 @@
STUDIP.ActionMenu.closeAll();
}
});
// Close all action menus when the escape key is pressed and rotate through all its items
// when TAB or SHIFT + TAB is pressed.
$(document).on('keydown', function(event) {
if (event.key === 'Escape') {
STUDIP.ActionMenu.closeAll();
} else if (event.key === 'Tab') {
//Check if the focus is inside an action menu:
let menu = $(event.target).closest('.action-menu');
if (menu.hasClass('is-open') && STUDIP.ActionMenu.tabThroughItems(menu, event.shiftKey)) {
event.preventDefault();
}
}
});
}(jQuery));
......@@ -100,7 +100,7 @@ class ActionMenu
const additionalClasses = Object.values({ ...this.element[0].classList }).filter((item) => item != 'action-menu');
const menu_width = this.content.width();
const menu_height = this.content.height();
// Reposition the menu?
if (position) {
let parents = getScrollableParents(this.element, menu_width, menu_height);
......@@ -122,9 +122,28 @@ class ActionMenu
this.position = false;
}
}
this.attachEventHandlers();
this.update();
}
// Close all action menus when the escape key is pressed and rotate through all its items
// when TAB or SHIFT + TAB is pressed.
attachEventHandlers() {
this.menu[0].addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
this.close(true);
} else if (event.key === 'Tab' && this.is_open) {
this.tabThroughItems(event.shiftKey);
event.preventDefault();
} else if (event.key === 'Enter' && event.target.matches('label')) {
event.target.querySelector('button,input').click();
event.preventDefault();
}
});
}
toggleScrollHandler(active) {
if (ActionMenu.scrollHandlerState === active) {
return;
......@@ -170,19 +189,23 @@ class ActionMenu
/**
* Toggle the menus display state. Pass a state to enforce it.
*/
toggle(state = null) {
toggle(state = null, focus = false) {
this.is_open = state === null ? !this.is_open : state;
this.update();
if (this.is_open) {
this.reposition();
this.menu.find('.action-menu-icon').focus();
ActionMenu.openMenus.push(this);
} else {
ActionMenu.openMenus = ActionMenu.openMenus.filter(menu => menu !== this);
}
// Always focus the toggle element
if (this.is_open || focus) {
this.menu.find('.action-menu-icon').focus();
}
this.toggleScrollHandler(ActionMenu.openMenus.filter(menu => menu.position).length > 0);
}
......@@ -237,36 +260,28 @@ class ActionMenu
* Handles the rotation through the action menu items when the first
* or last element of the menu has been reached.
*
* @param menu The menu whose items shall be rotated through.
*
* @param reverse Whether to rotate in reverse (true) or not (false).
* Defaults to false.
*/
static tabThroughItems(menu, reverse = false) {
if (reverse) {
//Put the focus on the last link in the menu, if the first link has the focus:
if (jQuery(menu).find('a:first:focus').length > 0) {
//Put the focus on the action menu button:
jQuery(menu).find('button.action-menu-icon').focus();
return true;
} else if (jQuery(menu).find('button.action-menu-icon:focus').length > 0) {
//Put the focus on the last action menu item:
jQuery(menu).find('a:last').focus();
return true;
}
} else {
//Put the focus on the first link in the menu, if the last link has the focus:
if (jQuery(menu).find('a:last:focus').length > 0) {
//Put the focus on the action menu button:
jQuery(menu).find('button.action-menu-icon').focus();
return true;
} else if (jQuery(menu).find('button.action-menu-icon:focus').length > 0) {
//Put the focus on the first action menu item:
jQuery(menu).find('a:first').focus();
return true;
}
tabThroughItems(reverse = false) {
const items = Array.from(this.menu[0].querySelectorAll([
'.action-menu-icon',
'.action-menu-item:not(.action-menu-item-disabled) a',
'.action-menu-item:not(.action-menu-item-disabled) button',
'.action-menu-item:not(.action-menu-item-disabled) label',
].join(',')));
// Get index of currently focussed element
let index = items.findIndex(element => element === document.activeElement);
if (index === -1) {
index = 0;
}
return false;
// Get new index based on direction
index = (index + (reverse ? -1 : 1) + items.length) % items.length;
// Focus element
items[index].focus();
}
}
......
......@@ -33,7 +33,7 @@
<span v-else class="action-menu-no-icon"></span>
{{ item.label }}
</a>
<label v-else-if="item.icon" class="undecorated" v-on="linkEvents(item)">
<label v-else-if="item.icon" class="undecorated" v-on="linkEvents(item)" tabindex="0">
<studip-icon :shape="item.icon"
:name="item.name"
class="action-menu-item-icon"
......
......@@ -49,7 +49,7 @@
</a>
<? elseif ($action['type'] === 'button'): ?>
<? if ($action['icon']): ?>
<label class="undecorated">
<label class="undecorated" tabindex="0">
<?= $action['icon']->asInput(false, $action['attributes'] + [
'class' => 'action-menu-item-icon',
'name' => $action['name'],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment