Skip to content
Snippets Groups Projects
hy-tabs.tsx 10.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • Tuukka Turu's avatar
    Tuukka Turu committed
    import {Component, ComponentInterface, h, Prop, State, Listen, Element} from '@stencil/core';
    // For easy reference
    let keys = {
      end: 35,
      home: 36,
      left: 37,
      up: 38,
      right: 39,
      down: 40,
      delete: 46,
    };
    
    // Add or substract depending on key pressed
    let direction = {
      37: -1,
      38: -1,
      39: 1,
      40: 1,
    };
    let checkTimeout;
    let focusTimeout;
    
    Tuukka Turu's avatar
    Tuukka Turu committed
    
    @Component({
      tag: 'hy-tabs',
      styleUrl: 'hy-tabs.scss',
      shadow: false,
    })
    export class HyTabs implements ComponentInterface {
      @Prop() tabId?: string;
      @Prop() tabListLabel: string = '';
      @State() focusTimeoutCleared: boolean = true;
      @State() tabButtonTitles: object[];
      @State() tabPanelsState: NodeListOf<Element>[];
      @State() tabButtons: NodeListOf<Element>[];
      @State() tabList: NodeListOf<Element>[];
      @Element() el: HTMLElement;
    
    Tuukka Turu's avatar
    Tuukka Turu committed
    
      componentWillLoad() {
        const tabItems = this.el.querySelectorAll('hy-tabs-item');
        if (tabItems) {
          this.tabButtonTitles = Array.from(tabItems).map((panel) => {
            return {
              title: panel.getAttribute('tab-title'),
              id: panel.getAttribute('tab-title').toLowerCase().replace(/\W/g, '-'),
            };
          });
        }
      }
    
      componentDidLoad() {
    
        let hyMainDiv = this.el.closest('.hy-main');
        if (hyMainDiv) {
          if (!hyMainDiv.classList.contains('with-sidebar')) {
            this.headerstyle = 'large';
          }
        }
    
    
    Tuukka Turu's avatar
    Tuukka Turu committed
        const tabContainer = this.el.querySelector('.hy-tabs__container') as any;
    
    
    Tuukka Turu's avatar
    Tuukka Turu committed
        if (tabContainer) {
          this.generateArrays(tabContainer);
    
          const leftButton = this.el.querySelectorAll('.hy-tab-scroll__left')[0];
          const rightButton = this.el.querySelectorAll('.hy-tab-scroll__right')[0];
          const tabList = this.tabList as any;
    
    Tuukka Turu's avatar
    Tuukka Turu committed
          this.checkScrollHidden(tabList, leftButton, rightButton);
    
    
          tabList.addEventListener(
            'scroll',
            () => {
              scrollTimeout = setTimeout(() => {
                window.clearTimeout(scrollTimeout);
                this.checkScrollHidden(tabList, leftButton, rightButton);
              }, 250);
            },
            false
          );
    
    
    Tuukka Turu's avatar
    Tuukka Turu committed
          if (tabList.offSetWidth <= document.body.scrollWidth) {
            leftButton.classList.add('is-hidden');
            rightButton.classList.add('is-hidden');
          }
    
          const oneTabWidth = 250;
          rightButton.addEventListener('click', (e) => {
            e.preventDefault();
            rightButton.classList.add('is-disabled');
            tabList.scrollBy({
              top: 0,
              left: +oneTabWidth,
              behavior: 'smooth',
            });
            checkTimeout = window.setTimeout(() => {
    
              window.clearTimeout(checkTimeout);
    
    Tuukka Turu's avatar
    Tuukka Turu committed
              this.checkScrollHidden(tabList, leftButton, rightButton);
              rightButton.classList.remove('is-disabled');
            }, 250);
          });
    
          leftButton.addEventListener('click', (e) => {
            e.preventDefault();
            leftButton.classList.add('is-disabled');
            tabList.scrollBy({
              top: 0,
              left: -oneTabWidth,
              behavior: 'smooth',
            });
            checkTimeout = window.setTimeout(() => {
    
              window.clearTimeout(checkTimeout);
    
    Tuukka Turu's avatar
    Tuukka Turu committed
              this.checkScrollHidden(tabList, leftButton, rightButton);
              leftButton.classList.remove('is-disabled');
            }, 250);
          });
        }
      }
    
      generateArrays(tc) {
        this.tabList = tc.querySelectorAll('[role="tablist"]')[0] as any;
        this.tabButtons = tc.querySelectorAll('[role="tab"]') as any;
        this.tabPanelsState = [tc.querySelectorAll('[role="tabpanel"]')] as any;
    
        if (this.tabButtons.length > 0) {
          this.addListeners(this.tabButtons, 1);
          this.activateTab(this.tabButtons[0], true);
        }
      }
    
      addListeners(tabs, index) {
        if (tabs.length > 1) {
          for (let i = 0; i < tabs.length; ++i) {
            tabs[index].addEventListener('click', this.clickEventListener);
            tabs[index].addEventListener('keydown', this.keydownEventListener);
            //tabs[index].addEventListener('keyup', this.keyupEventListener);
            tabs[index].index = index;
          }
        }
      }
    
      checkScrollHidden(tablist, leftButton, rightButton) {
        if (tablist.scrollLeft === 0) {
          leftButton.classList.add('is-hidden');
        } else {
          leftButton.classList.remove('is-hidden');
        }
        if (tablist.scrollLeft + tablist.clientWidth >= tablist.scrollWidth - 1) {
          rightButton.classList.add('is-hidden');
        } else {
          rightButton.classList.remove('is-hidden');
        }
      }
    
      // When a tab is clicked, activateTab is fired to activate it
      @Listen('click')
      clickEventListener(event) {
        if (event) {
          const target = event.target;
          const tabs = this.tabButtonTitles;
          if (tabs) {
            tabs.forEach((tab) => {
              const id = Object.values(tab)[1];
              if (id === target.id) {
                this.activateTab(target, true);
              }
            });
            event.stopPropagation();
            event.stopImmediatePropagation();
          }
        }
      }
    
      @Listen('keydown')
      keydownEventListener(event) {
        const key = event.keyCode;
        const tabs = this.tabButtonTitles as any;
        switch (key) {
          case keys.end:
            event.preventDefault();
            // Activate last tab
            this.activateTab(tabs[tabs.length - 1], true);
            break;
          case keys.home:
            event.preventDefault();
            // Activate first tab
            this.activateTab(tabs[0], true);
            break;
    
          // Up and down are in keydown
          // because we need to prevent page scroll >:)
          case keys.up:
          case keys.down:
            if (this.focusTimeoutCleared) {
              this.determineOrientation(event);
            }
            break;
        }
      }
    
      @Listen('keydown')
      keyupEventListener(event) {
        const key = event.keyCode;
    
        switch (key) {
          case keys.left:
          case keys.right:
    
            event.preventDefault();
    
    Tuukka Turu's avatar
    Tuukka Turu committed
            if (this.focusTimeoutCleared) {
              this.determineOrientation(event);
            }
            break;
        }
        event.stopPropagation();
        event.stopImmediatePropagation();
      }
    
      determineOrientation(event) {
        const leftButton = this.el.querySelectorAll('.hy-tab-scroll__left')[0];
        const rightButton = this.el.querySelectorAll('.hy-tab-scroll__right')[0];
        const tabList = this.tabList as any;
    
        this.checkScrollHidden(tabList, leftButton, rightButton);
    
        const key = event.keyCode;
        const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
        let proceed = false;
        if (vertical) {
          if (key === keys.up || key === keys.down) {
            event.preventDefault();
            proceed = true;
          }
        } else {
          if (key === keys.left || key === keys.right) {
            proceed = true;
          }
        }
    
        if (proceed) {
          this.switchTabOnArrowPress(event);
        }
      }
    
      switchTabOnArrowPress(event) {
        const pressed = event.keyCode;
    
        if (direction[pressed]) {
          const target = event.target;
          const tabs = this.tabButtons as any;
    
          for (let i = 0; i < tabs.length; i++) {
            if (tabs[i].id === target.id) {
              if (i > 0) {
                if (direction[pressed] === -1) {
                  tabs[i - 1].focus();
                  this.focusEventHandler(tabs[i - 1]);
                  break;
                }
              }
              if (i < tabs.length - 1) {
                if (direction[pressed] === 1) {
                  tabs[i + 1].focus();
                  this.focusEventHandler(tabs[i + 1]);
                  break;
                }
              }
            }
          }
        }
      }
    
      // Activates any given tab panel
      activateTab(tab, setFocus) {
        setFocus = setFocus || true;
        // Deactivate all other tabs
        this.deactivateTabs(this.tabButtons);
        // Remove tabindex attribute
        tab.removeAttribute('tabindex');
        tab.setAttribute('aria-selected', 'true');
        const controls = tab.getAttribute('aria-controls');
        this.el.querySelector(`#${controls}`).removeAttribute('hidden');
    
        if (setFocus) {
          tab.focus();
        }
      }
    
      // Deactivate all tabs and tab panels
      deactivateTabs(tabs) {
        for (let t = 0; t < tabs.length; t++) {
          tabs[t].setAttribute('tabindex', '-1');
          tabs[t].setAttribute('aria-selected', 'false');
          tabs[t].removeEventListener('focus', this.focusEventHandler);
        }
        const panels = this.tabPanelsState[0];
        for (let p = 0; p < panels.length; p++) {
          panels[p].setAttribute('hidden', 'hidden');
        }
      }
    
      @Listen('focus')
      focusEventHandler(tab) {
        const target = tab;
        this.focusTimeoutCleared = false;
        focusTimeout = window.setTimeout(() => {
    
          window.clearTimeout(focusTimeout);
          this.focusTimeoutCleared = true;
    
    Tuukka Turu's avatar
    Tuukka Turu committed
          const focused = document.activeElement;
          if (target === focused) {
            this.activateTab(target, false);
          }
    
    Tuukka Turu's avatar
    Tuukka Turu committed
      }
    
      render() {
    
        const classComponentAttributes = ['hy-tabs__container', `hy-tabs__container__${this.headerstyle}`].join(' ');
    
    Tuukka Turu's avatar
    Tuukka Turu committed
        const id = this.tabId.toLowerCase().replace(/\W/g, '-');
    
    
        if (window.location.hash) {
          const tabHash = window.location.hash.substring(1);
          const isUrlHashed = this.tabButtonTitles.some((tab) => tab['id'] === tabHash);
    
          if (isUrlHashed) {
            const hashElement = document.getElementById(tabHash);
            if (hashElement) this.activateTab(hashElement, true);
          }
        }
    
    
        return [
          <hy-box pt="1.25, 1.25, 1.5, 2.5" />,
    
    Tuukka Turu's avatar
    Tuukka Turu committed
          <div id={id} class={classComponentAttributes}>
            <div class="hy-tablist-container">
    
              <button tabindex="-1" class="hy-tab-scroll hy-tab-scroll__left is-hidden" aria-hidden="true">
    
    shamalainen's avatar
    shamalainen committed
                <span>
                  <hy-icon icon={'hy-icon-caret-left'} size={16} />
                </span>
    
    Tuukka Turu's avatar
    Tuukka Turu committed
              </button>
              <div role="tablist" aria-label={this.tabListLabel}>
                {this.tabButtonTitles &&
                  this.tabButtonTitles.map((item) => {
                    const title = Object.values(item)[0];
                    const id = title.toLowerCase().replace(/\W/g, '-');
                    return (
    
    shamalainen's avatar
    shamalainen committed
                      <button aria-selected="false" aria-controls={`${id}-tab`} class={this.headerstyle} role="tab" id={id}>
                        <span>{title}</span>
                      </button>
    
    Tuukka Turu's avatar
    Tuukka Turu committed
                    );
                  })}
              </div>
    
              <button tabindex="-1" class="hy-tab-scroll hy-tab-scroll__right" aria-hidden="true">
    
    shamalainen's avatar
    shamalainen committed
                <span>
                  <hy-icon icon={'hy-icon-caret-right'} size={16} />
                </span>
    
    Tuukka Turu's avatar
    Tuukka Turu committed
              </button>
            </div>
            <slot></slot>
    
          </div>,
          <hy-box mb="1.75, 1.75, 2, 2.5" />,
        ];
    
    Tuukka Turu's avatar
    Tuukka Turu committed
      }
    }