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; toggleOff: string; menuLinkId: string; isExternal: 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, EventEmitter, Event, Listen} from '@stencil/core'; import ResizeObserver from 'resize-observer-polyfill'; @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; @Event() menuDesktopToggled: EventEmitter; private _submenuLeftMargin: number = 32; private _headerBorderOffset: number = 0; private _hoverTimer = null; private _fadeOutTimer = null; private ro: ResizeObserver; @Watch('dataDesktopLinks') dataDesktopLinksWatcher(data: DesktopLinks[] | string) { this._dataDesktopLinks = typeof data === 'string' ? JSON.parse(data) : data; } componentWillLoad() { this.dataDesktopLinksWatcher(this.dataDesktopLinks); } removeBackdropShadow(size: number) { // Close backdrop shadow if the screen is < 1200px if (size < 1200) { this.showBackdropShadow(); } } showBackdropShadow(state = 'close', top = 0) { let hyHeader = this.el.closest('.hy-site-header') as HTMLElement; if (!hyHeader) return; let hyBackdropDiv = hyHeader.children[0] as HTMLElement; if (hyBackdropDiv) { if (state === 'open') { const windowHeight = window.outerHeight; hyBackdropDiv.style.height = `${windowHeight}px`; hyBackdropDiv.style.top = `${top}px`; hyBackdropDiv.style.position = 'absolute'; hyBackdropDiv.classList.add('is-active'); } else { hyBackdropDiv.removeAttribute('style'); hyBackdropDiv.classList.remove('is-active'); } } } showPanel(id) { // Close menu lang menu if it's open this.menuDesktopToggled.emit(); clearTimeout(this._fadeOutTimer); // 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.classList.remove('hy-desktop-menu-panel--overflow'); item.setAttribute('aria-hidden', 'true'); (item as HTMLElement).style.transition = 'none'; (item as HTMLElement).style.opacity = '0'; }); // 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 let topOffset = window.pageYOffset; const bodyElementClasses = document.body.classList; const headerElement = document.querySelector('hy-site-header'); const headerShadowRootElement = headerElement.shadowRoot.querySelector('header'); let headerElementHeight = headerShadowRootElement.offsetHeight + this._headerBorderOffset; let headerElementOffset = headerShadowRootElement.offsetTop; let panelElementOffset = headerElementHeight + headerElementOffset; // Add shadow backdrop let rect = activeMenuItemSibling.getBoundingClientRect(); let backdropOffset = headerElementHeight + headerElementOffset + rect.height; let backdropToolbarOffset = backdropOffset - 79; window.addEventListener('scroll', () => { topOffset = window.pageYOffset; if (topOffset === 0) { activeMenuItemSibling.style.top = `${panelElementOffset}px`; } if (headerShadowRootElement.classList.contains('hy-site-header--sticky-active') && topOffset > 0) { if (bodyElementClasses.contains('toolbar-horizontal')) { activeMenuItemSibling.style.top = `${headerElementHeight}px`; if (this.isDesktopMenuOpen) { this.showBackdropShadow('open', backdropToolbarOffset); } } } }); if (headerShadowRootElement.classList.contains('hy-site-header--sticky-active') && topOffset > 0) { if (bodyElementClasses.contains('toolbar-horizontal')) { activeMenuItemSibling.style.top = `${headerElementHeight}px`; if (this.isDesktopMenuOpen) { this.showBackdropShadow('open', backdropToolbarOffset); } } else { activeMenuItemSibling.style.top = `${panelElementOffset}px`; this.showBackdropShadow('open', backdropOffset); } } else { activeMenuItemSibling.style.top = `${panelElementOffset}px`; this.showBackdropShadow('open', backdropOffset); } // Position submenu block under the activated top menu item. const menuPanelLeftPosition = activeMenuItem.offsetLeft - 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'); }); if (fadeOut) { menuPanelItems.forEach((item) => { (item as HTMLElement).style.opacity = '0'; (item as HTMLElement).style.transition = 'opacity 1s'; }); this._fadeOutTimer = setTimeout(() => { menuPanelItems.forEach((item) => { item.classList.remove('hy-desktop-menu-panel--is-active'); item.setAttribute('aria-hidden', 'true'); }); }, 350); } else { menuPanelItems.forEach((item) => { item.classList.remove('hy-desktop-menu-panel--is-active'); item.setAttribute('aria-hidden', 'true'); (item as HTMLElement).style.opacity = '0'; (item as HTMLElement).style.transition = 'none'; }); } } handleDesktopMenuClose(event) { let fadeOut = true; this.closePanel(fadeOut); event.stopPropagation(); } // CLose the desktop menu panel if user opens the language menu. @Listen('menuLanguageToggled', {target: 'document'}) menuLanguageToggled() { let fadeOut = true; this.closePanel(fadeOut); } // Close the desktop menu panel if user opens University main menu @Listen('universityMainMenuToggled', {target: 'document'}) universityMainMenuPanelToggled() { let fadeOut = true; this.closePanel(fadeOut); } // Close the desktop menu panel if user opens search panel @Listen('searchPanelToggled', {target: 'document'}) searchPanelToggled() { let fadeOut = true; this.closePanel(fadeOut); } // CLose the desktop menu panel if user scrolls Sticky Header till the very top. @Listen('headerScrollUp', {target: 'document'}) headerScrollUp() { let fadeOut = false; this.closePanel(fadeOut); } handleDesktopMenuEnter(event, id) { clearTimeout(this._fadeOutTimer); 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(); } /* Close the panel if mouse is moving over the menu label. * */ handleDesktopMenuMove(event, id) { if (this.isDesktopMenuOpen) { let moveEvent = 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 (moveEvent.clientY < topBorder - 4) { 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() { // Set the browser resize observer to gather information about browser width. this.ro = new ResizeObserver((entries) => { for (const entry of entries) { this.removeBackdropShadow(entry.contentRect.width); } }); this.ro.observe(document.body); 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, toggleOff, label, labelExtra, isExternal, 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(' '); let target = isExternal ? '_blank' : '_self'; if (toggleOff == 'true') { menuLinkItems.push( <li onMouseEnter={(e) => this.handleDesktopMenuClose(e)}> <a aria-current={label} href={url} target={target} class="desktop-menu-link toggle" menu-link-id={id}> {labelExtra ? <span class="label">{labelExtra}</span> : <span class="label">{label}</span>} </a> </li> ); } else { menuLinkItems.push( <li> <button type="button" class={classAttributes} link-id={id} onMouseDown={(e) => this.handleDesktopMenuClick(e, id)} onFocus={(e) => this.handleDesktopMenuFocus(e, id)} onMouseEnter={(e) => this.handleDesktopMenuEnter(e, id)} 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" 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} target={target} 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, isExternal}) => { let subitemTarget = isExternal ? '_blank' : '_self'; return ( <li> <a href={url} target={subitemTarget}> <span class="heading-icon"> <hy-icon icon={'hy-icon-caret-right'} size={12} /> </span> <span class="label">{label}</span> {isExternal && ( <span class="external-icon"> <hy-icon icon={'hy-icon-arrow-right'} size={12} /> </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" onMouseLeave={(e) => this.handleDesktopMenuClose(e)} > <ul class="hy-site-header__menu-desktop-container">{this.menuLinkItems}</ul> </nav> ); } }