/* eslint-disable class-methods-use-this */
import { queryOne, queryAll } from '@ecl/dom-utils';
import EventManager from '@ecl/event-manager';
import isMobile from 'mobile-device-detect';
import { createFocusTrap } from 'focus-trap';
/**
* @param {HTMLElement} element DOM element for component instantiation and scope
* @param {Object} options
* @param {String} options.openSelector Selector for the hamburger button
* @param {String} options.backSelector Selector for the back button
* @param {String} options.innerSelector Selector for the menu inner
* @param {String} options.listSelector Selector for the menu items list
* @param {String} options.itemSelector Selector for the menu item
* @param {String} options.linkSelector Selector for the menu link
* @param {String} options.megaSelector Selector for the mega menu
* @param {String} options.subItemSelector Selector for the menu sub items
* @param {String} options.labelOpenAttribute The data attribute for open label
* @param {String} options.labelCloseAttribute The data attribute for close label
* @param {Boolean} options.attachClickListener Whether or not to bind click events
* @param {Boolean} options.attachHoverListener Whether or not to bind hover events
* @param {Boolean} options.attachFocusListener Whether or not to bind focus events
* @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events
* @param {Boolean} options.attachResizeListener Whether or not to bind resize events
*/
export class MegaMenu {
/**
* @static
* Shorthand for instance creation and initialisation.
*
* @param {HTMLElement} root DOM element for component instantiation and scope
*
* @return {Menu} An instance of Menu.
*/
static autoInit(root, { MEGA_MENU: defaultOptions = {} } = {}) {
const megaMenu = new MegaMenu(root, defaultOptions);
megaMenu.init();
root.ECLMegaMenu = megaMenu;
return megaMenu;
}
/**
* @event MegaMenu#onOpen
*/
/**
* @event MegaMenu#onClose
*/
/**
* @event MegaMenu#onOpenPanel
*/
/**
* @event MegaMenu#onBack
*/
/**
* @event MegaMenu#onItemClick
*/
/**
* @event MegaMenu#onFocusTrapToggle
*/
/**
* An array of supported events for this component.
*
* @type {Array<string>}
* @memberof MegaMenu
*/
supportedEvents = ['onOpen', 'onClose'];
constructor(
element,
{
openSelector = '[data-ecl-mega-menu-open]',
backSelector = '[data-ecl-mega-menu-back]',
innerSelector = '[data-ecl-mega-menu-inner]',
listSelector = '[data-ecl-mega-menu-list]',
itemSelector = '[data-ecl-mega-menu-item]',
linkSelector = '[data-ecl-mega-menu-link]',
megaSelector = '[data-ecl-mega-menu-mega]',
containerSelector = 'data-ecl-has-container',
subItemSelector = '[data-ecl-mega-menu-subitem]',
featuredAttribute = '[data-ecl-mega-menu-featured]',
labelOpenAttribute = 'data-ecl-mega-menu-label-open',
labelCloseAttribute = 'data-ecl-mega-menu-label-close',
attachClickListener = true,
attachFocusListener = true,
attachKeyListener = true,
attachResizeListener = true,
} = {},
) {
// Check element
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError(
'DOM element should be given to initialize this widget.',
);
}
this.element = element;
this.eventManager = new EventManager();
// Options
this.openSelector = openSelector;
this.backSelector = backSelector;
this.innerSelector = innerSelector;
this.listSelector = listSelector;
this.itemSelector = itemSelector;
this.linkSelector = linkSelector;
this.megaSelector = megaSelector;
this.subItemSelector = subItemSelector;
this.containerSelector = containerSelector;
this.labelOpenAttribute = labelOpenAttribute;
this.labelCloseAttribute = labelCloseAttribute;
this.attachClickListener = attachClickListener;
this.attachFocusListener = attachFocusListener;
this.attachKeyListener = attachKeyListener;
this.attachResizeListener = attachResizeListener;
this.featuredAttribute = featuredAttribute;
// Private variables
this.direction = 'ltr';
this.open = null;
this.toggleLabel = null;
this.back = null;
this.inner = null;
this.itemsList = null;
this.items = null;
this.links = null;
this.isOpen = false;
this.resizeTimer = null;
this.isKeyEvent = false;
this.isDesktop = false;
this.isLarge = false;
this.lastVisibleItem = null;
this.currentItem = null;
this.totalItemsWidth = 0;
this.breakpointL = 996;
this.openPanel = { num: 0, item: {} };
// Bind `this` for use in callbacks
this.handleClickOnOpen = this.handleClickOnOpen.bind(this);
this.handleClickOnClose = this.handleClickOnClose.bind(this);
this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
this.handleClickOnBack = this.handleClickOnBack.bind(this);
this.handleClickGlobal = this.handleClickGlobal.bind(this);
this.handleClickOnItem = this.handleClickOnItem.bind(this);
this.handleClickOnSubitem = this.handleClickOnSubitem.bind(this);
this.handleFocusOut = this.handleFocusOut.bind(this);
this.handleKeyboard = this.handleKeyboard.bind(this);
this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this);
this.handleResize = this.handleResize.bind(this);
this.useDesktopDisplay = this.useDesktopDisplay.bind(this);
this.checkMegaMenu = this.checkMegaMenu.bind(this);
this.closeOpenDropdown = this.closeOpenDropdown.bind(this);
this.checkDropdownHeight = this.checkDropdownHeight.bind(this);
this.positionMenuOverlay = this.positionMenuOverlay.bind(this);
this.resetStyles = this.resetStyles.bind(this);
this.handleFirstPanel = this.handleFirstPanel.bind(this);
this.handleSecondPanel = this.handleSecondPanel.bind(this);
this.disableScroll = this.disableScroll.bind(this);
this.enableScroll = this.enableScroll.bind(this);
}
/**
* Initialise component.
*/
init() {
if (!ECL) {
throw new TypeError('Called init but ECL is not present');
}
ECL.components = ECL.components || new Map();
// Query elements
this.open = queryOne(this.openSelector, this.element);
this.toggleLabel = queryOne('.ecl-link__label', this.open);
this.back = queryOne(this.backSelector, this.element);
this.inner = queryOne(this.innerSelector, this.element);
this.itemsList = queryOne(this.listSelector, this.element);
this.btnPrevious = queryOne(this.buttonPreviousSelector, this.element);
this.btnNext = queryOne(this.buttonNextSelector, this.element);
this.items = queryAll(this.itemSelector, this.element);
this.subItems = queryAll(this.subItemSelector, this.element);
this.links = queryAll(this.linkSelector, this.element);
// Check if we should use desktop display (it does not rely only on breakpoints)
this.isDesktop = this.useDesktopDisplay();
// Bind click events on buttons
if (this.attachClickListener) {
// Open
if (this.open) {
this.open.addEventListener('click', this.handleClickOnToggle);
}
// Back
if (this.back) {
this.back.addEventListener('click', this.handleClickOnBack);
this.back.addEventListener('keyup', this.handleKeyboard);
}
// Global click
if (this.attachClickListener) {
document.addEventListener('click', this.handleClickGlobal);
}
}
// Bind event on menu links
if (this.links) {
this.links.forEach((link) => {
if (this.attachFocusListener) {
link.addEventListener('focusout', this.handleFocusOut);
}
if (this.attachKeyListener) {
link.addEventListener('keyup', this.handleKeyboard);
}
});
}
// Bind event on sub menu links
if (this.subItems) {
this.subItems.forEach((subItem) => {
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
if (this.attachKeyListener && subLink) {
subLink.addEventListener('click', this.handleClickOnSubitem);
subLink.addEventListener('keyup', this.handleKeyboard);
}
if (this.attachFocusListener && subLink) {
subLink.addEventListener('focusout', this.handleFocusOut);
}
});
}
const seeAllLinks = queryAll('.ecl-mega-menu__see-all a', this.element);
if (seeAllLinks.length > 0) {
seeAllLinks.forEach((seeAll) => {
seeAll.addEventListener('keyup', this.handleKeyboard);
seeAll.addEventListener('blur', this.handleFocusOut);
});
}
// Bind global keyboard events
if (this.attachKeyListener) {
document.addEventListener('keyup', this.handleKeyboardGlobal);
}
// Bind resize events
if (this.attachResizeListener) {
window.addEventListener('resize', this.handleResize);
}
// Browse first level items
if (this.items) {
this.items.forEach((item) => {
// Check menu item display (right to left, full width, ...)
this.totalItemsWidth += item.offsetWidth;
if (
item.hasAttribute('data-ecl-has-children') ||
item.hasAttribute('data-ecl-has-container')
) {
// Bind click event on menu items
if (this.attachClickListener) {
item.addEventListener('click', this.handleClickOnItem);
}
}
});
}
// Create a focus trap around the menu
this.focusTrap = createFocusTrap(this.element, {
onActivate: () =>
this.element.classList.add('ecl-mega-menu-trap-is-active'),
onDeactivate: () =>
this.element.classList.remove('ecl-mega-menu-trap-is-active'),
});
this.handleResize();
// Set ecl initialized attribute
this.element.setAttribute('data-ecl-auto-initialized', 'true');
ECL.components.set(this.element, this);
}
/**
* Register a callback function for a specific event.
*
* @param {string} eventName - The name of the event to listen for.
* @param {Function} callback - The callback function to be invoked when the event occurs.
* @returns {void}
* @memberof MegaMenu
* @instance
*
* @example
* // Registering a callback for the 'onOpen' event
* megaMenu.on('onOpen', (event) => {
* console.log('Open event occurred!', event);
* });
*/
on(eventName, callback) {
this.eventManager.on(eventName, callback);
}
/**
* Trigger a component event.
*
* @param {string} eventName - The name of the event to trigger.
* @param {any} eventData - Data associated with the event.
* @memberof MegaMenu
*/
trigger(eventName, eventData) {
this.eventManager.trigger(eventName, eventData);
}
/**
* Destroy component.
*/
destroy() {
if (this.attachClickListener) {
if (this.open) {
this.open.removeEventListener('click', this.handleClickOnToggle);
}
if (this.back) {
this.back.removeEventListener('click', this.handleClickOnBack);
}
if (this.attachClickListener) {
document.removeEventListener('click', this.handleClickGlobal);
}
}
if (this.items && this.isDesktop) {
this.items.forEach((item) => {
if (
item.hasAttribute('data-ecl-has-children') ||
item.hasAttribute('data-ecl-has-container')
) {
if (this.attachClickListener) {
item.removeEventListener('click', this.handleClickOnItem);
}
}
});
}
if (this.links) {
this.links.forEach((link) => {
if (this.attachFocusListener) {
link.removeEventListener('focusout', this.handleFocusOut);
}
if (this.attachKeyListener) {
link.removeEventListener('keyup', this.handleKeyboard);
}
});
}
if (this.subItems) {
this.subItems.forEach((subItem) => {
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
if (this.attachKeyListener && subLink) {
subLink.removeEventListener('keyup', this.handleKeyboard);
}
if (this.attachClickListener && subLink) {
subLink.removeEventListener('click', this.handleClickOnSubitem);
}
if (this.attachFocusListener && subLink) {
subLink.removeEventListener('focusout', this.handleFocusOut);
}
});
}
if (this.attachKeyListener) {
document.removeEventListener('keyup', this.handleKeyboardGlobal);
}
if (this.attachResizeListener) {
window.removeEventListener('resize', this.handleResize);
}
this.closeOpenDropdown();
this.enableScroll();
if (this.element) {
this.element.removeAttribute('data-ecl-auto-initialized');
ECL.components.delete(this.element);
}
}
/**
* Disable page scrolling
*/
disableScroll() {
document.body.classList.add('ecl-mega-menu-prevent-scroll');
}
/**
* Enable page scrolling
*/
enableScroll() {
document.body.classList.remove('ecl-mega-menu-prevent-scroll');
}
/**
* Check if desktop display has to be used
* - not using a phone or tablet (whatever the screen size is)
* - not having hamburger menu on screen
*/
useDesktopDisplay() {
// Detect mobile devices
if (isMobile.isMobileOnly) {
return false;
}
// Force mobile display on tablet
if (isMobile.isTablet) {
this.element.classList.add('ecl-mega-menu--forced-mobile');
return false;
}
// After all that, check if the hamburger button is displayed
if (window.innerWidth < this.breakpointL) {
return false;
}
// Everything is fine to use desktop display
this.element.classList.remove('ecl-mega-menu--forced-mobile');
return true;
}
/**
* Reset the styles set by the script
*
* @param {string} desktop or mobile
*/
resetStyles(viewport) {
const subLists = queryAll('.ecl-mega-menu__sublist', this.element);
// Remove display:none from the sublists
if (subLists && viewport === 'mobile') {
subLists.forEach((list) => {
list.style.height = '';
});
// Two panels are opened
if (this.openPanel.num === 2) {
const menuItem = this.openPanel.item;
// Hide parent link in the first panel
menuItem.parentNode.parentNode.firstElementChild.style.display = 'none';
// Remove duplicated border
menuItem.parentNode.classList.add('ecl-mega-menu__sublist--no-border');
// Hide siblings
const siblings = menuItem.parentNode.childNodes;
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = 'none';
}
});
}
} else if (subLists && viewport === 'desktop') {
const parentLinks = queryAll('.ecl-mega-menu__parent-link');
if (parentLinks) {
// Reset the display for the parent links, they could be hidden
parentLinks.forEach((parent) => {
parent.style.display = '';
});
}
// Reset styles for the sublist and subitems
subLists.forEach((list) => {
if (!this.isLarge) {
list.parentNode.classList.remove('ecl-mega-menu__item--col2');
}
list.classList.remove('ecl-mega-menu__sublist--no-border');
list.childNodes.forEach((item) => {
item.style.display = '';
});
});
// Check if we have an open item, if we don't hide the overlay and enable scroll
const currentItems = [];
const currentItem = queryOne(
'.ecl-mega-menu__subitem--expanded',
this.element,
);
if (currentItem) {
currentItems.push(currentItem);
}
const currentSubItem = queryOne(
'.ecl-mega-menu__item--expanded',
this.element,
);
if (currentSubItem) {
currentItems.push(currentSubItem);
}
if (currentItems.length > 0) {
currentItems.forEach((current) => {
this.checkDropdownHeight(current);
});
} else {
this.element.setAttribute('aria-expanded', 'false');
this.element.removeAttribute('data-expanded');
this.enableScroll();
}
}
}
/**
* Trigger events on resize
* Uses a debounce, for performance
*/
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
const screenWidth = window.innerWidth;
if (this.prevScreenWidth !== undefined) {
// Check if the transition involves crossing the L breakpoint
const isTransition =
(this.prevScreenWidth <= this.breakpointL &&
screenWidth > this.breakpointL) ||
(this.prevScreenWidth > this.breakpointL &&
screenWidth <= this.breakpointL);
// If we are moving in or out the L breakpoint, reset the styles
if (isTransition) {
this.resetStyles(
screenWidth > this.breakpointL ? 'desktop' : 'mobile',
);
}
}
this.isDesktop = this.useDesktopDisplay();
this.isLarge = window.innerWidth > 1140;
// Update previous screen width
this.prevScreenWidth = screenWidth;
this.element.classList.remove('ecl-mega-menu--forced-mobile');
// RTL
this.direction = getComputedStyle(this.element).direction;
if (this.direction === 'rtl') {
this.element.classList.add('ecl-mega-menu--rtl');
} else {
this.element.classList.remove('ecl-mega-menu--rtl');
}
// Check droopdown height if needed
const expanded = queryOne('.ecl-mega-menu__item--expanded', this.element);
if (expanded && this.isDesktop) {
this.checkDropdownHeight(expanded);
}
if (this.openPanel.num === 2 && this.openPanel.item) {
this.checkMegaMenu(this.openPanel.item);
}
// Check the menu position
this.positionMenuOverlay();
}, 200);
}
/**
* Calculate dropdown height dynamically
*
* @param {Node} menuItem
*/
checkDropdownHeight(menuItem) {
setTimeout(() => {
const viewportHeight = window.innerHeight;
let dropdown = queryOne('.ecl-mega-menu__sublist', menuItem);
if (!dropdown) {
dropdown = queryOne('.ecl-mega-menu__mega-container', menuItem);
}
if (dropdown) {
const dropdownTop = dropdown.getBoundingClientRect().top;
let dropdownHeight = viewportHeight - dropdownTop;
const lastItem = queryOne('.ecl-mega-menu__see-all', dropdown);
// Arbitrary, but doing this prevents a misalignment between the two panels
if (lastItem) {
dropdownHeight -= 20;
}
dropdown.style.height = `${dropdownHeight}px`;
}
}, 100);
}
/**
* Dinamically set the position of the menu overlay
*/
positionMenuOverlay() {
const menuOverlay = queryOne('.ecl-mega-menu__overlay', this.element);
const megaMenus = queryAll(
'.ecl-mega-menu__item > .ecl-mega-menu__mega',
this.element,
);
if (!this.isDesktop) {
// In mobile, we get the bottom position of the site header header
setTimeout(() => {
const header = queryOne('.ecl-site-header__header', document);
if (header) {
const position = header.getBoundingClientRect();
const bottomPosition = Math.round(position.bottom);
if (menuOverlay) {
menuOverlay.style.top = `${bottomPosition}px`;
}
if (this.inner) {
this.inner.style.top = `${bottomPosition}px`;
}
if (megaMenus) {
megaMenus.forEach((mega) => {
mega.style.top = '';
});
}
}
}, 0);
} else {
setTimeout(() => {
// In desktop we get the bottom position of the whole site header
const siteHeader = queryOne('.ecl-site-header', document);
if (siteHeader) {
const headerRect = siteHeader.getBoundingClientRect();
const headerBottom = headerRect.bottom;
const item = queryOne(this.itemSelector, this.element);
const rect = item.getBoundingClientRect();
const rectHeight = rect.height + 4; // 4 pixels border
if (megaMenus) {
megaMenus.forEach((mega) => {
mega.style.top = `${rectHeight}px`;
});
}
if (menuOverlay) {
menuOverlay.style.top = `${headerBottom}px`;
}
} else {
const bottomPosition = this.element.getBoundingClientRect().bottom;
if (menuOverlay) {
menuOverlay.style.top = `${bottomPosition}px`;
}
if (megaMenus) {
megaMenus.forEach((mega) => {
mega.style.top = `${bottomPosition}px`;
});
}
}
}, 0);
}
}
/**
* Clone the selected item to show it on top of the panel.
*
* @param {Node} menuItem
*/
cloneItemInTheDrowdown(menuItem) {
const firstItemLink = queryOne('.ecl-link', menuItem).cloneNode(true);
const svg = queryOne('.ecl-icon use', firstItemLink);
if (svg) {
const hrefValue = svg.getAttribute('xlink:href');
if (hrefValue) {
const newHrefValue = hrefValue.replace('corner-arrow', 'arrow-left');
svg.parentElement.classList.add(
'ecl-icon--flip-horizontal',
'ecl-icon--xs',
);
svg.parentElement.classList.remove(
'ecl-icon--2xs',
'ecl-icon--rotate-180',
);
svg.setAttributeNS(
'http://www.w3.org/1999/xlink',
'xlink:href',
newHrefValue,
);
}
if (firstItemLink.id) {
firstItemLink.id = `${firstItemLink.id}-parent`;
}
const ariaLabel = menuItem.getAttribute('data-ecl-parent-aria-label');
if (ariaLabel) {
firstItemLink.setAttribute('aria-label', ariaLabel);
}
firstItemLink.classList.add('ecl-mega-menu__parent-link');
firstItemLink.addEventListener('keyup', this.handleKeyboard);
let innerList = queryOne('.ecl-mega-menu__mega', menuItem);
if (!innerList) {
innerList = queryOne('.ecl-mega-menu__mega-container', menuItem);
}
if (innerList && !queryOne('.ecl-mega-menu__parent-link', menuItem)) {
innerList.prepend(firstItemLink);
}
}
}
/**
* Handle second panel columns
*
* @param {Node} menuItem
*/
checkMegaMenu(menuItem) {
const menuMega = queryOne(this.megaSelector, menuItem);
if (menuMega && this.inner && this.isLarge) {
const subItems = queryAll(`${this.subItemSelector} a`, menuMega);
let itemsHeight = 0;
subItems.forEach((item) => {
itemsHeight += item.getBoundingClientRect().height;
});
const lastItem = queryOne('.ecl-mega-menu__see-all', menuMega);
if (lastItem) {
// Arbitrary, but does the job.
itemsHeight += 150;
}
const containerBounding = this.inner.getBoundingClientRect();
const containerBottom = containerBounding.bottom;
const availableHeight = window.innerHeight - containerBottom;
if (itemsHeight > availableHeight) {
menuMega.classList.add('ecl-mega-menu__item--col2');
}
} else if (menuMega) {
menuMega.classList.remove('ecl-mega-menu__item--col2');
}
}
/**
* Handles keyboard events specific to the menu.
*
* @param {Event} e
*/
handleKeyboard(e) {
const element = e.target;
const cList = element.classList;
const menuExpanded = this.element.getAttribute('aria-expanded');
// Detect press on Escape
if (e.key === 'Escape' || e.key === 'Esc') {
if (document.activeElement === element) {
element.blur();
}
if (menuExpanded === 'false') {
this.closeOpenDropdown();
}
return;
}
// Handle keyboard on parent links
if (cList.contains('ecl-mega-menu__parent-link')) {
if (e.key === 'ArrowUp') {
if (this.isDesktop) {
// Focus the first level menu item
element
.closest('.ecl-mega-menu__item--expanded')
.firstElementChild.focus();
} else {
// In mobile focus on the back button
this.back.focus();
}
}
if (e.key === 'ArrowDown') {
// Focus on the first sub-link
element.nextSibling.firstElementChild.firstElementChild.focus();
}
}
// Handle keyboard on the see all links
if (element.parentElement.classList.contains('ecl-mega-menu__see-all')) {
if (e.key === 'ArrowUp') {
// Focus on the last element of the sub-list
element.parentElement.previousSibling.firstChild.focus();
}
}
// Handle keyboard on the back button
if (cList.contains('ecl-mega-menu__back')) {
if (e.key === 'ArrowDown') {
e.preventDefault();
const expanded = queryOne(
'[aria-expanded="true"]',
element.parentElement.nextSibling,
);
// We have an opened list
if (expanded) {
const innerExpanded = queryOne('[aria-expanded="true"]', expanded);
// We have an opened sub-list
if (innerExpanded) {
queryOne('.ecl-mega-menu__parent-link', innerExpanded).focus();
} else {
queryOne('.ecl-mega-menu__parent-link', expanded).focus();
}
}
}
if (e.key === 'ArrowUp') {
// Focus on the open button
this.open.focus();
}
}
// Key actions to navigate between first level menu items
if (cList.contains('ecl-mega-menu__link')) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
let prevItem = element.previousSibling;
if (prevItem && prevItem.classList.contains('ecl-mega-menu__link')) {
prevItem.focus();
return;
}
prevItem = element.parentElement.previousSibling;
if (prevItem) {
const prevLink = queryOne('.ecl-mega-menu__link', prevItem);
if (prevLink) {
prevLink.focus();
return;
}
}
}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
if (
element.parentElement.getAttribute('aria-expanded') === 'true' &&
e.key === 'ArrowDown'
) {
const parentLink = queryOne(
'.ecl-mega-menu__parent-link',
element.parentElement,
);
if (parentLink) {
parentLink.focus();
return;
}
}
const nextItem = element.parentElement.nextSibling;
if (nextItem) {
const nextLink = queryOne('.ecl-mega-menu__link', nextItem);
if (nextLink) {
nextLink.focus();
return;
}
}
}
}
// Key actions to navigate between the sub-links
if (cList.contains('ecl-mega-menu__sublink')) {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextItem = element.parentElement.nextSibling;
let nextLink = '';
if (nextItem) {
nextLink = queryOne('.ecl-mega-menu__sublink', nextItem);
if (
!nextLink &&
nextItem.classList.contains('ecl-mega-menu__see-all')
) {
nextLink = nextItem.firstElementChild;
}
if (nextLink) {
nextLink.focus();
return;
}
}
}
if (e.key === 'ArrowUp') {
e.preventDefault();
const prevItem = element.parentElement.previousSibling;
if (prevItem) {
const prevLink = queryOne('.ecl-mega-menu__sublink', prevItem);
if (prevLink) {
prevLink.focus();
}
} else {
element.parentElement.parentElement.previousSibling.focus();
}
}
}
if (e.key === 'ArrowRight') {
const expanded =
element.parentElement.getAttribute('aria-expanded') === 'true';
if (expanded) {
e.preventDefault();
// Focus on the first element in the second panel
element.nextSibling.firstElementChild.focus();
}
}
}
/**
* Handles global keyboard events, triggered outside of the menu.
*
* @param {Event} e
*/
handleKeyboardGlobal(e) {
const menuExpanded = this.element.getAttribute('aria-expanded');
// Detect press on Escape
if (e.key === 'Escape' || e.key === 'Esc') {
if (menuExpanded === 'true') {
this.closeOpenDropdown();
}
}
}
/**
* Open menu list.
*
* @param {Event} e
*
* @fires MegaMenu#onOpen
*/
handleClickOnOpen(e) {
if (this.element.getAttribute('aria-expanded') === 'true') {
this.handleClickOnClose(e);
} else {
e.preventDefault();
this.disableScroll();
this.element.setAttribute('aria-expanded', 'true');
this.inner.setAttribute('aria-hidden', 'false');
this.isOpen = true;
this.openPanel.num = 1;
// Update label
const closeLabel = this.element.getAttribute(this.labelCloseAttribute);
if (this.toggleLabel && closeLabel) {
this.toggleLabel.innerHTML = closeLabel;
}
this.trigger('onOpen', e);
}
}
/**
* Close menu list.
*
* @param {Event} e
*
* @fires Menu#onClose
*/
handleClickOnClose(e) {
if (this.element.getAttribute('aria-expanded') === 'true') {
this.focusTrap.deactivate();
this.closeOpenDropdown();
this.trigger('onClose', e);
} else {
this.handleClickOnOpen(e);
}
}
/**
* Toggle menu list.
*
* @param {Event} e
*/
handleClickOnToggle(e) {
e.preventDefault();
if (this.isOpen) {
this.handleClickOnClose(e);
} else {
this.handleClickOnOpen(e);
}
}
/**
* Get back to previous list (on mobile)
*
* @fires MegaMenu#onBack
*/
handleClickOnBack() {
const level2 = queryOne('.ecl-mega-menu__subitem--expanded', this.element);
if (level2) {
const parentLinks = queryAll('.ecl-mega-menu__parent-link', this.element);
if (parentLinks) {
parentLinks.forEach((parent) => {
parent.style.display = '';
});
}
const sublists = queryAll('.ecl-mega-menu__sublist');
if (sublists) {
sublists.forEach((sublist) => {
sublist.classList.remove('ecl-mega-menu__sublist--no-border');
});
}
level2.setAttribute('aria-expanded', 'false');
level2.classList.remove(
'ecl-mega-menu__subitem--expanded',
'ecl-mega-menu__subitem--current',
);
const siblings = level2.parentElement.childNodes;
if (siblings) {
siblings.forEach((sibling) => {
sibling.style.display = '';
});
}
// Move focus on the parent link of the opened list
const expanded = queryOne('.ecl-mega-menu__item--expanded', this.element);
queryOne('.ecl-mega-menu__parent-link', expanded).focus();
this.openPanel.num = 1;
} else {
// Remove expanded class from inner menu
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
// Remove css class and attribute from menu items
this.items.forEach((item) => {
item.classList.remove(
'ecl-mega-menu__item--expanded',
'ecl-mega-menu__item--current',
);
item.setAttribute('aria-expanded', 'false');
});
// Move the focus to the first item in the menu
this.items[0].firstElementChild.focus();
this.openPanel.num = 0;
}
this.trigger('onBack', { level: level2 ? 2 : 1 });
}
/**
* Show/hide the first panel
*
* @param {Node} menuItem
* @param {string} op (expand or collapse)
*
* @fires MegaMenu#onOpenPanel
*/
handleFirstPanel(menuItem, op) {
switch (op) {
case 'expand': {
this.inner.classList.add('ecl-mega-menu__inner--expanded');
this.positionMenuOverlay();
this.cloneItemInTheDrowdown(menuItem);
this.checkDropdownHeight(menuItem);
this.element.setAttribute('data-expanded', true);
this.element.setAttribute('aria-expanded', 'true');
this.disableScroll();
this.items.forEach((item) => {
if (item.hasAttribute('aria-expanded')) {
if (item === menuItem) {
item.classList.add(
'ecl-mega-menu__item--expanded',
'ecl-mega-menu__item--current',
);
item.setAttribute('aria-expanded', 'true');
} else {
item.setAttribute('aria-expanded', 'false');
item.classList.remove(
'ecl-mega-menu__item--current',
'ecl-mega-menu__item--expanded',
);
}
}
});
queryOne('.ecl-mega-menu__parent-link', menuItem).focus();
const details = { panel: 1, item: menuItem };
this.trigger('OnOpenPanel', details);
break;
}
case 'collapse':
this.closeOpenDropdown();
break;
default:
}
}
/**
* Show/hide the second panel
*
* @param {Node} menuItem
* @param {string} op (expand or collapse)
*
* @fires MegaMenu#onOpenPanel
*/
handleSecondPanel(menuItem, op) {
let siblings;
switch (op) {
case 'expand': {
this.subItems.forEach((item) => {
if (item === menuItem) {
if (item.hasAttribute('aria-expanded')) {
item.setAttribute('aria-expanded', 'true');
item.classList.add('ecl-mega-menu__subitem--expanded');
}
item.classList.add('ecl-mega-menu__subitem--current');
} else {
if (item.hasAttribute('aria-expanded')) {
item.setAttribute('aria-expanded', 'false');
item.classList.remove('ecl-mega-menu__subitem--expanded');
}
item.classList.remove('ecl-mega-menu__subitem--current');
}
});
this.openPanel = { num: 2, item: menuItem };
siblings = menuItem.parentNode.childNodes;
if (this.isDesktop) {
this.checkDropdownHeight(menuItem);
// Reset style for the siblings, in case they were hidden
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = '';
}
});
} else {
// Hide parent link of the first panel
menuItem.parentNode.parentNode.firstElementChild.style.display =
'none';
// Remove double border, we have two sublists opened
menuItem.parentNode.classList.add(
'ecl-mega-menu__sublist--no-border',
);
// Hide other items in the sublist
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = 'none';
}
});
}
queryOne('.ecl-mega-menu__parent-link', menuItem).focus();
this.checkMegaMenu(menuItem);
const details = { panel: 2, item: menuItem };
this.trigger('OnOpenPanel', details);
break;
}
case 'collapse':
this.openPanel = { num: 1 };
menuItem.setAttribute('aria-expanded', 'false');
menuItem.classList.remove(
'ecl-mega-menu__subitem--expanded',
'ecl-mega-menu__subitem--current',
);
break;
default:
}
}
/**
* Click on a menu item
*
* @param {Event} e
*
* @fires MegaMenu#onItemClick
*/
handleClickOnItem(e) {
let isInTheContainer = false;
const menuItem = e.target.closest('li');
const container = queryOne(
'.ecl-mega-menu__mega-container-scrollable',
menuItem,
);
if (container) {
isInTheContainer = container.contains(e.target);
}
// We need to ensure that the click doesn't come from a parent link
// or from an open container, in that case we do not act.
if (
!e.target.parentNode.classList.contains('ecl-mega-menu__parent-link') &&
!e.target.classList.contains(
'ecl-mega-menu__mega-container-scrollable',
) &&
!isInTheContainer
) {
this.trigger('onItemClick', { item: menuItem, event: e });
const hasChildren = menuItem.getAttribute('aria-expanded');
if (hasChildren && menuItem.classList.contains('ecl-mega-menu__item')) {
e.preventDefault();
if (!this.isDesktop) {
this.handleFirstPanel(menuItem, 'expand');
} else {
const isExpandable = hasChildren === 'true';
if (isExpandable) {
this.handleFirstPanel(menuItem, 'collapse');
} else {
this.closeOpenDropdown();
this.handleFirstPanel(menuItem, 'expand');
}
}
}
}
}
/**
* Click on a subitem
*
* @param {Event} e
*/
handleClickOnSubitem(e) {
const menuItem = e.target.closest(this.subItemSelector);
if (menuItem && menuItem.hasAttribute('aria-expanded')) {
e.preventDefault();
const isExpanded = menuItem.getAttribute('aria-expanded') === 'true';
this.cloneItemInTheDrowdown(menuItem);
if (isExpanded) {
this.handleSecondPanel(menuItem, 'collapse');
} else {
this.handleSecondPanel(menuItem, 'expand');
}
}
}
/**
* Deselect any opened menu item
*
* @fires MegaMenu#onFocusTrapToggle
*/
closeOpenDropdown() {
this.enableScroll();
this.element.setAttribute('aria-expanded', 'false');
this.element.removeAttribute('data-expanded');
// Remove css class and attribute from inner menu
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
this.inner.setAttribute('aria-hidden', 'true');
// Remove css class and attribute from menu items
this.items.forEach((item) => {
item.classList.remove('ecl-mega-menu__item--current');
if (item.hasAttribute('aria-expanded')) {
item.setAttribute('aria-expanded', 'false');
item.classList.remove('ecl-mega-menu__item--expanded');
}
});
// Remove css class and attribute from menu subitems
this.subItems.forEach((item) => {
item.classList.remove('ecl-mega-menu__subitem--current');
if (item.hasAttribute('aria-expanded')) {
item.classList.remove('ecl-mega-menu__subitem--expanded');
item.setAttribute('aria-expanded', 'false');
item.style.display = '';
}
});
// Remove styles set for the sublists
const sublists = queryAll('.ecl-mega-menu__sublist');
if (sublists) {
sublists.forEach((sublist) => {
sublist.classList.remove('ecl-mega-menu__sublist--no-border');
});
}
// Remove styles set for the parent links
const parentLinks = queryAll('.ecl-mega-menu__parent-link', this.element);
if (parentLinks) {
parentLinks.forEach((parent) => {
parent.style.display = '';
});
}
// Update label
const openLabel = this.element.getAttribute(this.labelOpenAttribute);
if (this.toggleLabel && openLabel) {
this.toggleLabel.innerHTML = openLabel;
}
this.openPanel.num = 0;
// If the focus trap is active, deactivate it
this.focusTrap.deactivate();
this.trigger('onFocusTrapToggle', { active: false });
this.isOpen = false;
}
/**
* Focus out of a menu link
*
* @param {Event} e
*
* @fires MegaMenu#onFocusTrapToggle
*/
handleFocusOut(e) {
const element = e.target;
const menuExpanded = this.element.getAttribute('aria-expanded');
// Specific focus action for mobile menu
// Loop through the items and go back to close button
if (menuExpanded === 'true') {
const nextItem = element.parentElement.nextSibling;
if (!nextItem) {
const nextFocusTarget = e.relatedTarget;
if (!this.element.contains(nextFocusTarget)) {
// This is the last item, go back to close button
this.focusTrap.activate();
this.trigger('onFocusTrapToggle', {
active: true,
lastFocusedEl: element.parentElement,
});
}
}
}
}
/**
* Handles global click events, triggered outside of the menu.
*
* @param {Event} e
*/
handleClickGlobal(e) {
if (
!e.target.classList.contains(
'ecl-mega-menu__mega-container-scrollable',
) &&
(e.target.classList.contains('ecl-mega-menu__overlay') ||
!this.element.contains(e.target))
) {
this.closeOpenDropdown();
}
}
}
export default MegaMenu;