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; let scrollTimeout; @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; @Prop() headerstyle: string = 'common'; 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'; } } const tabContainer = this.el.querySelector('.hy-tabs__container') as any; 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; this.checkScrollHidden(tabList, leftButton, rightButton); tabList.addEventListener( 'scroll', () => { scrollTimeout = setTimeout(() => { window.clearTimeout(scrollTimeout); this.checkScrollHidden(tabList, leftButton, rightButton); }, 250); }, false ); 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); 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); 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(); 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; const focused = document.activeElement; if (target === focused) { this.activateTab(target, false); } }, 250); } render() { const classComponentAttributes = ['hy-tabs__container', `hy-tabs__container__${this.headerstyle}`].join(' '); const id = this.tabId.toLowerCase().replace(/\W/g, '-'); return [ <hy-box pt="1.25, 1.25, 1.5, 2.5" />, <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"> <span> <hy-icon icon={'hy-icon-caret-left'} size={16} /> </span> </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 ( <button aria-selected="false" aria-controls={`${id}-tab`} class={this.headerstyle} role="tab" id={id}> <span>{title}</span> </button> ); })} </div> <button tabindex="-1" class="hy-tab-scroll hy-tab-scroll__right" aria-hidden="true"> <span> <hy-icon icon={'hy-icon-caret-right'} size={16} /> </span> </button> </div> <slot></slot> </div>, <hy-box mb="1.75, 1.75, 2, 2.5" />, ]; } }