Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Menubar examples: Make mouse behaviors and parent menu item appearanc… #593

Merged
merged 8 commits into from
Jun 3, 2018
1 change: 0 additions & 1 deletion examples/menubar/menubar-2/css/menubarAction.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ ul[role="menubar"] [role="separator"] {
}

ul[role="menubar"] [role="menuitem"]:focus,
ul[role="menubar"] [role="menuitem"] [role="menuitem"]:hover,
ul[role="menubar"] [role="menu"] [role="menuitem"]:hover,
ul[role="menubar"] [role="menu"] [role="menuitemcheckbox"]:focus,
ul[role="menubar"] [role="menu"] [role="menuitemcheckbox"]:hover,
Expand Down
51 changes: 22 additions & 29 deletions examples/menubar/menubar-2/js/MenubarAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@
* @constructor MenubarAction
*
* @desc
* Wrapper object for a menubar (with nested submenus of links)
* Wrapper object for a menubar
*
* @param domNode
* The DOM element node that serves as the menubar container. Each
* child element of menubarNode that represents a menubaritem must
* be an A element
* The DOM element node that serves as the menubar container.
* Each child element of domNode that represents a menubaritem
* must be an A element.
*/
var MenubarAction = function (domNode) {
var elementChildren,
msgPrefix = 'Menubar constructor argument menubarNode ';
var msgPrefix = 'Menubar constructor argument domNode ';

// Check whether menubarNode is a DOM element
// Check whether domNode is a DOM element
if (!domNode instanceof Element) {
throw new TypeError(msgPrefix + 'is not a DOM Element.');
}

// Check whether menubarNode has descendant elements
// Check whether domNode has descendant elements
if (domNode.childElementCount === 0) {
throw new Error(msgPrefix + 'has no element children.');
}
// Check whether menubarNode has A elements
e = domNode.firstElementChild;

// Check whether domNode's descendant elements contain A elements
var e = domNode.firstElementChild;
while (e) {
var menubarItem = e.firstElementChild;
if (e && menubarItem && menubarItem.tagName !== 'A') {
throw new Error(msgPrefix + 'has child elements are not A elements.');
if (menubarItem && menubarItem.tagName !== 'A') {
throw new Error(msgPrefix + 'has child elements that are not A elements.');
}
e = e.nextElementSibling;
}
Expand All @@ -48,34 +48,31 @@ var MenubarAction = function (domNode) {

this.firstItem = null; // see Menubar init method
this.lastItem = null; // see Menubar init method

this.hasFocus = false; // see MenubarItem handleFocus, handleBlur
this.hasHover = false; // see Menubar handleMouseover, handleMouseout
};

/*
* @method MenubarAction.prototype.init
*
* @desc
* Adds ARIA role to the menubar node
* Traverse menubar children for A elements to configure each A element as a ARIA menuitem
* Traverse menubar children for A elements to configure each A element as an ARIA menuitem
* and populate menuitems array. Initialize firstItem and lastItem properties.
*/
MenubarAction.prototype.init = function (actionManager) {
var menubarItem, childElement, menuElement, textContent, numItems;
var menubarItem, menuElement, textContent, numItems;

this.actionManager = actionManager;

this.domNode.setAttribute('role', 'menubar');

// Traverse the element children of menubarNode: configure each with
// Traverse the element children of the menubar domNode: configure each with
// menuitem role behavior and store reference in menuitems array.
e = this.domNode.firstElementChild;
var e = this.domNode.firstElementChild;

while (e) {
var menuElement = e.firstElementChild;
menuElement = e.firstElementChild;

if (e && menuElement && menuElement.tagName === 'A') {
if (menuElement && menuElement.tagName === 'A') {
menubarItem = new MenubarItemAction(menuElement, this);
menubarItem.init();
this.menubarItems.push(menubarItem);
Expand All @@ -99,11 +96,10 @@ MenubarAction.prototype.init = function (actionManager) {

MenubarAction.prototype.setFocusToItem = function (newItem) {
var flag = false;
var newItem;
for (var i = 0; i < this.menubarItems.length; i++) {
var mbi = this.menubarItems[i];
if (mbi.domNode.tabIndex == 0) {
flag = mbi.domNode.getAttribute('aria-expanded') === 'true';
if (mbi.domNode.tabIndex === 0) {
flag = mbi.popupMenu && mbi.popupMenu.isOpen();
}
mbi.domNode.tabIndex = -1;
if (mbi.popupMenu) {
Expand All @@ -124,8 +120,7 @@ MenubarAction.prototype.setFocusToLastItem = function (flag) {
};

MenubarAction.prototype.setFocusToPreviousItem = function (currentItem) {

var index;
var newItem, index;

if (currentItem === this.firstItem) {
newItem = this.lastItem;
Expand All @@ -136,11 +131,10 @@ MenubarAction.prototype.setFocusToPreviousItem = function (currentItem) {
}

this.setFocusToItem(newItem);

};

MenubarAction.prototype.setFocusToNextItem = function (currentItem) {
var index;
var newItem, index;

if (currentItem === this.lastItem) {
newItem = this.firstItem;
Expand All @@ -150,7 +144,6 @@ MenubarAction.prototype.setFocusToNextItem = function (currentItem) {
newItem = this.menubarItems[ index + 1 ];
}
this.setFocusToItem(newItem);

};

MenubarAction.prototype.setFocusByFirstCharacter = function (currentItem, char) {
Expand Down
76 changes: 40 additions & 36 deletions examples/menubar/menubar-2/js/MenubarItemAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,27 @@
*/

/*
* @constructor MenubarItem
* @constructor MenubarItemAction
*
* @desc
* Object that configures menu item elements by setting tabIndex
* Object that configures menubar item elements by setting tabIndex
* and registering itself to handle pertinent events.
*
* While menuitem elements handle many keydown events, as well as
* focus and blur events, they do not maintain any state variables,
* delegating those responsibilities to its associated menu object.
*
* Consequently, it is only necessary to create one instance of
* MenubarItem from within the menu object; its configure method
* can then be called on each menuitem element.
*
* @param domNode
* The DOM element node that serves as the menu item container.
* The menuObj PopupMenu is responsible for checking that it has
* The DOM element node that serves as the menubar item container.
* The menubarObj is responsible for checking that it has
* requisite metadata, e.g. role="menuitem".
*
* @param menuObj
* The PopupMenu object that is a delegate for the menu DOM element
* that contains the menuitem element.
* @param menubarObj
* The MenubarAction object that is a delegate for the menubar DOM element
* that contains the menubar item element.
*/
var MenubarItemAction = function (domNode, menuObj) {
var MenubarItemAction = function (domNode, menubarObj) {

this.menubar = menuObj;
this.menubar = menubarObj;
this.domNode = domNode;
this.popupMenu = false;

this.hasFocus = false;
this.hasHover = false;

this.keyCode = Object.freeze({
'TAB': 9,
'RETURN': 13,
Expand All @@ -61,10 +50,8 @@ MenubarItemAction.prototype.init = function () {

this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
this.domNode.addEventListener('click', this.handleClick.bind(this));
this.domNode.addEventListener('focus', this.handleFocus.bind(this));
this.domNode.addEventListener('blur', this.handleBlur.bind(this));
this.domNode.addEventListener('focusout', this.handleFocusout.bind(this));
this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));

// initialize pop up menus

Expand All @@ -80,8 +67,7 @@ MenubarItemAction.prototype.init = function () {
MenubarItemAction.prototype.handleKeydown = function (event) {
var tgt = event.currentTarget,
char = event.key,
flag = false,
clickEvent;
flag = false;

function isPrintableCharacter (str) {
return str.length === 1 && str.match(/\S/);
Expand All @@ -98,6 +84,13 @@ MenubarItemAction.prototype.handleKeydown = function (event) {
}
break;

case this.keyCode.ESC:
if (this.popupMenu) {
this.popupMenu.close();
}
flag = true;
break;

case this.keyCode.LEFT:
this.menubar.setFocusToPreviousItem(this);
flag = true;
Expand Down Expand Up @@ -149,22 +142,33 @@ MenubarItemAction.prototype.handleKeydown = function (event) {
};

MenubarItemAction.prototype.handleClick = function (event) {
if (this.popupMenu) {
if (!this.popupMenu.isOpen()) {
// clicking on menubar item opens menu
this.popupMenu.open();
}
else {
// clicking again on same menubar item closes menu
this.popupMenu.close();
}
// prevent scroll to top of page when anchor element is clicked
event.preventDefault();
}
};

MenubarItemAction.prototype.handleFocus = function (event) {
this.menubar.hasFocus = true;
MenubarItemAction.prototype.menubarElement = function (el) {
return this.menubar.domNode.contains(el);
};

MenubarItemAction.prototype.handleBlur = function (event) {
this.menubar.hasFocus = false;
MenubarItemAction.prototype.handleFocusout = function (event) {
// if the next element to get focus is not in the menubar or its menus, then close menu
if (!this.menubarElement(event.relatedTarget)) {
if (this.popupMenu && this.popupMenu.isOpen()) {
this.popupMenu.close();
}
}
};

MenubarItemAction.prototype.handleMouseover = function (event) {
this.hasHover = true;
this.popupMenu.open();
};

MenubarItemAction.prototype.handleMouseout = function (event) {
this.hasHover = false;
setTimeout(this.popupMenu.close.bind(this.popupMenu, false), 300);
this.menubar.setFocusToItem(this);
};
47 changes: 9 additions & 38 deletions examples/menubar/menubar-2/js/PopupMenuAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
* The controller object is expected to have the following properties:
* 1. domNode: The controller object's DOM element node, needed for
* retrieving positioning information.
* 2. hasHover: boolean that indicates whether the controller object's
* domNode has responded to a mouseover event with no subsequent
* mouseout event having occurred.
*/
var PopupMenuAction = function (domNode, controllerObj, actionManager) {
var elementChildren,
Expand All @@ -44,16 +41,6 @@ var PopupMenuAction = function (domNode, controllerObj, actionManager) {
throw new Error(msgPrefix + 'has no element children.');
}

// Check whether domNode descendant elements have A elements
var childElement = domNode.firstElementChild;
while (childElement) {
var menuitem = childElement.firstElementChild;
if (menuitem && menuitem === 'A') {
throw new Error(msgPrefix + 'has descendant elements that are not A elements.');
}
childElement = childElement.nextElementSibling;
}

this.domNode = domNode;
this.controller = controllerObj;
this.actionManager = actionManager;
Expand All @@ -63,9 +50,6 @@ var PopupMenuAction = function (domNode, controllerObj, actionManager) {

this.firstItem = null; // see PopupMenu init method
this.lastItem = null; // see PopupMenu init method

this.hasFocus = false; // see MenuItem handleFocus, handleBlur
this.hasHover = false; // see PopupMenu handleMouseover, handleMouseout
};

/*
Expand All @@ -89,9 +73,6 @@ PopupMenuAction.prototype.init = function () {
this.domNode.setAttribute('aria-label', label);
}

this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));

// Traverse the element children of domNode: configure each with
// menuitem role behavior and store reference in menuitems array.
menuElements = this.domNode.getElementsByTagName('LI');
Expand All @@ -100,7 +81,7 @@ PopupMenuAction.prototype.init = function () {

menuElement = menuElements[i];

if (!menuElement.firstElementChild && menuElement.getAttribute('role') != 'separator') {
if (!menuElement.firstElementChild && menuElement.getAttribute('role') !== 'separator') {
menuItem = new MenuItem(menuElement, this);
menuItem.init();
this.menuitems.push(menuItem);
Expand Down Expand Up @@ -158,17 +139,6 @@ PopupMenuAction.prototype.updateMenuStates = function () {

};

/* EVENT HANDLERS */

PopupMenuAction.prototype.handleMouseover = function (event) {
this.hasHover = true;
};

PopupMenuAction.prototype.handleMouseout = function (event) {
this.hasHover = false;
setTimeout(this.close.bind(this, false), 300);
};

/* FOCUS MANAGEMENT METHODS */

PopupMenuAction.prototype.setFocusToController = function (command) {
Expand Down Expand Up @@ -257,21 +227,22 @@ PopupMenuAction.prototype.open = function () {
var rect = this.controller.domNode.getBoundingClientRect();

// set CSS properties
this.domNode.style.display = 'block';
this.domNode.style.position = 'absolute';
this.domNode.style.top = (rect.height - 1) + 'px';
this.domNode.style.left = '0px';
this.domNode.style.zIndex = 100;
this.domNode.style.display = 'block';

// set aria-expanded attribute
this.controller.domNode.setAttribute('aria-expanded', 'true');
};

PopupMenuAction.prototype.close = function (force) {
PopupMenuAction.prototype.isOpen = function () {
return this.controller.domNode.getAttribute('aria-expanded') === 'true';
};

if (force || (!this.hasFocus && !this.hasHover && !this.controller.hasHover)) {
this.domNode.style.display = 'none';
this.domNode.style.zIndex = 0;
this.controller.domNode.setAttribute('aria-expanded', 'false');
}
PopupMenuAction.prototype.close = function () {
this.domNode.style.display = 'none';
this.domNode.style.zIndex = 0;
this.controller.domNode.setAttribute('aria-expanded', 'false');
};
Loading