Skip to content
Snippets Groups Projects
hy-desktop-menu-links.tsx 18.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • export interface ShortcutLinks {
      shortcut_title: string;
      shortcut_url: string;
      shortcut_is_external: string;
      shortcut_aria_label: string;
    }
    
    
    export interface DesktopLinks {
      label: string;
    
      labelExtra: string;
    
      description: string;
    
      menuLinkId: string;
    
    druid's avatar
    druid committed
      isExternal: string;
    
      shortcutsTitle: string;
      closeButtonTitle: string;
    
    druid's avatar
    druid committed
      items: Array<DesktopLinks>;
    
      shortcuts: Array<ShortcutLinks>;
    
    druid's avatar
    druid committed
    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;
    
    druid's avatar
    druid committed
      private _headerBorderOffset: number = 0;
    
      private _hoverTimer = null;
    
    druid's avatar
    druid committed
      private _fadeOutTimer = null;
    
      @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;
    
    shamalainen's avatar
    shamalainen committed
    
        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');
    
            hyBackdropDiv.removeAttribute('style');
    
            hyBackdropDiv.classList.remove('is-active');
    
    druid's avatar
    druid committed
        // Close menu lang menu if it's open
    
        this.menuDesktopToggled.emit();
    
    
    druid's avatar
    druid committed
        clearTimeout(this._fadeOutTimer);
    
    druid's avatar
    druid committed
    
        // Open desktop menu panel
    
        this.isDesktopMenuOpen = true;
    
    
        const menuItems = this.el.shadowRoot.querySelectorAll(`.desktop-menu-link`);
    
    druid's avatar
    druid committed
        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;
    
    druid's avatar
    druid committed
    
    
        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.
    
    druid's avatar
    druid committed
        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) {
    
    druid's avatar
    druid committed
        this.isDesktopMenuOpen = false;
        this.currenOpenMenuId = 0;
        this.showBackdropShadow();
    
        this.clearPanelItemsStatus(fadeOut);
        clearTimeout(this._hoverTimer);
    
    druid's avatar
    druid committed
      }
    
    
      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');
        });
    
    druid's avatar
    druid committed
        if (fadeOut) {
          menuPanelItems.forEach((item) => {
            (item as HTMLElement).style.opacity = '0';
    
            (item as HTMLElement).style.transition = 'opacity 1s';
    
    druid's avatar
    druid committed
          });
    
          this._fadeOutTimer = setTimeout(() => {
            menuPanelItems.forEach((item) => {
              item.classList.remove('hy-desktop-menu-panel--is-active');
              item.setAttribute('aria-hidden', 'true');
            });
    
    druid's avatar
    druid committed
          }, 350);
    
    druid's avatar
    druid committed
        } else {
          menuPanelItems.forEach((item) => {
            item.classList.remove('hy-desktop-menu-panel--is-active');
            item.setAttribute('aria-hidden', 'true');
    
    druid's avatar
    druid committed
            (item as HTMLElement).style.opacity = '0';
            (item as HTMLElement).style.transition = 'none';
    
    druid's avatar
    druid committed
          });
        }
    
      handleDesktopMenuClose(event) {
    
        let fadeOut = true;
        this.closePanel(fadeOut);
    
    druid's avatar
    druid committed
        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);
      }
    
    
    druid's avatar
    druid committed
      // 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) {
    
    druid's avatar
    druid committed
        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();
      }
    
    
    druid's avatar
    druid committed
      handleDesktopMenuLeave(event) {
        let leaveEvent = event as MouseEvent;
    
    druid's avatar
    druid committed
        let hyHeader = this.el.closest('.hy-site-header') as HTMLElement;
        const headerHeight = hyHeader.offsetTop + hyHeader.offsetHeight;
    
        if (leaveEvent.clientY < headerHeight - 4) {
          this.closePanel();
        }
    
    druid's avatar
    druid committed
      /*
        Close the panel if mouse is moving over the menu label.
      * */
    
      handleDesktopMenuMove(event, id) {
    
    druid's avatar
    druid committed
        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) {
    
    druid's avatar
    druid committed
              this.closePanel();
            }
    
    druid's avatar
    druid committed
          }
    
    druid's avatar
    druid committed
          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);
    
        // 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>;
    
        if (links) {
          links.map(
            ({
              menuLinkId: id,
              shortcuts,
              items,
              url,
              description,
              label,
              labelExtra,
    
    druid's avatar
    druid committed
              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(' ');
    
    druid's avatar
    druid committed
              let target = isExternal ? '_blank' : '_self';
    
    
              menuLinkItems.push(
                <li>
    
                    type="button"
                    class={classAttributes}
                    link-id={id}
    
    druid's avatar
    druid committed
                    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>
    
    druid's avatar
    druid committed
                    <hy-icon icon={'hy-icon-caret-down'} size={32} />
    
    druid's avatar
    druid committed
                  <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}
    
    druid's avatar
    druid committed
                          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}>
    
    druid's avatar
    druid committed
                          {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>
              );
            }
          );
        }
    
    druid's avatar
    druid committed
    
    
        this.menuLinkItems = menuLinkItems;
    
    druid's avatar
    druid committed
        return (
    
    druid's avatar
    druid committed
          <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>
    
    shamalainen's avatar
    shamalainen committed
          </nav>
    
    druid's avatar
    druid committed
        );