export interface ShortcutLinks { shortcut_title: string; shortcut_url: string; shortcut_is_external: string; shortcut_aria_label: string; } export interface DesktopLinks { label: string; labelExtra: string; url: string; description: string; menuLinkId: string; isActive: string; shortcutsTitle: string; closeButtonTitle: string; items: Array<DesktopLinks>; shortcuts: Array<ShortcutLinks>; } import {ColorVariant} from '../../../utils/utils'; import {Component, h, Element, Prop, State, Watch} from '@stencil/core'; @Component({ tag: 'hy-desktop-menu-links', styleUrl: 'hy-desktop-menu-links.scss', shadow: true, }) export class HyDesktopMenuLinks { @Element() el: HTMLElement; /* First level menu links to be displayed on Desktop screens. * */ @Prop() dataDesktopLinks: DesktopLinks[] | string; private _dataDesktopLinks: DesktopLinks[]; @State() firstLevelLinksList: Array<object> = []; @State() menuLinkItems: Array<object> = []; @State() hasToolbar: boolean = false; @State() isDesktopMenuOpen: boolean = false; @State() currenOpenMenuId: number = 0; private _submenuLeftMargin: number = 32; private _headerBorderOffset: number = 0; private _hoverTimer = null; @Watch('dataDesktopLinks') dataDesktopLinksWatcher(data: DesktopLinks[] | string) { this._dataDesktopLinks = typeof data === 'string' ? JSON.parse(data) : data; } componentWillLoad() { this.dataDesktopLinksWatcher(this.dataDesktopLinks); } showBackdropShadow(state = 'close', top = 0) { let hyHeader = this.el.closest('.hy-site-header') as HTMLElement; let hyBackdropDiv = hyHeader.children[0] as HTMLElement; if (hyBackdropDiv) { if (state === 'open') { hyBackdropDiv.classList.add('is-active'); hyBackdropDiv.style.top = `${top}px`; } else { hyBackdropDiv.style.top = '0'; hyBackdropDiv.classList.remove('is-active'); } } } showPanel(id) { // Close menu lang menu if it's open //this.closeLangMenu(); // Open desktop menu panel this.isDesktopMenuOpen = true; const menuItems = this.el.shadowRoot.querySelectorAll(`.desktop-menu-link`); const menuPanelItems = this.el.shadowRoot.querySelectorAll('.hy-desktop-menu-panel'); // all panels const activeMenuItem = this.el.shadowRoot.querySelector(`.desktop-menu-link[link-id="${id}"]`) as HTMLElement; const activeMenuItemSibling = activeMenuItem.nextElementSibling as HTMLElement; // current panel // Reset elements by removing the active classes. menuItems.forEach((item) => { item.classList.remove('desktop-menu-link--is-active'); item.setAttribute('aria-expanded', 'false'); }); menuPanelItems.forEach((item) => { item.classList.remove('hy-desktop-menu-panel--is-active'); item.setAttribute('aria-hidden', 'true'); (item as HTMLElement).style.transition = 'none'; }); // Add active classes to the currently active item and its sibling element. activeMenuItem.classList.add('desktop-menu-link--is-active'); activeMenuItem.setAttribute('aria-expanded', 'true'); activeMenuItemSibling.classList.add('hy-desktop-menu-panel--is-active'); (activeMenuItemSibling as HTMLElement).style.opacity = '1'; if (this.hasToolbar) { activeMenuItemSibling.classList.add('hy-desktop-menu-panel--is-active--has-toolbar'); } activeMenuItemSibling.setAttribute('aria-hidden', 'false'); //Hide is-active-trail underlining const activeTrailMenuItem = this.el.shadowRoot.querySelector( `.desktop-menu-link__label--is-active-trail` ) as HTMLElement; if (activeTrailMenuItem) { activeTrailMenuItem.classList.add('desktop-menu-link__label--is-active-trail--disabled'); } // Add panels top value automatically with the correct header height const headerHeight = this.el.parentElement.classList.contains('hy-site-header--sticky-active') ? `${this.el.parentElement.offsetHeight + this._headerBorderOffset}px` : `${this.el.parentElement.offsetTop + this.el.parentElement.offsetHeight + this._headerBorderOffset}px`; console.log(this.el.parentElement.offsetTop + ' ' + this.el.parentElement.offsetHeight); activeMenuItemSibling.style.top = headerHeight; // Add shadow backdrop let rect = activeMenuItemSibling.getBoundingClientRect(); this.showBackdropShadow('open', rect.bottom); // Position submenu block under the activated top menu item. let activeButtonRect = activeMenuItem.getBoundingClientRect(); const menuPanelLeftPosition = activeButtonRect.left - this._submenuLeftMargin; activeMenuItemSibling.style.paddingLeft = `${menuPanelLeftPosition}px`; // Position shortcuts block. let shortcutsDiv = activeMenuItemSibling.querySelectorAll('ul.shortcuts-panel')[0] as HTMLElement; // shortcuts block if (shortcutsDiv) { let subMenuDiv = activeMenuItemSibling.querySelectorAll( '.hy-desktop-menu-panel__desktop-menu__menu-items' )[0] as HTMLElement; // 2nd level menu block let spaceLeftAfterSubmenu = subMenuDiv.getBoundingClientRect().right + shortcutsDiv.offsetWidth; if (spaceLeftAfterSubmenu >= document.body.scrollWidth) { // Shortcuts should be placed to the left. let shortcutsLeftPosition = subMenuDiv.getBoundingClientRect().left - shortcutsDiv.offsetWidth; shortcutsDiv.style.left = shortcutsLeftPosition.toString().concat('px'); } else { // Shortcuts should be placed to the right. let shortcutsLeftPosition = subMenuDiv.getBoundingClientRect().right; shortcutsDiv.style.left = shortcutsLeftPosition.toString().concat('px'); } } } closePanel(fadeOut = false) { this.isDesktopMenuOpen = false; this.currenOpenMenuId = 0; this.showBackdropShadow(); this.clearPanelItemsStatus(fadeOut); clearTimeout(this._hoverTimer); } clearPanelItemsStatus(fadeOut = false) { const menuItems = this.el.shadowRoot.querySelectorAll(`.desktop-menu-link`); const menuPanelItems = this.el.shadowRoot.querySelectorAll('.hy-desktop-menu-panel'); //Show is-active-trail underlining const activeTrailMenuItem = this.el.shadowRoot.querySelector( `.desktop-menu-link__label--is-active-trail` ) as HTMLElement; if (activeTrailMenuItem) { activeTrailMenuItem.classList.remove('desktop-menu-link__label--is-active-trail--disabled'); } // Reset elements by removing the active classes. menuItems.forEach((item) => { item.classList.remove('desktop-menu-link--is-active'); item.setAttribute('aria-expanded', 'false'); }); menuPanelItems.forEach((item) => { item.classList.remove('hy-desktop-menu-panel--is-active'); item.setAttribute('aria-hidden', 'true'); (item as HTMLElement).style.opacity = '0'; if (fadeOut) { (item as HTMLElement).style.transition = 'opacity 1s'; } }); } handleDesktopMenuClose(event) { let fadeOut = true; this.closePanel(fadeOut); event.stopPropagation(); } handleDesktopMenuEnter(event, id) { this._hoverTimer = setTimeout(() => { const activeMenuItem = this.el.shadowRoot.querySelector(`.desktop-menu-link[link-id="${id}"]`) as HTMLElement; // Set focus to the button. if (activeMenuItem !== null) activeMenuItem.focus(); this.currenOpenMenuId = id; this.showPanel(id); }, 350); event.stopPropagation(); } handleDesktopMenuLeave(event) { let leaveEvent = event as MouseEvent; let hyHeader = this.el.closest('.hy-site-header') as HTMLElement; const headerHeight = hyHeader.offsetTop + hyHeader.offsetHeight; if (leaveEvent.clientY < headerHeight - 4) { this.closePanel(); } event.stopPropagation(); } handleDesktopMenuMove(event, id) { let toggleEvent = event as MouseEvent; const activeMenuItem = this.el.shadowRoot.querySelector( `.desktop-menu-link[link-id="${id}"] .desktop-menu-link__label` ) as HTMLElement; let topBorder = activeMenuItem.getClientRects()[0].top; if (this.currenOpenMenuId == id) { // Mouse moving around the same menu link if (toggleEvent.clientY < topBorder) { this.closePanel(); } } event.stopPropagation(); } handleDesktopMenuFocus(event, id) { if (this.currenOpenMenuId != id) { this.currenOpenMenuId = id; this.showPanel(id); } event.stopPropagation(); } handleDesktopMenuClick(event, id) { if (!this.isDesktopMenuOpen) { this.currenOpenMenuId = id; this.showPanel(id); } else { this.handleDesktopMenuClose(event); } event.stopPropagation(); } componentDidLoad() { let hyToolbar = document.querySelectorAll('#toolbar-administration')[0]; if (hyToolbar) { this.hasToolbar = true; } const links = this._dataDesktopLinks as Array<DesktopLinks>; let menuLinkItems = []; if (links) { links.map( ({ menuLinkId: id, shortcuts, items, url, description, label, labelExtra, isActive, shortcutsTitle, closeButtonTitle, }) => { let classAttributes = [ 'desktop-menu-link', isActive === 'true' ? 'desktop-menu-link--is-active-trail' : '', ].join(' '); let classAttributesLabel = [ 'desktop-menu-link__label', isActive === 'true' ? 'desktop-menu-link__label--is-active-trail' : '', ].join(' '); menuLinkItems.push( <li> <button type="button" class={classAttributes} link-id={id} onClick={(e) => this.handleDesktopMenuClick(e, id)} onFocus={(e) => this.handleDesktopMenuFocus(e, id)} onMouseEnter={(e) => this.handleDesktopMenuEnter(e, id)} onMouseLeave={(e) => this.handleDesktopMenuLeave(e)} onMouseMove={(e) => this.handleDesktopMenuMove(e, id)} aria-expanded="false" > <span class={classAttributesLabel}>{label}</span> <hy-icon icon={'hy-icon-caret-down'} size={32} /> </button> <div class="hy-desktop-menu-panel" onMouseLeave={(e) => this.handleDesktopMenuClose(e)} aria-hidden="true" > <div class="hy-desktop-menu-panel__desktop-menu"> <div class="hy-desktop-menu-panel__desktop-menu__menu-items"> <a aria-current={label} href={url} class="hy-desktop-menu-panel__desktop-menu__first-level-menu-item" menu-link-id={id} > <span class="heading-icon"> <hy-icon icon={'hy-icon-arrow-right'} size={40} /> </span> {labelExtra ? <span class="label">{labelExtra}</span> : <span class="label">{label}</span>} {description && <span class="description">{description}</span>} </a> <ul class={'hy-desktop-menu-panel__desktop-menu__second-level-menu'} menu-link-id={id}> {items.map(({label, url}) => ( <li> <a href={url}> <span class="heading-icon"> <hy-icon icon={'hy-icon-caret-right'} size={12} /> </span> <span class="label">{label}</span> </a> </li> ))} </ul> </div> {shortcuts.length > 0 && ( <ul class="shortcuts-panel"> <h2 class="shortcuts-panel__title">{shortcutsTitle}</h2> {shortcuts.map( ({shortcut_title, shortcut_url, shortcut_is_external, shortcut_aria_label}, index) => { let target = shortcut_is_external ? '_blank' : '_self'; let shortcutClass = [ 'shortcuts-panel__shortcut-item', index == 0 ? 'shortcuts-panel__shortcut-item__first' : '', ].join(' '); return ( <li class={shortcutClass}> <a aria-current={shortcut_aria_label} href={shortcut_url} class="shortcut-item__link" target={target} aria-label={shortcut_aria_label} > <span class="label">{shortcut_title}</span> <span class="icon"> <hy-icon icon={'hy-icon-arrow-right'} size={24} /> </span> </a> </li> ); } )} </ul> )} </div> <button onClick={(e) => this.handleDesktopMenuClose(e)} class={{ 'hy-desktop-menu-panel__panel-toggle': true, }} aria-label="Close menu" > <span class="hy-desktop-menu-panel__panel-toggle__label"> <span class="hy-desktop-menu-panel__panel-toggle__label__title">{closeButtonTitle}</span> <hy-icon icon={'hy-icon-remove'} size={20} fill={ColorVariant.black} /> </span> </button> </div> </li> ); } ); } this.menuLinkItems = menuLinkItems; } render() { return ( <nav role={'navigation'} class="hy-site-header__menu-desktop"> <ul class="hy-site-header__menu-desktop-container">{this.menuLinkItems}</ul> </nav> ); } }