UNPKG

@ithaka/bonsai

Version:
167 lines (142 loc) 6.09 kB
"use strict"; 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 };