MDL-42796 ActionMenu: Add additional keyboard event handling

Add additional handling for up/down/escape key presses and consolidate them
with the existing tab/shift+tab handling.
This commit is contained in:
Andrew Nicols
2013-12-06 16:20:10 +08:00
parent a4e1fefa39
commit 11dd4dadb4
9 changed files with 751 additions and 114 deletions
+2 -6
View File
@@ -1099,24 +1099,20 @@ class core_renderer extends renderer_base {
foreach ($menu->get_primary_actions($this) as $action) {
if ($action instanceof renderable) {
$content = $this->render($action);
$role = 'presentation';
} else {
$content = $action;
$role = 'menuitem';
}
$output .= html_writer::tag('li', $content, array('role' => $role));
$output .= html_writer::tag('li', $content, array('role' => 'presentation'));
}
$output .= html_writer::end_tag('ul');
$output .= html_writer::start_tag('ul', $menu->attributessecondary);
foreach ($menu->get_secondary_actions() as $action) {
if ($action instanceof renderable) {
$content = $this->render($action);
$role = 'presentation';
} else {
$content = $action;
$role = 'menuitem';
}
$output .= html_writer::tag('li', $content, array('role' => $role));
$output .= html_writer::tag('li', $content, array('role' => 'presentation'));
}
$output .= html_writer::end_tag('ul');
$output .= html_writer::end_tag('div');
@@ -13,11 +13,21 @@ var BODY = Y.one(Y.config.doc.body),
SELECTOR = {
CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
MENU : '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
MENUBAR: '[role="menubar"]',
MENUITEM: '[role="menuitem"]',
MENUCONTENT : '.menu[data-rel=menu-content]',
MENUCONTENTCHILD: 'li a',
MENUCHILD: '.menu li a',
TOGGLE : '.toggle-display',
KEEPOPEN: '[data-keepopen="1"]'
KEEPOPEN: '[data-keepopen="1"]',
MENUBARITEMS: [
'[role="menubar"] > [role="menuitem"]',
'[role="menubar"] > [role="presentation"] > [role="menuitem"]'
],
MENUITEMS: [
'> [role="menuitem"]',
'> [role="presentation"] > [role="menuitem"]'
]
},
ACTIONMENU,
ALIGN = {
@@ -72,6 +82,33 @@ ACTIONMENU.prototype = {
*/
menulink: null,
/**
* The set of menu nodes.
*
* @property menuChildren
* @type NodeList
* @protected
*/
menuChildren: null,
/**
* The first menu item.
*
* @property firstMenuChild
* @type Node
* @protected
*/
firstMenuChild: null,
/**
* The last menu item.
*
* @property lastMenuChild
* @type Node
* @protected
*/
lastMenuChild: null,
/**
* Called during the initialisation process of the object.
* @method initializer
@@ -79,8 +116,16 @@ ACTIONMENU.prototype = {
initializer : function() {
Y.log('Initialising the action menu manager', 'debug', ACTIONMENU.NAME);
Y.all(SELECTOR.MENU).each(this.enhance, this);
BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this);
BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.toggleMenu, 'enter,space', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
// Ensure that we toggle on menuitems when the spacebar is pressed.
BODY.delegate('key', function(e) {
e.currentTarget.simulate('click');
e.preventDefault();
}, 'down:32', SELECTOR.MENUBARITEMS.join(','));
},
/**
@@ -106,6 +151,93 @@ ACTIONMENU.prototype = {
}
},
/**
* Handle movement between menu items in a menubar.
*
* @method moveMenuItem
* @param {EventFacade} e The event generating the move request
* @chainable
*/
moveMenuItem: function(e) {
var nextFocus,
menuitem = e.target.ancestor(SELECTOR.MENUITEM, true);
if (e.keyCode === 37) {
nextFocus = this.getMenuItem(menuitem, true);
} else if (e.keyCode === 39) {
nextFocus = this.getMenuItem(menuitem);
}
if (nextFocus) {
nextFocus.focus();
}
return this;
},
/**
* Get the next menuitem in a menubar.
*
* @method getMenuItem
* @param {Node} currentItem The currently focused item in the menubar
* @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards
* @return {Node|null} The next item, or null if none was found
*/
getMenuItem: function(currentItem, previous) {
var menubar = currentItem.ancestor(SELECTOR.MENUBAR),
menuitems;
if (!menubar) {
return null;
}
menuitems = menubar.all(SELECTOR.MENUITEMS.join(','));
if (!menuitems) {
return null;
}
var childCount = menuitems.size();
if (childCount === 1) {
// Only one item, exit now because we should already be on it.
return null;
}
// Determine the next child.
var index = 0,
direction = 1,
checkCount = 0;
// Work out the index of the currently selected item.
for (index = 0; index < childCount; index++) {
if (menuitems.item(index) === currentItem) {
break;
}
}
// Check that the menu item was found - otherwise return null.
if (menuitems.item(index) !== currentItem) {
return null;
}
// Reverse the direction if we want the previous item.
if (previous) {
direction = -1;
}
do {
// Update the index in the direction of travel.
index += direction;
next = menuitems.item(index);
// Check that we don't loop multiple times.
checkCount++;
} while (next && next.hasAttribute('hidden'));
return next;
},
/**
* Hides the menu if it is visible.
* @method hideMenu
@@ -134,6 +266,17 @@ ACTIONMENU.prototype = {
}
},
showIfHidden: function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
if (!menuvisible) {
e.preventDefault();
this.showMenu(e, menu);
}
return this;
},
/**
* Toggles the display of the menu.
* @method toggleMenu
@@ -142,9 +285,8 @@ ACTIONMENU.prototype = {
toggleMenu : function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
// Stop all immediate propogation of the events we attach later will be
// triggered and the dialogue immediately hidden again.
e.halt(true);
e.preventDefault();
this.hideMenu();
if (menuvisible) {
// The menu was visible and the user has clicked to toggle it again.
@@ -154,35 +296,103 @@ ACTIONMENU.prototype = {
},
/**
* 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.
* Handle keyboard events when the menu is open. We respond to:
* * escape (exit)
* * tab (move to next menu item)
* * up/down (move to previous/next menu item)
*
* @method checkFocus
* @method handleKeyboardEvent
* @param {EventFacade} e The key event
*/
checkFocus: function(e) {
var nodelist = this.dialogue.all(SELECTOR.MENUCHILD),
firstNode,
lastNode;
handleKeyboardEvent: function(e) {
var next;
if (nodelist) {
firstNode = nodelist.item(0);
lastNode = nodelist.pop();
// Handle when the menu is still selected.
if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) {
if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) {
this.firstMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 38 && this.lastMenuChild) {
this.lastMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 9 && e.shiftKey) {
this.hideMenu();
e.preventDefault();
}
return this;
}
var menulink = this.menulink;
if (e.target === firstNode && e.shiftKey) {
if (e.keyCode === 27) {
// The escape key was pressed so close the menu.
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();
} else if (e.keyCode === 32) {
// The space bar was pressed. Trigger a click.
e.preventDefault();
e.currentTarget.simulate('click');
} else if (e.keyCode === 9) {
// The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options.
// We only override the Shift + Tab on the first option, and Tab on the last option to change where the focus is moved to.
if (e.target === this.firstMenuChild && e.shiftKey) {
this.hideMenu();
e.preventDefault();
} else if (e.target === this.lastMenuChild && !e.shiftKey) {
if (this.hideMenu()) {
// Determine the next selector and focus on it.
next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR);
if (next) {
next.focus();
}
}
}
} else if (e.keyCode === 38 || e.keyCode === 40) {
// The up (38) or down (40) key was pushed.
// On cursor moves we loops through the menu rather than exiting it as in the tab behaviour.
var found = false,
index = 0,
direction = 1,
checkCount = 0;
// Determine which menu item is currently selected.
while (!found && index < this.menuChildren.size()) {
if (this.menuChildren.item(index) === e.currentTarget) {
found = true;
} else {
index++;
}
}
if (!found) {
Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu');
return;
}
if (e.keyCode === 38) {
// Moving up so reverse the direction.
direction = -1;
}
// Try to find the next
do {
index += direction;
if (index < 0) {
index = this.menuChildren.size() - 1;
} else if (index >= this.menuChildren.size()) {
// Handle wrapping.
index = 0;
}
next = this.menuChildren.item(index);
// Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
checkCount++;
} while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden'));
if (next) {
next.focus();
e.preventDefault();
}
}
},
@@ -208,33 +418,37 @@ ACTIONMENU.prototype = {
showMenu : function(e, menu) {
Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME);
var ownerselector = menu.getData('owner'),
menucontent = menu.one(SELECTOR.MENUCONTENT),
menuchild;
menucontent = menu.one(SELECTOR.MENUCONTENT);
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);
} else {
this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true);
}
this.constrain(menucontent.set('aria-hidden', false));
if (e.type && e.type === 'key') {
menuchild = menucontent.one(SELECTOR.MENUCONTENTCHILD);
if (menuchild) {
menuchild.focus();
}
this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD);
if (this.menuChildren) {
this.firstMenuChild = this.menuChildren.item(0);
this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1);
this.firstMenuChild.focus();
}
// 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 keyboard changes.
this.events.push(menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this));
// Close the menu after a button was pushed.
this.events.push(menu.delegate('click', function(e) {
@@ -396,4 +610,4 @@ M.core.actionmenu.newDOMNode = function(node) {
};
}, '@VERSION@', {"requires": ["base", "event"]});
}, '@VERSION@', {"requires": ["base", "event", "node-event-simulate"]});
File diff suppressed because one or more lines are too long
+247 -34
View File
@@ -13,11 +13,21 @@ var BODY = Y.one(Y.config.doc.body),
SELECTOR = {
CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
MENU : '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
MENUBAR: '[role="menubar"]',
MENUITEM: '[role="menuitem"]',
MENUCONTENT : '.menu[data-rel=menu-content]',
MENUCONTENTCHILD: 'li a',
MENUCHILD: '.menu li a',
TOGGLE : '.toggle-display',
KEEPOPEN: '[data-keepopen="1"]'
KEEPOPEN: '[data-keepopen="1"]',
MENUBARITEMS: [
'[role="menubar"] > [role="menuitem"]',
'[role="menubar"] > [role="presentation"] > [role="menuitem"]'
],
MENUITEMS: [
'> [role="menuitem"]',
'> [role="presentation"] > [role="menuitem"]'
]
},
ACTIONMENU,
ALIGN = {
@@ -72,14 +82,49 @@ ACTIONMENU.prototype = {
*/
menulink: null,
/**
* The set of menu nodes.
*
* @property menuChildren
* @type NodeList
* @protected
*/
menuChildren: null,
/**
* The first menu item.
*
* @property firstMenuChild
* @type Node
* @protected
*/
firstMenuChild: null,
/**
* The last menu item.
*
* @property lastMenuChild
* @type Node
* @protected
*/
lastMenuChild: null,
/**
* Called during the initialisation process of the object.
* @method initializer
*/
initializer : function() {
Y.all(SELECTOR.MENU).each(this.enhance, this);
BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this);
BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.toggleMenu, 'enter,space', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
// Ensure that we toggle on menuitems when the spacebar is pressed.
BODY.delegate('key', function(e) {
e.currentTarget.simulate('click');
e.preventDefault();
}, 'down:32', SELECTOR.MENUBARITEMS.join(','));
},
/**
@@ -105,6 +150,93 @@ ACTIONMENU.prototype = {
}
},
/**
* Handle movement between menu items in a menubar.
*
* @method moveMenuItem
* @param {EventFacade} e The event generating the move request
* @chainable
*/
moveMenuItem: function(e) {
var nextFocus,
menuitem = e.target.ancestor(SELECTOR.MENUITEM, true);
if (e.keyCode === 37) {
nextFocus = this.getMenuItem(menuitem, true);
} else if (e.keyCode === 39) {
nextFocus = this.getMenuItem(menuitem);
}
if (nextFocus) {
nextFocus.focus();
}
return this;
},
/**
* Get the next menuitem in a menubar.
*
* @method getMenuItem
* @param {Node} currentItem The currently focused item in the menubar
* @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards
* @return {Node|null} The next item, or null if none was found
*/
getMenuItem: function(currentItem, previous) {
var menubar = currentItem.ancestor(SELECTOR.MENUBAR),
menuitems;
if (!menubar) {
return null;
}
menuitems = menubar.all(SELECTOR.MENUITEMS.join(','));
if (!menuitems) {
return null;
}
var childCount = menuitems.size();
if (childCount === 1) {
// Only one item, exit now because we should already be on it.
return null;
}
// Determine the next child.
var index = 0,
direction = 1,
checkCount = 0;
// Work out the index of the currently selected item.
for (index = 0; index < childCount; index++) {
if (menuitems.item(index) === currentItem) {
break;
}
}
// Check that the menu item was found - otherwise return null.
if (menuitems.item(index) !== currentItem) {
return null;
}
// Reverse the direction if we want the previous item.
if (previous) {
direction = -1;
}
do {
// Update the index in the direction of travel.
index += direction;
next = menuitems.item(index);
// Check that we don't loop multiple times.
checkCount++;
} while (next && next.hasAttribute('hidden'));
return next;
},
/**
* Hides the menu if it is visible.
* @method hideMenu
@@ -132,6 +264,17 @@ ACTIONMENU.prototype = {
}
},
showIfHidden: function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
if (!menuvisible) {
e.preventDefault();
this.showMenu(e, menu);
}
return this;
},
/**
* Toggles the display of the menu.
* @method toggleMenu
@@ -140,9 +283,8 @@ ACTIONMENU.prototype = {
toggleMenu : function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
// Stop all immediate propogation of the events we attach later will be
// triggered and the dialogue immediately hidden again.
e.halt(true);
e.preventDefault();
this.hideMenu();
if (menuvisible) {
// The menu was visible and the user has clicked to toggle it again.
@@ -152,35 +294,102 @@ ACTIONMENU.prototype = {
},
/**
* 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.
* Handle keyboard events when the menu is open. We respond to:
* * escape (exit)
* * tab (move to next menu item)
* * up/down (move to previous/next menu item)
*
* @method checkFocus
* @method handleKeyboardEvent
* @param {EventFacade} e The key event
*/
checkFocus: function(e) {
var nodelist = this.dialogue.all(SELECTOR.MENUCHILD),
firstNode,
lastNode;
handleKeyboardEvent: function(e) {
var next;
if (nodelist) {
firstNode = nodelist.item(0);
lastNode = nodelist.pop();
// Handle when the menu is still selected.
if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) {
if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) {
this.firstMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 38 && this.lastMenuChild) {
this.lastMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 9 && e.shiftKey) {
this.hideMenu();
e.preventDefault();
}
return this;
}
var menulink = this.menulink;
if (e.target === firstNode && e.shiftKey) {
if (e.keyCode === 27) {
// The escape key was pressed so close the menu.
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();
} else if (e.keyCode === 32) {
// The space bar was pressed. Trigger a click.
e.preventDefault();
e.currentTarget.simulate('click');
} else if (e.keyCode === 9) {
// The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options.
// We only override the Shift + Tab on the first option, and Tab on the last option to change where the focus is moved to.
if (e.target === this.firstMenuChild && e.shiftKey) {
this.hideMenu();
e.preventDefault();
} else if (e.target === this.lastMenuChild && !e.shiftKey) {
if (this.hideMenu()) {
// Determine the next selector and focus on it.
next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR);
if (next) {
next.focus();
}
}
}
} else if (e.keyCode === 38 || e.keyCode === 40) {
// The up (38) or down (40) key was pushed.
// On cursor moves we loops through the menu rather than exiting it as in the tab behaviour.
var found = false,
index = 0,
direction = 1,
checkCount = 0;
// Determine which menu item is currently selected.
while (!found && index < this.menuChildren.size()) {
if (this.menuChildren.item(index) === e.currentTarget) {
found = true;
} else {
index++;
}
}
if (!found) {
return;
}
if (e.keyCode === 38) {
// Moving up so reverse the direction.
direction = -1;
}
// Try to find the next
do {
index += direction;
if (index < 0) {
index = this.menuChildren.size() - 1;
} else if (index >= this.menuChildren.size()) {
// Handle wrapping.
index = 0;
}
next = this.menuChildren.item(index);
// Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
checkCount++;
} while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden'));
if (next) {
next.focus();
e.preventDefault();
}
}
},
@@ -205,33 +414,37 @@ ACTIONMENU.prototype = {
*/
showMenu : function(e, menu) {
var ownerselector = menu.getData('owner'),
menucontent = menu.one(SELECTOR.MENUCONTENT),
menuchild;
menucontent = menu.one(SELECTOR.MENUCONTENT);
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);
} else {
this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true);
}
this.constrain(menucontent.set('aria-hidden', false));
if (e.type && e.type === 'key') {
menuchild = menucontent.one(SELECTOR.MENUCONTENTCHILD);
if (menuchild) {
menuchild.focus();
}
this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD);
if (this.menuChildren) {
this.firstMenuChild = this.menuChildren.item(0);
this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1);
this.firstMenuChild.focus();
}
// 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 keyboard changes.
this.events.push(menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this));
// Close the menu after a button was pushed.
this.events.push(menu.delegate('click', function(e) {
@@ -392,4 +605,4 @@ M.core.actionmenu.newDOMNode = function(node) {
};
}, '@VERSION@', {"requires": ["base", "event"]});
}, '@VERSION@', {"requires": ["base", "event", "node-event-simulate"]});
+247 -33
View File
@@ -11,11 +11,21 @@ var BODY = Y.one(Y.config.doc.body),
SELECTOR = {
CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
MENU : '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]',
MENUBAR: '[role="menubar"]',
MENUITEM: '[role="menuitem"]',
MENUCONTENT : '.menu[data-rel=menu-content]',
MENUCONTENTCHILD: 'li a',
MENUCHILD: '.menu li a',
TOGGLE : '.toggle-display',
KEEPOPEN: '[data-keepopen="1"]'
KEEPOPEN: '[data-keepopen="1"]',
MENUBARITEMS: [
'[role="menubar"] > [role="menuitem"]',
'[role="menubar"] > [role="presentation"] > [role="menuitem"]'
],
MENUITEMS: [
'> [role="menuitem"]',
'> [role="presentation"] > [role="menuitem"]'
]
},
ACTIONMENU,
ALIGN = {
@@ -70,6 +80,33 @@ ACTIONMENU.prototype = {
*/
menulink: null,
/**
* The set of menu nodes.
*
* @property menuChildren
* @type NodeList
* @protected
*/
menuChildren: null,
/**
* The first menu item.
*
* @property firstMenuChild
* @type Node
* @protected
*/
firstMenuChild: null,
/**
* The last menu item.
*
* @property lastMenuChild
* @type Node
* @protected
*/
lastMenuChild: null,
/**
* Called during the initialisation process of the object.
* @method initializer
@@ -77,8 +114,16 @@ ACTIONMENU.prototype = {
initializer : function() {
Y.log('Initialising the action menu manager', 'debug', ACTIONMENU.NAME);
Y.all(SELECTOR.MENU).each(this.enhance, this);
BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this);
BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.toggleMenu, 'enter,space', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this);
// Ensure that we toggle on menuitems when the spacebar is pressed.
BODY.delegate('key', function(e) {
e.currentTarget.simulate('click');
e.preventDefault();
}, 'down:32', SELECTOR.MENUBARITEMS.join(','));
},
/**
@@ -104,6 +149,93 @@ ACTIONMENU.prototype = {
}
},
/**
* Handle movement between menu items in a menubar.
*
* @method moveMenuItem
* @param {EventFacade} e The event generating the move request
* @chainable
*/
moveMenuItem: function(e) {
var nextFocus,
menuitem = e.target.ancestor(SELECTOR.MENUITEM, true);
if (e.keyCode === 37) {
nextFocus = this.getMenuItem(menuitem, true);
} else if (e.keyCode === 39) {
nextFocus = this.getMenuItem(menuitem);
}
if (nextFocus) {
nextFocus.focus();
}
return this;
},
/**
* Get the next menuitem in a menubar.
*
* @method getMenuItem
* @param {Node} currentItem The currently focused item in the menubar
* @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards
* @return {Node|null} The next item, or null if none was found
*/
getMenuItem: function(currentItem, previous) {
var menubar = currentItem.ancestor(SELECTOR.MENUBAR),
menuitems;
if (!menubar) {
return null;
}
menuitems = menubar.all(SELECTOR.MENUITEMS.join(','));
if (!menuitems) {
return null;
}
var childCount = menuitems.size();
if (childCount === 1) {
// Only one item, exit now because we should already be on it.
return null;
}
// Determine the next child.
var index = 0,
direction = 1,
checkCount = 0;
// Work out the index of the currently selected item.
for (index = 0; index < childCount; index++) {
if (menuitems.item(index) === currentItem) {
break;
}
}
// Check that the menu item was found - otherwise return null.
if (menuitems.item(index) !== currentItem) {
return null;
}
// Reverse the direction if we want the previous item.
if (previous) {
direction = -1;
}
do {
// Update the index in the direction of travel.
index += direction;
next = menuitems.item(index);
// Check that we don't loop multiple times.
checkCount++;
} while (next && next.hasAttribute('hidden'));
return next;
},
/**
* Hides the menu if it is visible.
* @method hideMenu
@@ -132,6 +264,17 @@ ACTIONMENU.prototype = {
}
},
showIfHidden: function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
if (!menuvisible) {
e.preventDefault();
this.showMenu(e, menu);
}
return this;
},
/**
* Toggles the display of the menu.
* @method toggleMenu
@@ -140,9 +283,8 @@ ACTIONMENU.prototype = {
toggleMenu : function(e) {
var menu = e.target.ancestor(SELECTOR.MENU),
menuvisible = (menu.hasClass('show'));
// Stop all immediate propogation of the events we attach later will be
// triggered and the dialogue immediately hidden again.
e.halt(true);
e.preventDefault();
this.hideMenu();
if (menuvisible) {
// The menu was visible and the user has clicked to toggle it again.
@@ -152,35 +294,103 @@ ACTIONMENU.prototype = {
},
/**
* 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.
* Handle keyboard events when the menu is open. We respond to:
* * escape (exit)
* * tab (move to next menu item)
* * up/down (move to previous/next menu item)
*
* @method checkFocus
* @method handleKeyboardEvent
* @param {EventFacade} e The key event
*/
checkFocus: function(e) {
var nodelist = this.dialogue.all(SELECTOR.MENUCHILD),
firstNode,
lastNode;
handleKeyboardEvent: function(e) {
var next;
if (nodelist) {
firstNode = nodelist.item(0);
lastNode = nodelist.pop();
// Handle when the menu is still selected.
if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) {
if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) {
this.firstMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 38 && this.lastMenuChild) {
this.lastMenuChild.focus();
e.preventDefault();
} else if (e.keyCode === 9 && e.shiftKey) {
this.hideMenu();
e.preventDefault();
}
return this;
}
var menulink = this.menulink;
if (e.target === firstNode && e.shiftKey) {
if (e.keyCode === 27) {
// The escape key was pressed so close the menu.
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();
} else if (e.keyCode === 32) {
// The space bar was pressed. Trigger a click.
e.preventDefault();
e.currentTarget.simulate('click');
} else if (e.keyCode === 9) {
// The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options.
// We only override the Shift + Tab on the first option, and Tab on the last option to change where the focus is moved to.
if (e.target === this.firstMenuChild && e.shiftKey) {
this.hideMenu();
e.preventDefault();
} else if (e.target === this.lastMenuChild && !e.shiftKey) {
if (this.hideMenu()) {
// Determine the next selector and focus on it.
next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR);
if (next) {
next.focus();
}
}
}
} else if (e.keyCode === 38 || e.keyCode === 40) {
// The up (38) or down (40) key was pushed.
// On cursor moves we loops through the menu rather than exiting it as in the tab behaviour.
var found = false,
index = 0,
direction = 1,
checkCount = 0;
// Determine which menu item is currently selected.
while (!found && index < this.menuChildren.size()) {
if (this.menuChildren.item(index) === e.currentTarget) {
found = true;
} else {
index++;
}
}
if (!found) {
Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu');
return;
}
if (e.keyCode === 38) {
// Moving up so reverse the direction.
direction = -1;
}
// Try to find the next
do {
index += direction;
if (index < 0) {
index = this.menuChildren.size() - 1;
} else if (index >= this.menuChildren.size()) {
// Handle wrapping.
index = 0;
}
next = this.menuChildren.item(index);
// Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
checkCount++;
} while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden'));
if (next) {
next.focus();
e.preventDefault();
}
}
},
@@ -206,33 +416,37 @@ ACTIONMENU.prototype = {
showMenu : function(e, menu) {
Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME);
var ownerselector = menu.getData('owner'),
menucontent = menu.one(SELECTOR.MENUCONTENT),
menuchild;
menucontent = menu.one(SELECTOR.MENUCONTENT);
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);
} else {
this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true);
}
this.constrain(menucontent.set('aria-hidden', false));
if (e.type && e.type === 'key') {
menuchild = menucontent.one(SELECTOR.MENUCONTENTCHILD);
if (menuchild) {
menuchild.focus();
}
this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD);
if (this.menuChildren) {
this.firstMenuChild = this.menuChildren.item(0);
this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1);
this.firstMenuChild.focus();
}
// 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 keyboard changes.
this.events.push(menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this));
// Close the menu after a button was pushed.
this.events.push(menu.delegate('click', function(e) {
+2 -1
View File
@@ -2,7 +2,8 @@
"moodle-core-actionmenu": {
"requires": [
"base",
"event"
"event",
"node-event-simulate"
]
}
}
+1 -2
View File
@@ -1575,8 +1575,7 @@ img#persona_signin { cursor: pointer; }
width: 12px;
height: 12px;
}
.moodle-actionmenu[data-enhanced].show .menu a:hover,
.moodle-actionmenu[data-enhanced].show .menu a:focus {color: #ffffff;background-color: #0088cc;}
.moodle-actionmenu[data-enhanced].show .menu a:hover {color: #ffffff;background-color: #0088cc;}
.moodle-actionmenu[data-enhanced].show .menu a:first-child {-webkit-border-top-right-radius: 4px;border-top-right-radius: 4px;-webkit-border-top-left-radius: 4px;border-top-left-radius: 4px;-moz-border-radius-topright: 4px;-moz-border-radius-topleft: 4px;}
.moodle-actionmenu[data-enhanced].show .menu a:last-child {-webkit-border-bottom-right-radius: 4px;border-bottom-right-radius: 4px;-webkit-border-bottom-left-radius: 4px;border-bottom-left-radius: 4px;-moz-border-radius-bottomright: 4px;-moz-border-radius-bottomleft: 4px;}
.moodle-actionmenu[data-enhanced].show .menu a.hidden {display: none;}
+1 -2
View File
@@ -2060,8 +2060,7 @@ img#persona_signin {
display: block;
color: @dropdownLinkColor;
padding:2px 1em 2px 28px;
&:hover,
&:focus {
&:hover {
color: @dropdownLinkColorHover;
background-color: @dropdownLinkBackgroundHover;
}
File diff suppressed because one or more lines are too long