@ithaka/bonsai
Version:
ITHAKA core styling
167 lines (142 loc) • 6.09 kB
JavaScript
;
import $ from "jquery";
import { DropdownMenu } from "foundation-sites/js/foundation.dropdownMenu";
import { BonsaiBase } from "./bonsai.base";
/** DropdownMenu Module
*
* @class
* @param {jQuery} elements - jQuery object to use for initializing the dropdownmenu component.
* @param {Object} options - Optional parameters
*
* @example
* const dropdownmenu = new BonsaiDropdownMenu($("ul.dropdown"));
*
* @return Returns object that has initialized each dropdownmenu given the incoming `options`.
*/
class BonsaiDropdownMenu extends BonsaiBase {
constructor(elements, options = {}) {
super(elements, options);
Bonsai.DropdownMenus = Bonsai.hasOwnProperty("DropdownMenus") ? Bonsai.DropdownMenus : {members: {}, reflow: this.reflow};
this._initializeDropdownMenus();
}
/**
* Iterates through all elements of the jQuery selector and creates a DropdownMenu object. If the tab element has an ID, it
* is added to the global Bonsai.DropdownMenus Object for global event listening
*
* @private
*/
_initializeDropdownMenus() {
this.elements.each((index, element) => {
let $element = $(element),
elementID = $element.attr("id"),
theDropdownMenu = new DropdownMenu($element, this.options);
this._closeMenuOnClick($element, theDropdownMenu);
this._closeMenuOnLostFocus($element, theDropdownMenu);
this._fixOverlappingDropdownMenusOnMobile($element);
this._focusTopLevelItem($element, theDropdownMenu);
this._setAriaAttributes($element);
// Prevent disabled menu items from triggering dropdown events (e.g. closing the modal)
$element.find("[role='menuitem'].disabled").unbind("click.zf.dropdownmenu touchstart.zf.dropdownmenu");
if(elementID) {
Bonsai.DropdownMenus.members[elementID] = theDropdownMenu;
}
});
}
/**
* Clicking on a top-level menu item should close the dropdown menu if it is already open
*
* @private
*/
_closeMenuOnClick($element, theDropdownMenu) {
$element.find("[role='menuitem'].is-dropdown-submenu-parent").on({
"click": (event) => {
if ($(event.currentTarget).hasClass("is-active")) {
theDropdownMenu._hide();
}
}
});
}
/**
* Dropdown menus should close if they're no longer in focus
*
* @private
*/
_closeMenuOnLostFocus($element, theDropdownMenu) {
const submenuParents = $element.find(".is-dropdown-submenu-parent");
submenuParents.each((index, submenuParent) => {
const $submenuParent = $(submenuParent);
const menuItems = $submenuParent.find(".submenu [role='menuitem']");
$submenuParent.on({
"keydown": (event) => {
const isTab = event.key === "Tab";
const hasShift = event.shiftKey;
if (menuItems) {
const firstItemLosingFocus = hasShift && isTab && event.target === menuItems[0];
const lastItemLosingFocus = !hasShift && isTab && event.target === menuItems[menuItems.length - 1];
if (firstItemLosingFocus || lastItemLosingFocus) {
theDropdownMenu._hide();
}
}
}
});
});
}
/**
* Fixes a bug in foundation that prevents previously opened dropdownmenus from closing when a new one is clicked.
* The issue stems from the fact that the foundation dropdownmenu component reinitializes a $body click listener
* on EVERY click to accomplishing this. On mobile, due to the timing of the 'touchstart' event, this new $body
* click listener gets reinitialized before the previously opened menu listener could close. The fix here is to simply
* turn off the 'touchstart' events, as it appears that 'click' events still get fired on mobile devices.
*/
_fixOverlappingDropdownMenusOnMobile($element) {
$element.find("[role='menuitem']").off("touchstart.zf.dropdownmenu");
}
/**
* The escape key should close the menu and leave focus on the top-level navigation item
*
* @private
*/
_focusTopLevelItem($element, theDropdownMenu) {
const submenuItems = $element.find(".submenu [role='menuitem']");
submenuItems && submenuItems.each((index, menuItem) => {
const $menuItem = $(menuItem);
$menuItem.on({
"keydown": (event) => {
const isEscape = event.key === "Escape";
if (isEscape) {
event.stopPropagation();
$menuItem.parents(".is-dropdown-submenu-parent").children("a[role='menuitem']").focus();
theDropdownMenu._hide();
}
}
});
});
}
/**
* Set aria-expanded on open and close of dropdown menu item with submenu
*
* @private
*/
_setAriaAttributes($element) {
$element.find(".is-dropdown-submenu-parent [role='menuitem']").attr("aria-expanded", false);
$element.on({
"show.zf.dropdownmenu": () => {
$element.find(".is-dropdown-submenu-parent.is-active [role='menuitem']").attr("aria-expanded", true);
},
"hide.zf.dropdownmenu": () => {
$element.find(".is-dropdown-submenu-parent [role='menuitem']").attr("aria-expanded", false);
}
});
}
/**
* Reflow method to re-bind jQuery element to a Dropdownmenu object
*
* @public
*/
reflow() {
$.each(Bonsai.DropdownMenus.members, (elementID, theDropdownMenu) => {
Bonsai.DropdownMenus.members[elementID] = new DropdownMenu($(`#${elementID}`));
});
}
}
export { BonsaiDropdownMenu };