var PopupMenuLinks = function (domNode, controllerObj) { var elementChildren, msgPrefix = 'PopupMenuLinks constructor argument domNode '; // Check whether domNode is a DOM element if (!domNode instanceof Element) { throw new TypeError(msgPrefix + 'is not a DOM Element.'); } // Check whether domNode has child elements if (domNode.childElementCount === 0) { 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.menuitems = []; // see PopupMenuLinks init method this.firstChars = []; // see PopupMenuLinks init method this.firstItem = null; // see PopupMenuLinks init method this.lastItem = null; // see PopupMenuLinks init method this.hasFocus = false; // see MenuItemLinks handleFocus, handleBlur this.hasHover = false; // see PopupMenuLinks handleMouseover, handleMouseout }; /* * @method PopupMenuLinks.prototype.init * * @desc * Add domNode event listeners for mouseover and mouseout. Traverse * domNode children to configure each menuitem and populate menuitems * array. Initialize firstItem and lastItem properties. */ PopupMenuLinks.prototype.init = function () { var childElement, menuElement, menuItem, textContent, numItems, label; // Configure the domNode itself this.domNode.tabIndex = -1; this.domNode.setAttribute('role', 'menu'); if (!this.domNode.getAttribute('aria-labelledby') && !this.domNode.getAttribute('aria-label') && !this.domNode.getAttribute('title')) { label = this.controller.domNode.innerHTML; 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. childElement = this.domNode.firstElementChild; while (childElement) { menuElement = childElement.firstElementChild; if (menuElement && menuElement.tagName === 'A') { menuItem = new MenuItemLinks(menuElement, this); menuItem.init(); this.menuitems.push(menuItem); textContent = menuElement.textContent.trim(); this.firstChars.push(textContent.substring(0, 1).toLowerCase()); } childElement = childElement.nextElementSibling; } // Use populated menuitems array to initialize firstItem and lastItem. numItems = this.menuitems.length; if (numItems > 0) { this.firstItem = this.menuitems[0]; this.lastItem = this.menuitems[numItems - 1]; } }; /* EVENT HANDLERS */ PopupMenuLinks.prototype.handleMouseover = function (event) { this.hasHover = true; }; PopupMenuLinks.prototype.handleMouseout = function (event) { this.hasHover = false; setTimeout(this.close.bind(this, false), 1); }; /* FOCUS MANAGEMENT METHODS */ PopupMenuLinks.prototype.setFocusToController = function (command) { if (typeof command !== 'string') { command = ''; } if (command === 'previous') { this.controller.menubar.setFocusToPreviousItem(this.controller); } else { if (command === 'next') { this.controller.menubar.setFocusToNextItem(this.controller); } else { this.controller.domNode.focus(); } } }; PopupMenuLinks.prototype.setFocusToFirstItem = function () { this.firstItem.domNode.focus(); }; PopupMenuLinks.prototype.setFocusToLastItem = function () { this.lastItem.domNode.focus(); }; PopupMenuLinks.prototype.setFocusToPreviousItem = function (currentItem) { var index; if (currentItem === this.firstItem) { this.lastItem.domNode.focus(); } else { index = this.menuitems.indexOf(currentItem); this.menuitems[index - 1].domNode.focus(); } }; PopupMenuLinks.prototype.setFocusToNextItem = function (currentItem) { var index; if (currentItem === this.lastItem) { this.firstItem.domNode.focus(); } else { index = this.menuitems.indexOf(currentItem); this.menuitems[index + 1].domNode.focus(); } }; PopupMenuLinks.prototype.setFocusByFirstCharacter = function (currentItem, char) { var start, index, char = char.toLowerCase(); // Get start index for search based on position of currentItem start = this.menuitems.indexOf(currentItem) + 1; if (start === this.menuitems.length) { start = 0; } // Check remaining slots in the menu index = this.getIndexFirstChars(start, char); // If not found in remaining slots, check from beginning if (index === -1) { index = this.getIndexFirstChars(0, char); } // If match was found... if (index > -1) { this.menuitems[index].domNode.focus(); } }; PopupMenuLinks.prototype.getIndexFirstChars = function (startIndex, char) { for (var i = startIndex; i < this.firstChars.length; i++) { if (char === this.firstChars[i]) { return i; } } return -1; }; /* MENU DISPLAY METHODS */ PopupMenuLinks.prototype.open = function () { // get position and bounding rectangle of controller object's DOM node var rect = this.controller.domNode.getBoundingClientRect(); // set CSS properties this.domNode.setAttribute('class', 'opened'); this.domNode.setAttribute('aria-hidden', 'false'); // this.domNode.style.position = 'absolute'; // this.domNode.style.top = rect.height + 'px'; // this.domNode.style.left = '0px'; // set aria-expanded attribute this.controller.domNode.setAttribute('aria-expanded', 'true'); }; PopupMenuLinks.prototype.close = function (force) { if (this.domNode.getAttribute('aria-hidden') == 'false' && this.controller.domNode.getAttribute('aria-expanded') == 'false') { this.domNode.setAttribute('aria-hidden', 'true'); } // this.domNode.setAttribute('aria-hidden', 'true'); if (force || (!this.hasFocus && !this.hasHover && !this.controller.hasHover)) { // this.domNode.style.display = 'none'; this.controller.domNode.setAttribute('aria-expanded', 'false'); this.domNode.setAttribute('class', ''); this.domNode.setAttribute('aria-hidden', 'true'); } }; /* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * * File: MenuItemLinks.js * * Desc: Popup Menu Menuitem widget that implements ARIA Authoring Practices * * Author: Jon Gunderson, Ku Ja Eun, Nicholas Hoyt and Brian Loh */ /* * @constructor MenuItemLinks * * @desc * Wrapper object for a simple menu item in a popup menu * * @param domNode * The DOM element node that serves as the menu item container. * The menuObj PopupMenu is responsible for checking that it has * requisite metadata, e.g. role="menuitem". * * @param menuObj * The object that is a wrapper for the PopupMenu DOM element that * contains the menu item DOM element. See PopupMenu.js */ var MenuItemLinks = function (domNode, menuObj) { this.domNode = domNode; this.menu = menuObj; this.keyCode = Object.freeze({ 'TAB': 9, 'RETURN': 13, 'ESC': 27, 'SPACE': 32, 'PAGEUP': 33, 'PAGEDOWN': 34, 'END': 35, 'HOME': 36, 'LEFT': 37, 'UP': 38, 'RIGHT': 39, 'DOWN': 40 }); }; MenuItemLinks.prototype.init = function () { this.domNode.tabIndex = -1; if (!this.domNode.getAttribute('role')) { this.domNode.setAttribute('role', 'menuitem'); } this.domNode.addEventListener('keydown', this.handleKeydown.bind(this)); this.domNode.addEventListener('keypress', this.handleKeypress.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('mouseover', this.handleMouseover.bind(this)); this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); }; /* EVENT HANDLERS */ MenuItemLinks.prototype.handleKeydown = function (event) { var tgt = event.currentTarget, flag = false, clickEvent; // Console.log("[MenuItemLinks][handleKeydown]: " + event.keyCode + " " + this.menu) switch (event.keyCode) { case this.keyCode.SPACE: case this.keyCode.RETURN: // Create simulated mouse event to mimic the behavior of ATs // and let the event handler handleClick do the housekeeping. try { clickEvent = new MouseEvent('click', { 'view': window, 'bubbles': true, 'cancelable': true }); } catch (err) { if (document.createEvent) { // DOM Level 3 for IE 9+ clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent('click', true, true); } } tgt.dispatchEvent(clickEvent); flag = true; break; case this.keyCode.ESC: this.menu.setFocusToController(); this.menu.close(true); flag = true; break; case this.keyCode.UP: this.menu.setFocusToPreviousItem(this); flag = true; break; case this.keyCode.DOWN: this.menu.setFocusToNextItem(this); flag = true; break; case this.keyCode.LEFT: this.menu.setFocusToController('previous'); this.menu.close(true); flag = true; break; case this.keyCode.RIGHT: this.menu.setFocusToController('next'); this.menu.close(true); flag = true; break; case this.keyCode.HOME: case this.keyCode.PAGEUP: this.menu.setFocusToFirstItem(); flag = true; break; case this.keyCode.END: case this.keyCode.PAGEDOWN: this.menu.setFocusToLastItem(); flag = true; break; case this.keyCode.TAB: this.menu.setFocusToController(); this.menu.close(true); break; default: break; } if (flag) { event.stopPropagation(); event.preventDefault(); } }; MenuItemLinks.prototype.handleKeypress = function (event) { var char = String.fromCharCode(event.charCode); function isPrintableCharacter(str) { return str.length === 1 && str.match(/\S/); } if (isPrintableCharacter(char)) { this.menu.setFocusByFirstCharacter(this, char); } }; MenuItemLinks.prototype.handleClick = function (event) { this.menu.setFocusToController(); this.menu.close(true); }; MenuItemLinks.prototype.handleFocus = function (event) { this.menu.hasFocus = true; }; MenuItemLinks.prototype.handleBlur = function (event) { this.menu.hasFocus = false; setTimeout(this.menu.close.bind(this.menu, false), 1); }; MenuItemLinks.prototype.handleMouseover = function (event) { this.menu.hasHover = true; this.menu.open(); }; MenuItemLinks.prototype.handleMouseout = function (event) { this.menu.hasHover = false; setTimeout(this.menu.close.bind(this.menu, false), 1); }; /* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * * File: Menubutton.js * * Desc: Menubutton Menuitem widget that implements ARIA Authoring Practices * * Author: Jon Gunderson, Ku Ja Eun, Nicholas Hoyt and Brian Loh */ /* * @constructor MenubuttonItem * * @desc * Object that configures menu 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 * MenubuttonItem 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 * requisite metadata, e.g. role="menuitem". * */ var Menubutton = function (domNode) { this.domNode = domNode; this.popupMenu = false; this.hasFocus = false; this.hasHover = false; this.keyCode = Object.freeze({ 'TAB': 9, 'RETURN': 13, 'ESC': 27, 'SPACE': 32, 'PAGEUP': 33, 'PAGEDOWN': 34, 'END': 35, 'HOME': 36, 'LEFT': 37, 'UP': 38, 'RIGHT': 39, 'DOWN': 40 }); }; Menubutton.prototype.init = function () { this.domNode.setAttribute('aria-haspopup', 'true'); this.domNode.setAttribute('aria-expanded', 'false'); 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('mouseover', this.handleMouseover.bind(this)); this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); // initialize pop up menus var popupMenu = document.getElementById(this.domNode.getAttribute('aria-controls')); if (popupMenu) { this.popupMenu = new PopupMenuLinks(popupMenu, this); this.popupMenu.init(); } }; Menubutton.prototype.handleKeydown = function (event) { var tgt = event.currentTarget, flag = false, clickEvent; switch (event.keyCode) { case this.keyCode.SPACE: case this.keyCode.RETURN: case this.keyCode.DOWN: if (this.popupMenu) { this.popupMenu.open(); this.popupMenu.setFocusToFirstItem(); } flag = true; break; case this.keyCode.UP: if (this.popupMenu) { this.popupMenu.open(); this.popupMenu.setFocusToLastItem(); flag = true; } break; default: break; } if (flag) { event.stopPropagation(); event.preventDefault(); } }; Menubutton.prototype.handleClick = function (event) { if (this.domNode.getAttribute('aria-expanded') == 'true') { this.popupMenu.close(); closeDropdown(); } else { this.popupMenu.open(); this.popupMenu.setFocusToFirstItem(); } }; Menubutton.prototype.handleFocus = function (event) { this.popupMenu.hasFocus = true; }; Menubutton.prototype.handleBlur = function (event) { this.popupMenu.hasFocus = false; }; Menubutton.prototype.handleMouseover = function (event) { //this.hasHover = true; //this.popupMenu.open(); }; Menubutton.prototype.handleMouseout = function (event) { this.hasHover = false; setTimeout(this.popupMenu.close.bind(this.popupMenu, false), 1); }; $(function () { // set dropdown $('.m-form__dropdown>ul').attr('aria-hidden', 'true'); $('.m-form__dropdown>a').attr('tabIndex', 0); $('.m-form__dropdown>a').click(function (e) { e.preventDefault(); }); $('.m-form__dropdown>ul li').click(function () { setDropdownValue($(this)); // set event handler $('body').trigger("dropdownChange", [$(this)]); }); $(document).on('keydown', ".m-form__dropdown>ul li", function (e) { if (e.keyCode == 13) { setDropdownValue($(this)); } }); $(document).bind('touchstart click', function (e) { var dropdown = $('.custom-dropdown'); if (!dropdown.is(e.target) && dropdown.has(e.target).length === 0) { closeDropdown(); } }); function setDropdownValue($this) { $this.siblings().find('a').removeClass('selected'); $this.find('a').addClass('selected'); var value = $this.find('a').attr('data-value'); var displayValue = $this.find('a').text(); $this.parents('.m-form__dropdown').parent().find('input').val(value); $this.parents('.m-form__dropdown').find('a span').text(displayValue); } }); function closeDropdown() { $('.m-form__dropdown>a').attr('aria-expanded', 'false'); $('.m-form__dropdown>ul').removeAttr('opened'); }