import { queryOne, queryAll } from '@ecl/dom-utils';
import EventManager from '@ecl/event-manager';
/**
* @param {HTMLElement} element DOM element for component instantiation and scope
* @param {Object} options
* @param {String} options.containerSelector Selector for container element
* @param {String} options.contentSelector Selector for tabs with content
* @param {String} options.listSelector Selector for list element
* @param {String} options.listItemsSelector Selector for tabs element
* @param {String} options.moreButtonSelector Selector for more button element
* @param {String} options.moreLabelSelector Selector for more button label element
* @param {String} options.prevSelector Selector for prev element
* @param {String} options.nextSelector Selector for next element
* @param {Boolean} options.attachClickListener
* @param {Boolean} options.attachResizeListener
*/
export class Tabs {
/**
* @static
* Shorthand for instance creation and initialisation.
*
* @param {HTMLElement} root DOM element for component instantiation and scope
*
* @return {Tabs} An instance of Tabs.
*/
static autoInit(root, { TABS: defaultOptions = {} } = {}) {
const tabs = new Tabs(root, defaultOptions);
tabs.init();
root.ECLTabs = tabs;
return tabs;
}
/**
* An array of supported events for this component.
*
* @type {Array<string>}
* @memberof Select
*/
supportedEvents = ['onToggle'];
constructor(
element,
{
containerSelector = '.ecl-tabs__container',
contentSelector = 'data-ecl-tabs-with-content',
listSelector = '.ecl-tabs__list',
listItemsSelector = '.ecl-tabs__item:not(.ecl-tabs__item--more)',
moreItemSelector = '.ecl-tabs__item--more',
moreButtonSelector = '.ecl-tabs__toggle',
moreLabelSelector = '.ecl-tabs__toggle .ecl-button__label',
prevSelector = '.ecl-tabs__prev',
nextSelector = '.ecl-tabs__next',
activeSelector = 'ecl-tabs__link--active',
attachClickListener = true,
attachResizeListener = true,
} = {},
) {
// Check element
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError(
'DOM element should be given to initialize this widget.',
);
}
this.element = element;
this.eventManager = new EventManager();
// Options
this.containerSelector = containerSelector;
this.contentSelector = contentSelector;
this.listSelector = listSelector;
this.listItemsSelector = listItemsSelector;
this.moreItemSelector = moreItemSelector;
this.moreButtonSelector = moreButtonSelector;
this.moreLabelSelector = moreLabelSelector;
this.prevSelector = prevSelector;
this.nextSelector = nextSelector;
this.activeSelector = activeSelector;
this.attachClickListener = attachClickListener;
this.attachResizeListener = attachResizeListener;
// Private variables
this.container = null;
this.list = null;
this.listItems = null;
this.moreItem = null;
this.moreButton = null;
this.moreButtonActive = false;
this.moreLabel = null;
this.moreLabelValue = null;
this.dropdown = null;
this.dropdownList = null;
this.dropdownItems = null;
this.allowShift = true;
this.buttonNextSize = 0;
this.index = 0;
this.total = 0;
this.tabsKey = [];
this.firstTab = null;
this.lastTab = null;
this.direction = 'ltr';
this.isMobile = false;
this.resizeTimer = null;
this.tabs = [];
this.activeTab = null;
// Bind `this` for use in callbacks
this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
this.handleClickOnTabs = this.handleClickOnTabs.bind(this);
this.handleResize = this.handleResize.bind(this);
this.closeMoreDropdown = this.closeMoreDropdown.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.shiftTabs = this.shiftTabs.bind(this);
this.handleKeyboardOnTabs = this.handleKeyboardOnTabs.bind(this);
this.moveFocus = this.moveFocus.bind(this);
this.arrowFocusToTab = this.arrowFocusToTab.bind(this);
this.tabsKeyEvents = this.tabsKeyEvents.bind(this);
this.handleFocusOnToggle = this.handleFocusOnToggle.bind(this);
this.handleMouseDownOnToggle = this.handleMouseDownOnToggle.bind(this);
this.handleFocusOnTab = this.handleFocusOnTab.bind(this);
}
/**
* Initialise component.
*/
init() {
if (!ECL) {
throw new TypeError('Called init but ECL is not present');
}
ECL.components = ECL.components || new Map();
this.container = queryOne(this.containerSelector, this.element);
this.hasContent = this.element.hasAttribute(this.contentSelector);
this.list = queryOne(this.listSelector, this.element);
this.listItems = queryAll(this.listItemsSelector, this.element);
this.moreItem = queryOne(this.moreItemSelector, this.element);
this.moreButton = queryOne(this.moreButtonSelector, this.element);
this.moreLabel = queryOne(this.moreLabelSelector, this.element);
this.moreLabelValue = this.moreLabel.innerText;
this.btnPrev = queryOne(this.prevSelector, this.element);
this.btnNext = queryOne(this.nextSelector, this.element);
this.total = this.listItems.length;
if (this.moreButton) {
// Create the "more" dropdown and clone existing list items
this.dropdown = document.createElement('div');
this.dropdown.classList.add('ecl-tabs__dropdown');
this.dropdownList = document.createElement('div');
this.dropdownList.classList.add('ecl-tabs__dropdown-list');
this.listItems.forEach((item) => {
const originalLink = queryOne('.ecl-link', item);
if (!originalLink) return;
const li = document.createElement('div');
li.className = 'ecl-tabs__item';
const a = document.createElement('a');
a.className = originalLink.className;
a.setAttribute('role', 'tab');
a.setAttribute('aria-selected', 'false');
a.setAttribute('tabindex', '-1');
a.href = originalLink.getAttribute('href');
// This will only copy text, not any markup used as the label of the link
a.textContent = originalLink.textContent?.trim() ?? '';
if (originalLink.id) {
a.dataset.id = originalLink.id;
}
li.appendChild(a);
li.addEventListener('click', this.handleClickOnTabs);
this.dropdownList.appendChild(li);
});
this.dropdown.appendChild(this.dropdownList);
this.moreItem.appendChild(this.dropdown);
this.dropdownItems = queryAll(
'.ecl-tabs__dropdown .ecl-tabs__item',
this.element,
);
}
if (this.btnNext) {
this.buttonNextSize = this.btnNext.getBoundingClientRect().width;
}
// Bind events
if (this.attachClickListener && this.moreButton) {
this.moreButton.addEventListener(
'mousedown',
this.handleMouseDownOnToggle,
);
this.moreButton.addEventListener('click', this.handleClickOnToggle);
this.moreButton.addEventListener('focus', this.handleFocusOnToggle);
}
if (this.attachClickListener && document && this.moreButton) {
document.addEventListener('click', this.handleClickOutside);
}
if (this.hasContent) {
this.listItems.forEach((tab) => {
const link = queryOne('.ecl-tabs__link', tab);
const url = new URL(link.href);
if (url.hash) {
const id = url.hash.slice(1);
const content = document.getElementById(id);
if (content) {
content.setAttribute('role', 'tabpanel');
link.setAttribute('aria-controls', id);
if (link.id) {
content.setAttribute('aria-labelledby', link.id);
}
}
this.tabs.push({ link, id, content });
tab.addEventListener('click', this.handleClickOnTabs);
}
});
const currentHash = window.location.hash.slice(1);
this.activeTab = null;
const hasInitialHash = !!currentHash;
if (hasInitialHash) {
this.activeTab = this.tabs.find((t) => t.id === currentHash);
}
if (!this.activeTab) {
this.activeTab = this.tabs.find((t) =>
t.link.classList.contains(this.activeSelector),
);
}
if (!this.activeTab) {
this.activeTab = this.tabs[0];
}
if (this.activeTab) {
let isVisibleTab = false;
this.tabs.forEach((t) => {
const isActive = t === this.activeTab;
if (isActive) {
isVisibleTab = true;
}
t.link.classList.toggle(this.activeSelector, isActive);
t.link.setAttribute('aria-selected', isActive ? 'true' : 'false');
t.link.setAttribute('tabindex', isActive ? '0' : '-1');
if (t.content) {
t.content.style.display = isActive ? 'block' : 'none';
if (!t.content.hasAttribute('z-index')) {
t.content.setAttribute('z-index', -1);
}
}
});
if (this.moreButton) {
this.dropdownItems.forEach((item) => {
const dropdownLink = item.querySelector('a');
const dropdownUrl = new URL(dropdownLink.href);
const dropdownId = dropdownUrl.hash?.slice(1);
const isActive = dropdownId === this.activeTab.id;
dropdownLink.classList.toggle(this.activeSelector, isActive);
dropdownLink.setAttribute(
'aria-selected',
isActive ? 'true' : 'false',
);
if (isActive && !isVisibleTab) {
this.moreButtonActive = true;
this.moreButton.classList.add('ecl-tabs__toggle--active');
}
});
}
if (hasInitialHash) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const content = this.activeTab.content;
if (content && content.offsetParent !== null) {
content.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
});
});
}
}
}
if (this.attachClickListener && this.btnNext) {
this.btnNext.addEventListener(
'click',
this.shiftTabs.bind(this, 'next', true),
);
}
if (this.attachClickListener && this.btnPrev) {
this.btnPrev.addEventListener(
'click',
this.shiftTabs.bind(this, 'prev', true),
);
}
if (this.attachResizeListener) {
window.addEventListener('resize', this.handleResize);
}
// Tries ten times to get a reliable width for the list.
const ensureTabsReady = (callback, retries = 10) => {
const hasSize = this.list && this.list.offsetWidth > 0;
if (hasSize) {
callback();
} else if (retries > 0) {
requestAnimationFrame(() => ensureTabsReady(callback, retries - 1));
}
};
requestAnimationFrame(() => ensureTabsReady(() => this.handleResize()));
// Set ecl initialized attribute
this.element.setAttribute('data-ecl-auto-initialized', 'true');
ECL.components.set(this.element, this);
}
/**
* Register a callback function for a specific event.
*
* @param {string} eventName - The name of the event to listen for.
* @param {Function} callback - The callback function to be invoked when the event occurs.
* @returns {void}
* @memberof Tabs
* @instance
*
* @example
* // Registering a callback for the 'onToggle' event
* inpage.on('onToggle', (event) => {
* console.log('Toggle event occurred!', event);
* });
*/
on(eventName, callback) {
this.eventManager.on(eventName, callback);
}
/**
* Trigger a component event.
*
* @param {string} eventName - The name of the event to trigger.
* @param {any} eventData - Data associated with the event.
*
* @memberof Tabs
*/
trigger(eventName, eventData) {
this.eventManager.trigger(eventName, eventData);
}
/**
* Destroy component.
*/
destroy() {
if (this.dropdown) {
this.dropdown.remove();
}
if (this.moreButton) {
this.moreLabel.textContent = this.moreLabelValue;
this.moreButton.replaceWith(this.moreButton.cloneNode(true));
}
if (this.btnNext) {
this.btnNext.replaceWith(this.btnNext.cloneNode(true));
}
if (this.btnPrev) {
this.btnPrev.replaceWith(this.btnPrev.cloneNode(true));
}
if (this.attachClickListener && document && this.moreButton) {
document.removeEventListener('click', this.handleClickOutside);
}
if (this.attachResizeListener) {
window.removeEventListener('resize', this.handleResize);
}
if (this.tabsKey) {
this.tabsKey.forEach((item) => {
item.addEventListener('keydown', this.handleKeyboardOnTabs);
});
}
if (this.hasContent) {
this.tabs.forEach((tab) => {
tab.removeEventListener('click', this.handleClickOnTabs);
});
}
if (this.element) {
this.element.removeAttribute('data-ecl-auto-initialized');
ECL.components.delete(this.element);
}
}
/**
* Action to shift next or previous tabs on mobile format.
* @param {int|string} dir
*/
shiftTabs(dir) {
this.index = dir === 'next' ? this.index + 1 : this.index - 1;
// Show or hide prev or next button based on tab index
if (this.index >= 1) {
this.btnPrev.style.display = 'flex';
this.container.classList.add('ecl-tabs__container--left');
} else {
this.btnPrev.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--left');
}
if (this.index >= this.total - 1) {
this.btnNext.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--right');
} else {
this.btnNext.style.display = 'flex';
this.container.classList.add('ecl-tabs__container--right');
}
// Slide tabs
let newOffset = 0;
this.direction = getComputedStyle(this.element).direction;
if (this.direction === 'rtl') {
newOffset = Math.ceil(
this.list.offsetWidth -
this.listItems[this.index].offsetLeft -
this.listItems[this.index].offsetWidth,
);
} else {
newOffset = Math.ceil(this.listItems[this.index].offsetLeft);
}
// Calculate available width, accounting for the right button width
// Note: left button doesn't reduce width due to negative margin on list
let availableWidth = this.element.getBoundingClientRect().width;
if (this.container.classList.contains('ecl-tabs__container--right')) {
availableWidth -= this.buttonNextSize;
}
const maxScroll = Math.ceil(
this.list.getBoundingClientRect().width - availableWidth,
);
if (newOffset > maxScroll) {
this.btnNext.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--right');
newOffset = maxScroll;
}
this.list.style.transitionDuration = '0.4s';
if (this.direction === 'rtl') {
this.list.style.transform = `translate3d(${newOffset}px, 0px, 0px)`;
} else {
this.list.style.transform = `translate3d(-${newOffset}px, 0px, 0px)`;
}
}
/**
* Track mouse interaction on the "more" button.
*/
handleMouseDownOnToggle() {
this.isMouseEvent = true;
}
/**
* Toggle the "more" dropdown.
*/
handleClickOnToggle(e) {
this.isMouseEvent = false;
if (this.dropdown.classList.contains('ecl-tabs__dropdown--show')) {
this.closeMoreDropdown();
} else {
this.openMoreDropdown();
}
this.trigger('onToggle', e);
}
/**
* Handle focus on the "more" button - open dropdown and focus active item.
*/
handleFocusOnToggle() {
// Only handle focus if the active tab is in the dropdown
// and focus came from keyboard (not mouse click or tab from dropdown)
if (!this.moreButtonActive || this.isMouseEvent || this.isTabEvent) {
this.isTabEvent = false;
return;
}
// Open the dropdown
this.openMoreDropdown();
// Find and focus the active item in the dropdown
const activeDropdownLink = this.dropdown.querySelector(
`.${this.activeSelector}`,
);
if (activeDropdownLink) {
activeDropdownLink.focus();
}
}
/**
* Handle focus on a tab link - sync transform on mobile.
* @param {Event} e
*/
handleFocusOnTab(e) {
if (!this.isMobile) return;
const tab = e.currentTarget;
const tabIndex = this.tabsKey.indexOf(tab);
if (tabIndex === -1) return;
// Sync this.index with focused tab
this.index = tabIndex;
// Reset any native scroll
this.list.scrollLeft = 0;
this.container.scrollLeft = 0;
// Update button visibility
if (tabIndex > 0) {
this.btnPrev.style.display = 'flex';
this.container.classList.add('ecl-tabs__container--left');
} else {
this.btnPrev.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--left');
}
if (tabIndex >= this.total - 1) {
this.btnNext.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--right');
} else {
this.btnNext.style.display = 'flex';
this.container.classList.add('ecl-tabs__container--right');
}
// Sync transform to show the focused tab
if (tabIndex > 0) {
const prevBtnWidth = this.btnPrev.getBoundingClientRect().width;
const tabOffset = this.listItems[tabIndex].offsetLeft - prevBtnWidth;
this.list.style.transform = `translate3d(-${tabOffset}px, 0px, 0px)`;
} else {
this.list.style.transform = 'translate3d(0px, 0px, 0px)';
}
}
/**
* Sets the callback function to be executed on toggle.
* @param {Function} callback - The callback function to be set.
*/
set onToggle(callback) {
this.onToggleCallback = callback;
}
/**
* Gets the callback function set for toggle events.
* @returns {Function|null} - The callback function, or null if not set.
*/
get onToggle() {
return this.onToggleCallback;
}
/**
* Hide and show content when clicking on a tab.
*/
handleClickOnTabs(e) {
let isVisibleTab = false;
const tabUrl = new URL(e.target.href);
const tabId = tabUrl.hash ? tabUrl.hash.slice(1) : null;
// We only handle hashes
if (!tabId) return;
e.preventDefault();
// Visible tabs
this.tabs.forEach((tab) => {
// Toggle content visibility
if (tab.content) {
tab.content.style.display = tab.id !== tabId ? 'none' : 'block';
}
// Toggle active styles and attributes
if (tab.id !== tabId) {
tab.link.classList.remove(this.activeSelector);
tab.link.setAttribute('aria-selected', 'false');
tab.link.setAttribute('tabindex', '-1');
} else {
this.activeTab = tab;
tab.link.classList.add(this.activeSelector);
tab.link.setAttribute('aria-selected', 'true');
tab.link.setAttribute('tabindex', '0');
if (
!tab.link
.closest('.ecl-tabs__item')
.classList.contains('ecl-tabs__item--hidden')
) {
isVisibleTab = true;
}
}
});
if (this.moreButton) {
// Reset styles for the more button
this.moreButtonActive = false;
this.moreButton.classList.remove('ecl-tabs__toggle--active');
// Hidden tabs
this.dropdownItems.forEach((item) => {
const dropdownLink = item.getElementsByTagName('a')[0];
const dropdownUrl = new URL(dropdownLink);
const dropdownId = dropdownUrl.hash ? dropdownUrl.hash.slice(1) : null;
// Toggle active styles and attributes
if (dropdownId !== tabId) {
dropdownLink.classList.remove(this.activeSelector);
dropdownLink.setAttribute('aria-selected', 'false');
} else {
dropdownLink.classList.add(this.activeSelector);
dropdownLink.setAttribute('aria-selected', 'true');
if (!isVisibleTab) {
this.moreButtonActive = true;
this.moreButton.classList.add('ecl-tabs__toggle--active');
}
}
});
// Update tabindex on more button based on whether active tab is hidden
this.moreButton.setAttribute(
'tabindex',
this.moreButtonActive ? '0' : '-1',
);
}
// Add the hash to the URL
history.replaceState(null, '', `#${tabId}`);
}
/**
* Trigger events on resize.
*/
handleResize() {
// Close dropdown if more button is not displayed
if (window.getComputedStyle(this.moreButton).display === 'none') {
this.closeMoreDropdown();
}
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
this.list.style.transform = 'translate3d(0px, 0px, 0px)';
// Behaviors for mobile format
const vw = Math.max(
document.documentElement.clientWidth || 0,
window.innerWidth || 0,
);
if (vw <= 480) {
this.isMobile = true;
this.index = 1;
this.list.style.transitionDuration = '0.4s';
this.shiftTabs(this.index);
if (this.moreItem) {
this.moreItem.classList.add('ecl-tabs__item--hidden');
}
if (this.moreButton) {
this.moreButton.classList.add('ecl-tabs__toggle--hidden');
}
// Reset to auto so scrollWidth reflects the natural content width
this.list.style.width = 'auto';
this.listItems.forEach((item) => {
item.classList.remove('ecl-tabs__item--hidden');
});
// Use scrollWidth to also include item margins
this.list.style.width = `${this.list.scrollWidth}px`;
this.btnNext.style.display = 'flex';
this.container.classList.add('ecl-tabs__container--right');
this.btnPrev.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--left');
this.tabsKeyEvents();
return;
}
this.isMobile = false;
// Behaviors for Tablet and desktop format (More button)
this.btnNext.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--right');
this.btnPrev.style.display = 'none';
this.container.classList.remove('ecl-tabs__container--left');
this.list.style.width = 'auto';
// Hide items that won't fit in the list
// Temporarily show the more button to get its actual width
this.moreItem.classList.remove('ecl-tabs__item--hidden');
this.moreButton.classList.remove('ecl-tabs__toggle--hidden');
const moreButtonWidth = this.moreButton.getBoundingClientRect().width;
this.moreItem.classList.add('ecl-tabs__item--hidden');
this.moreButton.classList.add('ecl-tabs__toggle--hidden');
const hiddenItems = [];
const listWidth =
this.list.getBoundingClientRect().width || this.list.offsetWidth;
this.moreButtonActive = false;
// Measure all items first to check if they fit without the button
this.listItems.forEach((item) =>
item.classList.remove('ecl-tabs__item--hidden'),
);
const itemWidths = this.listItems.map((item) => {
const itemMargin =
parseFloat(getComputedStyle(item).marginInlineEnd) || 0;
return item.getBoundingClientRect().width + itemMargin;
});
const totalWidth = itemWidths.reduce((sum, w) => sum + w, 0);
if (totalWidth > listWidth) {
// Reserve space for the more button and hide items that don't fit
this.listItems.forEach((item) =>
item.classList.add('ecl-tabs__item--hidden'),
);
let stopWidth = moreButtonWidth;
this.listItems.forEach((item, i) => {
item.classList.remove('ecl-tabs__item--hidden');
if (
listWidth >= stopWidth + itemWidths[i] &&
!hiddenItems.includes(i - 1)
) {
stopWidth += itemWidths[i];
} else {
item.classList.add('ecl-tabs__item--hidden');
if (queryOne('.ecl-tabs__link--active', item)) {
this.moreButtonActive = true;
}
hiddenItems.push(i);
}
});
}
// Add active class to the more button if it contains an active element
if (this.moreButtonActive) {
this.moreButton.classList.add('ecl-tabs__toggle--active');
// Make more button focusable when active tab is hidden
this.moreButton.setAttribute('tabindex', '0');
} else {
this.moreButton.classList.remove('ecl-tabs__toggle--active');
this.moreButton.setAttribute('tabindex', '-1');
}
// Toggle the visibility of More button and items in dropdown
if (!hiddenItems.length) {
this.moreItem.classList.add('ecl-tabs__item--hidden');
this.moreButton.classList.add('ecl-tabs__toggle--hidden');
} else {
this.moreItem.classList.remove('ecl-tabs__item--hidden');
this.moreButton.classList.remove('ecl-tabs__toggle--hidden');
this.moreLabel.textContent = this.moreLabelValue.replace(
'%d',
hiddenItems.length,
);
this.dropdownItems.forEach((item, i) => {
if (!hiddenItems.includes(i)) {
item.classList.add('ecl-tabs__item--hidden');
} else {
item.classList.remove('ecl-tabs__item--hidden');
}
});
}
this.tabsKeyEvents();
}, 100);
}
/**
* Bind key events on tabs for accessibility.
*/
tabsKeyEvents() {
this.tabsKey = [];
this.listItems.forEach((item, index, array) => {
let tab = null;
if (!item.classList.contains('ecl-tabs__item--hidden')) {
tab = queryOne('.ecl-tabs__link', item);
} else {
const dropdownItem = this.dropdownItems[index];
tab = queryOne('.ecl-tabs__link', dropdownItem);
}
tab.addEventListener('keydown', this.handleKeyboardOnTabs);
tab.addEventListener('focus', this.handleFocusOnTab);
this.tabsKey.push(tab);
if (index === 0) {
this.firstTab = tab;
}
if (index === array.length - 1) {
this.lastTab = tab;
}
});
}
/**
* Close the dropdown.
*/
closeMoreDropdown() {
this.moreButton.setAttribute('aria-expanded', false);
this.dropdown.classList.remove('ecl-tabs__dropdown--show');
}
/**
* Open the dropdown.
*/
openMoreDropdown() {
this.moreButton.setAttribute('aria-expanded', true);
this.dropdown.classList.add('ecl-tabs__dropdown--show');
}
/**
* Handle click outside the dropdown to close it.
* @param {Event} e
*/
handleClickOutside(e) {
let el = e.target;
while (el) {
if (el === this.moreButton) {
return;
}
el = el.parentNode;
}
this.closeMoreDropdown();
}
/**
* @param {Event} e
*/
handleKeyboardOnTabs(e) {
const tgt = e.currentTarget;
switch (e.key) {
case 'ArrowLeft':
this.arrowFocusToTab(tgt, 'prev');
break;
case 'ArrowRight':
this.arrowFocusToTab(tgt, 'next');
break;
case 'Home':
this.moveFocus(this.firstTab);
break;
case 'End':
this.moveFocus(this.lastTab);
break;
case ' ':
this.handleClickOnTabs(e);
break;
case 'Escape':
this.closeMoreDropdown();
break;
case 'Tab':
// Close dropdown when tabbing out
// Set flag to prevent focus handler from reopening
this.isTabEvent = true;
this.closeMoreDropdown();
// Temporarily make more button non-tabbable so focus skips it
this.moreButton.setAttribute('tabindex', '-1');
// Restore tabindex and reset isTabEvent after focus has moved
setTimeout(() => {
this.isTabEvent = false;
if (this.moreButtonActive) {
this.moreButton.setAttribute('tabindex', '0');
}
}, 0);
// Move focus to the tab panel (if any)
if (!e.shiftKey && this.hasContent) {
if (this.activeTab.content) {
this.activeTab.content.focus();
}
}
break;
default:
}
}
/**
* @param {HTMLElement} currentTab tab element
*/
moveFocus(currentTab) {
if (currentTab.closest('.ecl-tabs__dropdown')) {
this.openMoreDropdown();
} else {
this.closeMoreDropdown();
}
currentTab.focus();
}
/**
* @param {HTMLElement} currentTab tab element
* @param {string} direction key arrow direction
*/
arrowFocusToTab(currentTab, direction) {
let index = this.tabsKey.indexOf(currentTab);
index = direction === 'next' ? index + 1 : index - 1;
const startTab = direction === 'next' ? this.firstTab : this.lastTab;
const endTab = direction === 'next' ? this.lastTab : this.firstTab;
if (this.isMobile) {
if (currentTab !== endTab) {
// moveFocus will trigger handleFocusOnTab which syncs the transform
this.moveFocus(this.tabsKey[index]);
}
return;
}
if (currentTab === endTab) {
this.moveFocus(startTab);
} else {
this.moveFocus(this.tabsKey[index]);
}
}
}
export default Tabs;