tabs.js

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;