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 @@ ...@@ -34,19 +34,4 @@
STUDIP.ActionMenu.closeAll(); 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)); }(jQuery));
...@@ -122,9 +122,28 @@ class ActionMenu ...@@ -122,9 +122,28 @@ class ActionMenu
this.position = false; this.position = false;
} }
} }
this.attachEventHandlers();
this.update(); 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) { toggleScrollHandler(active) {
if (ActionMenu.scrollHandlerState === active) { if (ActionMenu.scrollHandlerState === active) {
return; return;
...@@ -170,19 +189,23 @@ class ActionMenu ...@@ -170,19 +189,23 @@ class ActionMenu
/** /**
* Toggle the menus display state. Pass a state to enforce it. * 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.is_open = state === null ? !this.is_open : state;
this.update(); this.update();
if (this.is_open) { if (this.is_open) {
this.reposition(); this.reposition();
this.menu.find('.action-menu-icon').focus();
ActionMenu.openMenus.push(this); ActionMenu.openMenus.push(this);
} else { } else {
ActionMenu.openMenus = ActionMenu.openMenus.filter(menu => menu !== this); 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); this.toggleScrollHandler(ActionMenu.openMenus.filter(menu => menu.position).length > 0);
} }
...@@ -237,36 +260,28 @@ class ActionMenu ...@@ -237,36 +260,28 @@ class ActionMenu
* Handles the rotation through the action menu items when the first * Handles the rotation through the action menu items when the first
* or last element of the menu has been reached. * 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). * @param reverse Whether to rotate in reverse (true) or not (false).
* Defaults to false. * Defaults to false.
*/ */
static tabThroughItems(menu, reverse = false) { tabThroughItems(reverse = false) {
if (reverse) { const items = Array.from(this.menu[0].querySelectorAll([
//Put the focus on the last link in the menu, if the first link has the focus: '.action-menu-icon',
if (jQuery(menu).find('a:first:focus').length > 0) { '.action-menu-item:not(.action-menu-item-disabled) a',
//Put the focus on the action menu button: '.action-menu-item:not(.action-menu-item-disabled) button',
jQuery(menu).find('button.action-menu-icon').focus(); '.action-menu-item:not(.action-menu-item-disabled) label',
return true; ].join(',')));
} else if (jQuery(menu).find('button.action-menu-icon:focus').length > 0) {
//Put the focus on the last action menu item: // Get index of currently focussed element
jQuery(menu).find('a:last').focus(); let index = items.findIndex(element => element === document.activeElement);
return true; if (index === -1) {
} index = 0;
} 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;
}
} }
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 @@ ...@@ -33,7 +33,7 @@
<span v-else class="action-menu-no-icon"></span> <span v-else class="action-menu-no-icon"></span>
{{ item.label }} {{ item.label }}
</a> </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" <studip-icon :shape="item.icon"
:name="item.name" :name="item.name"
class="action-menu-item-icon" class="action-menu-item-icon"
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
</a> </a>
<? elseif ($action['type'] === 'button'): ?> <? elseif ($action['type'] === 'button'): ?>
<? if ($action['icon']): ?> <? if ($action['icon']): ?>
<label class="undecorated"> <label class="undecorated" tabindex="0">
<?= $action['icon']->asInput(false, $action['attributes'] + [ <?= $action['icon']->asInput(false, $action['attributes'] + [
'class' => 'action-menu-item-icon', 'class' => 'action-menu-item-icon',
'name' => $action['name'], 'name' => $action['name'],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment