var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
/* eslint-disable no-void */
/* eslint-disable no-param-reassign */
import { html, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { Component } from '../../models';
import Tab from '../tab/tab.component';
import { TAG_NAME as TAB_TAGNAME } from '../tab/tab.constants';
import Button from '../button/button.component';
import { ROLE } from '../../utils/roles';
import { KeyDownHandledMixin } from '../../utils/mixins/KeyDownHandledMixin';
import { KeyToActionMixin } from '../../utils/mixins/KeyToActionMixin';
import { ListNavigationMixin } from '../../utils/mixins/ListNavigationMixin';
import { CaptureDestroyEventForChildElement } from '../../utils/mixins/lifecycle/CaptureDestroyEventForChildElement';
import { ElementStore } from '../../utils/controllers/ElementStore';
import { TabListComponentMixin } from '../../utils/mixins/TabListComponentMixin';
import styles from './tablist.styles';
import { ARROW_BUTTON_DIRECTION } from './tablist.constants';
import { getFirstTab, getLastTab, getActiveTab } from './tablist.utils';
/**
 * Tab list organizes tabs into a container.
 *
 * Children of the tab list are `mdc-tab` elements, sent to the default slot.
 *
 * The tabs can be navigated using the left/right arrow keys, and selected by clicking,
 *  or pressing the Enter and Space keys.
 *
 * **Implicit accessibility rules**
 *
 * - The element that serves as the container for the set of tabs has role `tablist`.
 * - Each element that serves as a tab has role `tab` and is contained within the element with role `tablist`.
 * - The active tab element has the state `aria-selected` set to `true`
 *   and all other tab elements have it set to `false`.
 *
 *
 * **Accessibility notes for consuming (have to be explicitly set when you consume the component)**
 *
 * - Each element that contains the `content panel` for a `tab` has role `tabpanel`.
 * - The `tablist` element needs to have a label provided by `data-aria-label`.
 * - Each element with role `tab` has the property `aria-controls`
 *  that should refer to its associated `tabpanel` element.
 * - Each element with role `tabpanel` has the property `aria-labelledby` referring to its associated `tab` element.
 * - If a `tab` element has a popup menu, it needs to have the property `aria-haspopup` set to either `menu` or `true`.
 *
 * @tagname mdc-tablist
 *
 * @dependency mdc-tab
 * @dependency mdc-button
 *
 * @event change - (React: onChange) This event is dispatched when the tab is selected.
 * Event that fires when user changes the active tab.
 *     The function sent as the argument will receive the fired event
 *      and the new tab id can be fetched from event.detail.tabId.
 *
 *    `(event: CustomEvent) => handleTabChange(event.detail.tabId);`
 *
 * @slot Default slot for mdc-tab elements.
 *
 * @cssproperty --mdc-tablist-gap - Gap between tabs
 * @cssproperty --mdc-tablist-width - Width of the tablist
 * @cssproperty --mdc-tablist-arrow-button-margin - Margin value for the arrow button
 *
 * @csspart container - The tablist container.
 */
class TabList extends ListNavigationMixin(CaptureDestroyEventForChildElement(KeyDownHandledMixin(KeyToActionMixin(TabListComponentMixin(Component))))) {
    /**
     * @internal
     */
    get navItems() {
        return this.itemsStore.items;
    }
    /**
     * @internal
     */
    isRtl() {
        var _a;
        return (((_a = document.querySelector('html')) === null || _a === void 0 ? void 0 : _a.getAttribute('dir')) === 'rtl' || window.getComputedStyle(this).direction === 'rtl');
    }
    /**
     * @internal
     */
    isValidItem(item) {
        return item.matches(`${TAB_TAGNAME}:not([disabled])`);
    }
    /**
     * @internal
     */
    onElementStoreUpdate(item, changeType, index) {
        if (changeType === 'added') {
            // Update the tabIndex of the list items when a new item is added.
            item.tabIndex = -1;
            if (this.navItems.length === 0) {
                // If this is the first item added, set its tabIndex to 0 to make it focusable
                item.tabIndex = 0;
            }
        }
        else if (changeType === 'removed' && item.tabIndex === 0) {
            let newIndex = index + 1;
            if (newIndex >= this.navItems.length) {
                newIndex = index - 1;
            }
            const tab = this.navItems.at(newIndex);
            if (tab instanceof Tab) {
                tab.focus();
            }
            this.resetTabIndexes(newIndex, false);
        }
    }
    /**
     * Overriding the resetTabIndexAndSetFocus function to set the focus on the new active tab when the active tab changes.
     * Also, handle the visibility of the arrow buttons after resetting the tab indexes and setting the focus.
     * @internal
     */
    resetTabIndexAndSetFocus(newIndex, oldIndex) {
        var _a;
        const result = super.resetTabIndexAndSetFocus(newIndex, oldIndex, false, false);
        const itemToFocus = (_a = this.navItems) === null || _a === void 0 ? void 0 : _a.at(newIndex);
        if (itemToFocus instanceof Tab) {
            itemToFocus.focus();
        }
        this.handleArrowButtonVisibility();
        return result;
    }
    constructor() {
        super();
        /**
         * @internal
         */
        this.orientation = 'horizontal';
        /**
         * Instead of using 'left' and 'right' for notation of the arrow buttons, we use 'forward' and 'backward'.
         * Since the elements of the tablist gets flipped in RTL layouts, the arrow buttons also flip.
         * For both RTL and LTR layouts, the forward arrow button moves the tabs in the forward direction.
         * i.e. the forward arrow button moves the tabs to the right in LTR layouts and to the left in RTL layouts.
         */
        /**
         * @internal
         */
        this.showForwardArrowButton = false;
        /**
         * @internal
         */
        this.showBackwardArrowButton = false;
        // This must be initialized after the destroyed event listener
        // to keep the element in the itemStore in order to move the focus correctly
        this.itemsStore = new ElementStore(this, {
            isValidItem: this.isValidItem,
            onStoreUpdate: this.onElementStoreUpdate.bind(this),
        });
    }
    /**
     * Set the initial active tab after firstUpdated,
     * since this.tabs (=slot) are only available after
     * the first update
     *
     * Also set up the event listeners.
     */
    async firstUpdated() {
        var _a, _b;
        if (!this.navItems) {
            return;
        }
        /**
         * If there are no tabs, skip the initialization of the active tab and the event listeners.
         * Then throw an error.
         */
        if (Array.isArray(this.navItems) && this.navItems.length === 0) {
            if (this.onerror) {
                this.onerror('The tablist component must have at least one child tab');
            }
            return;
        }
        const tabIds = this.navItems.map(tab => tab.tabId);
        if (new Set(tabIds).size !== this.navItems.length) {
            if (this.onerror) {
                this.onerror('The tabs inside the tab list must have unique tab ids');
            }
        }
        const resizeObserver = new ResizeObserver(async () => {
            this.handleArrowButtonVisibility();
        });
        resizeObserver.observe(this);
        if (!this.activeTabId) {
            this.activeTabId = (_a = getFirstTab(this.navItems)) === null || _a === void 0 ? void 0 : _a.tabId;
        }
        (_b = this.tabsContainer) === null || _b === void 0 ? void 0 : _b.addEventListener('focusin', this.handleFocus.bind(this));
    }
    disconnectedCallback() {
        var _a;
        super.disconnectedCallback();
        (_a = this.tabsContainer) === null || _a === void 0 ? void 0 : _a.removeEventListener('focusin', this.handleFocus);
    }
    /**
     * Override focus to delegate to the active tab.
     * When a dialog focus-trap or any other caller invokes `.focus()` on the tablist host,
     * focus is forwarded directly to the active tab instead of landing on the shadow host.
     */
    focus(options) {
        const activeTab = getActiveTab(this.navItems);
        if (activeTab) {
            activeTab.focus(options);
        }
        else {
            super.focus(options);
        }
    }
    /**
     * When the tablist receives focus, then focus the active tab.
     *
     * @param event - Focus event.
     * @internal
     */
    async handleFocus(event) {
        /**
         * If the element losing focus is a tab, do nothing.
         * If the element gaining focus is not a tab or the container itself (e.g., via delegatesFocus
         * or a click on the container), do nothing — the focus() override handles the programmatic case.
         * This also covers the case when previous focus was on a tab that belongs to another tablist.
         */
        if (event.relatedTarget instanceof Tab || !(event.target instanceof Tab)) {
            return;
        }
        const activeTab = getActiveTab(this.navItems || []);
        if (!(activeTab instanceof Tab)) {
            return;
        }
        const activeTabIndex = this.navItems.findIndex(tab => tab === activeTab);
        this.resetTabIndexAndSetFocus(activeTabIndex, undefined);
    }
    /**
     * Should the arrow button be shown.
     *
     * @internal
     *
     * @param direction - The direction of the arrow button.
     */
    shouldShowArrowButton(direction) {
        return direction === ARROW_BUTTON_DIRECTION.FORWARD ? this.showForwardArrowButton : this.showBackwardArrowButton;
    }
    /**
     * If an arrow button is in focus and it gets removed, switch focus to the opposite arrow button.
     * If both arrow buttons are removed, switch focus to the active tab.
     *
     * @internal
     */
    switchFocus() {
        var _a, _b;
        if (!this.showBackwardArrowButton && !this.showForwardArrowButton) {
            (_a = getActiveTab(this.navItems || [])) === null || _a === void 0 ? void 0 : _a.focus();
        }
        else if ((this.showBackwardArrowButton && !this.showForwardArrowButton) ||
            (this.showForwardArrowButton && !this.showBackwardArrowButton)) {
            (_b = this.notFocusedArrowButton) === null || _b === void 0 ? void 0 : _b.focus();
        }
    }
    /**
     * Show or hide the arrow buttons based on the position of the tabs
     * corresponding to the tab list.
     *
     * @internal
     */
    handleArrowButtonVisibility() {
        var _a, _b, _c;
        if (!this.navItems || !this.tabsContainer) {
            return;
        }
        let isArrowButtonFocused = false;
        /**
         * If the active element is an arrow button,
         * then run the switchFocus function after setting the arrow button visibility.
         */
        if (((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) instanceof Button) {
            isArrowButtonFocused = true;
        }
        const firstTab = getFirstTab(this.navItems);
        const lastTab = getLastTab(this.navItems);
        if (!firstTab || !lastTab) {
            return;
        }
        const firstTabLeftEdgePosition = firstTab.getBoundingClientRect().left;
        const tabListLeftEdgePosition = (_b = this.tabsContainer) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect().left;
        const lastTabRightEdgePosition = lastTab.getBoundingClientRect().right;
        const tabListRightEdgePosition = (_c = this.tabsContainer) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect().right;
        // when RTL, the edges are reversed
        const firstTabRightEdgePosition = firstTab.getBoundingClientRect().right;
        const lastTabLeftEdgePosition = lastTab.getBoundingClientRect().left;
        if (!this.isRtl()) {
            if (firstTabLeftEdgePosition <= tabListLeftEdgePosition) {
                this.showBackwardArrowButton = true;
            }
            else {
                this.showBackwardArrowButton = false;
            }
            if (lastTabRightEdgePosition > tabListRightEdgePosition) {
                this.showForwardArrowButton = true;
            }
            else {
                this.showForwardArrowButton = false;
            }
            if (isArrowButtonFocused) {
                this.switchFocus();
            }
            return;
        }
        if (firstTabRightEdgePosition > tabListRightEdgePosition) {
            this.showBackwardArrowButton = true;
        }
        else {
            this.showBackwardArrowButton = false;
        }
        if (lastTabLeftEdgePosition < tabListLeftEdgePosition) {
            this.showForwardArrowButton = true;
        }
        else {
            this.showForwardArrowButton = false;
        }
        if (isArrowButtonFocused) {
            this.switchFocus();
        }
    }
    /**
     * Scroll tabs forward or backward.
     *
     * @internal
     *
     * @param direction - The direction to scroll the tabs in.
     */
    async scrollTabs(direction) {
        var _a;
        const forwardMultiplier = !this.isRtl() ? 1 : -1;
        const backwardMultiplier = !this.isRtl() ? -1 : 1;
        (_a = this.tabsContainer) === null || _a === void 0 ? void 0 : _a.scrollBy({
            left: this.tabsContainer.clientWidth *
                (direction === ARROW_BUTTON_DIRECTION.FORWARD ? forwardMultiplier : backwardMultiplier),
            // @ts-ignore : https://github.com/Microsoft/TypeScript/issues/28755
            behavior: 'instant',
        });
        this.handleArrowButtonVisibility();
    }
    render() {
        const forwardArrowDirection = !this.isRtl() ? 'right' : 'left';
        const backwardArrowDirection = !this.isRtl() ? 'left' : 'right';
        const arrowButton = (direction) => html ` ${this.shouldShowArrowButton(direction)
            ? html `<mdc-button
            variant="tertiary"
            prefix-icon="arrow-${direction === ARROW_BUTTON_DIRECTION.FORWARD
                ? forwardArrowDirection
                : backwardArrowDirection}-regular"
            aria-label="Scroll tabs ${direction === ARROW_BUTTON_DIRECTION.FORWARD
                ? forwardArrowDirection
                : backwardArrowDirection}"
            @click="${() => this.scrollTabs(direction)}"
          ></mdc-button>`
            : nothing}`;
        return html ` ${arrowButton('backward')}
      <div part="container" role="${ROLE.TABLIST}" tabindex="-1" aria-label="${ifDefined(this.dataAriaLabel)}">
        <slot></slot>
      </div>
      ${arrowButton(ARROW_BUTTON_DIRECTION.FORWARD)}`;
    }
}
TabList.styles = [...Component.styles, ...styles];
TabList.shadowRootOptions = { ...Component.shadowRootOptions, delegatesFocus: true };
__decorate([
    property({ type: String, attribute: 'data-aria-label' }),
    __metadata("design:type", String)
], TabList.prototype, "dataAriaLabel", void 0);
__decorate([
    query('[part="container"]'),
    __metadata("design:type", HTMLDivElement)
], TabList.prototype, "tabsContainer", void 0);
__decorate([
    query('mdc-button:not(:focus-visible)'),
    __metadata("design:type", Button)
], TabList.prototype, "notFocusedArrowButton", void 0);
__decorate([
    state(),
    __metadata("design:type", Boolean)
], TabList.prototype, "showForwardArrowButton", void 0);
__decorate([
    state(),
    __metadata("design:type", Boolean)
], TabList.prototype, "showBackwardArrowButton", void 0);
export default TabList;
