Commit 79864ef2 authored by Andrew Nicols's avatar Andrew Nicols
Browse files

MDL-40975 ActionMenu: Ensure we maintain consistent tab order

When the actionmenu is open, we should focus on it's first element, and
then naturally tab through it. Tabbing from the end should take us to the
first element after the menu button. This may not be the same as the first
element after the menu itself because of the nature of primary and
secondary action links. We also need to shift-tab back in the same manner.
parent 7f4f7081
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -9,8 +9,11 @@ var BODY = Y.one(Y.config.doc.body),
MENUSHOWN : 'action-menu-shown'
},
SELECTOR = {
CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
MENU : '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
MENUCONTENT : '.menu[data-rel=menu-content]',
MENUCONTENTCHILD: 'li a',
MENUCHILD: '.menu li a',
TOGGLE : '.toggle-display'
},
ACTIONMENU,
......@@ -57,6 +60,15 @@ ACTIONMENU.prototype = {
*/
owner : null,
/**
* The menu button that toggles this open.
*
* @property menulink
* @type Node
* @protected
*/
menulink: null,
/**
* Called during the initialisation process of the object.
* @method initializer
......@@ -102,16 +114,21 @@ ACTIONMENU.prototype = {
this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true);
this.dialogue = null;
}
if (this.owner) {
this.owner.removeClass(CSS.MENUSHOWN);
this.owner = null;
}
for (var i in this.events) {
if (this.events[i].detach) {
this.events[i].detach();
}
}
this.events = [];
if (this.owner) {
this.owner.removeClass(CSS.MENUSHOWN);
this.owner = null;
}
if (this.menulink) {
this.menulink.focus();
this.menulink = null;
}
},
/**
......@@ -130,13 +147,49 @@ ACTIONMENU.prototype = {
// The menu was visible and the user has clicked to toggle it again.
return;
}
this.showMenu(menu);
this.showMenu(e, menu);
// Close the menu if the user presses escape.
this.events.push(BODY.on('key', this.hideMenu, 'esc', this));
// Close the menu if the user clicks outside the menu.
this.events.push(BODY.on('click', this.hideIfOutside, this));
// Close the menu if the user focuses outside the menu.
this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this));
// Check tabbing.
this.events.push(menu.delegate('key', this.checkFocus, 'down:9', SELECTOR.MENUCHILD, this));
},
/**
* Check current focus when moving around with the tab key.
* This will ensure that when the etreme menu items are reached, the
* menu is closed and the next DOM element is focused.
*
* @method checkFocus
* @param {EventFacade} e The key event
*/
checkFocus: function(e) {
var nodelist = this.dialogue.all(SELECTOR.MENUCHILD),
firstNode,
lastNode;
if (nodelist) {
firstNode = nodelist.item(0);
lastNode = nodelist.pop();
}
var menulink = this.menulink;
if (e.target === firstNode && e.shiftKey) {
this.hideMenu();
e.preventDefault();
} else if (e.target === lastNode && !e.shiftKey) {
var next;
if (this.hideMenu()) {
next = menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR);
if (next) {
next.focus();
}
}
}
},
/**
......@@ -154,21 +207,31 @@ ACTIONMENU.prototype = {
/**
* Displays the menu with the given content and alignment.
* @param {EventFacade} e
* @param {Node} menu
* @param Array align
* @returns {M.core.dialogue|dialogue}
*/
showMenu : function(menu) {
showMenu : function(e, menu) {
Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME);
var ownerselector = menu.getData('owner'),
menucontent = menu.one(SELECTOR.MENUCONTENT);
menucontent = menu.one(SELECTOR.MENUCONTENT),
menuchild;
this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null;
this.dialogue = menu;
menu.addClass('show');
if (this.owner) {
this.owner.addClass(CSS.MENUSHOWN);
this.menulink = this.owner.one(SELECTOR.TOGGLE);
}
this.constrain(menucontent.set('aria-hidden', false));
if (e.type && e.type === 'key') {
menuchild = menucontent.one(SELECTOR.MENUCONTENTCHILD);
if (menuchild) {
menuchild.focus();
}
}
return true;
},
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment