chromium/third_party/material_web_components/components-chromium/node_modules/@material/web/tabs/internal/tabs.js

/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { __decorate } from "tslib";
import '../../divider/divider.js';
import { html, isServer, LitElement } from 'lit';
import { property, query, queryAssignedElements } from 'lit/decorators.js';
import { ANIMATE_INDICATOR } from './tab.js';
/**
 * @fires change {Event} Fired when the selected tab changes. The target's
 * `activeTabIndex` or `activeTab` provide information about the selection
 * change. The change event is fired when a user interaction like a space/enter
 * key or click cause a selection change. The tab selection based on these
 * actions can be cancelled by calling preventDefault on the triggering
 * `keydown` or `click` event. --bubbles
 *
 * @example
 * // perform an action if a tab is clicked
 * tabs.addEventListener('change', (event: Event) => {
 *   if (event.target.activeTabIndex === 2)
 *     takeAction();
 *   }
 * });
 *
 * // prevent a click from triggering tab selection under some condition
 * tabs.addEventListener('click', (event: Event) => {
 *   if (notReady)
 *     event.preventDefault();
 *   }
 * });
 *
 */
export class Tabs extends LitElement {
    /**
     * The currently selected tab, `null` only when there are no tab children.
     *
     * @export
     */
    get activeTab() {
        return this.tabs.find((tab) => tab.active) ?? null;
    }
    set activeTab(tab) {
        // Ignore setting activeTab to null. As long as there are children, one tab
        // must be selected.
        if (tab) {
            this.activateTab(tab);
        }
    }
    /**
     * The index of the currently selected tab.
     *
     * @export
     */
    get activeTabIndex() {
        return this.tabs.findIndex((tab) => tab.active);
    }
    set activeTabIndex(index) {
        const activateTabAtIndex = () => {
            const tab = this.tabs[index];
            // Ignore out-of-bound indices.
            if (tab) {
                this.activateTab(tab);
            }
        };
        if (!this.slotElement) {
            // This is needed to support setting the activeTabIndex via a lit property
            // binding.
            //
            // ```ts
            // html`
            //   <md-tabs .activeTabIndex=${1}>
            //     <md-tab>First</md-tab>
            //     <md-tab>Second</md-tab>
            //   </md-tabs>
            // `;
            // ```
            //
            // It's needed since lit's rendering lifecycle is asynchronous, and the
            // `<slot>` element hasn't rendered, so `tabs` is empty.
            this.updateComplete.then(activateTabAtIndex);
            return;
        }
        activateTabAtIndex();
    }
    get focusedTab() {
        return this.tabs.find((tab) => tab.matches(':focus-within'));
    }
    constructor() {
        super();
        /**
         * Whether or not to automatically select a tab when it is focused.
         */
        this.autoActivate = false;
        this.internals = 
        // Cast needed for closure
        this.attachInternals();
        if (!isServer) {
            this.internals.role = 'tablist';
            this.addEventListener('keydown', this.handleKeydown.bind(this));
            this.addEventListener('keyup', this.handleKeyup.bind(this));
            this.addEventListener('focusout', this.handleFocusout.bind(this));
        }
    }
    /**
     * Scrolls the toolbar, if overflowing, to the active tab, or the provided
     * tab.
     *
     * @param tabToScrollTo The tab that should be scrolled to. Defaults to the
     *     active tab.
     * @return A Promise that resolves after the tab has been scrolled to.
     */
    async scrollToTab(tabToScrollTo) {
        await this.updateComplete;
        const { tabs } = this;
        tabToScrollTo ??= this.activeTab;
        if (!tabToScrollTo ||
            !tabs.includes(tabToScrollTo) ||
            !this.tabsScrollerElement) {
            return;
        }
        // wait for tabs to render.
        for (const tab of this.tabs) {
            await tab.updateComplete;
        }
        const offset = tabToScrollTo.offsetLeft;
        const extent = tabToScrollTo.offsetWidth;
        const scroll = this.scrollLeft;
        const hostExtent = this.offsetWidth;
        const scrollMargin = 48;
        const min = offset - scrollMargin;
        const max = offset + extent - hostExtent + scrollMargin;
        const to = Math.min(min, Math.max(max, scroll));
        // When a tab is focused, use 'auto' to use the CSS `scroll-behavior`. The
        // default behavior is smooth scrolling. However, when there is not a tab
        // focused on initialization, use 'instant' to immediately bring the focused
        // tab into view.
        const behavior = !this.focusedTab ? 'instant' : 'auto';
        this.tabsScrollerElement.scrollTo({ behavior, top: 0, left: to });
    }
    render() {
        return html `
      <div class="tabs">
        <slot
          @slotchange=${this.handleSlotChange}
          @click=${this.handleTabClick}></slot>
      </div>
      <md-divider part="divider"></md-divider>
    `;
    }
    async handleTabClick(event) {
        const tab = event.target;
        // Allow event to bubble
        await 0;
        if (event.defaultPrevented || !isTab(tab) || tab.active) {
            return;
        }
        this.activateTab(tab);
    }
    activateTab(activeTab) {
        const { tabs } = this;
        const previousTab = this.activeTab;
        if (!tabs.includes(activeTab) || previousTab === activeTab) {
            // Ignore setting activeTab to a tab element that is not a child.
            return;
        }
        for (const tab of tabs) {
            tab.active = tab === activeTab;
        }
        if (previousTab) {
            // Don't dispatch a change event if activating a tab when no previous tabs
            // were selected, such as when md-tabs auto-selects the first tab.
            const defaultPrevented = !this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
            if (defaultPrevented) {
                for (const tab of tabs) {
                    tab.active = tab === previousTab;
                }
                return;
            }
            activeTab[ANIMATE_INDICATOR](previousTab);
        }
        this.updateFocusableTab(activeTab);
        this.scrollToTab(activeTab);
    }
    updateFocusableTab(focusableTab) {
        for (const tab of this.tabs) {
            tab.tabIndex = tab === focusableTab ? 0 : -1;
        }
    }
    // focus item on keydown and optionally select it
    async handleKeydown(event) {
        // Allow event to bubble.
        await 0;
        const isLeft = event.key === 'ArrowLeft';
        const isRight = event.key === 'ArrowRight';
        const isHome = event.key === 'Home';
        const isEnd = event.key === 'End';
        // Ignore non-navigation keys
        if (event.defaultPrevented || (!isLeft && !isRight && !isHome && !isEnd)) {
            return;
        }
        const { tabs } = this;
        // Don't try to select another tab if there aren't any.
        if (tabs.length < 2) {
            return;
        }
        // Prevent default interactions, such as scrolling.
        event.preventDefault();
        let indexToFocus;
        if (isHome || isEnd) {
            indexToFocus = isHome ? 0 : tabs.length - 1;
        }
        else {
            // Check if moving forwards or backwards
            const isRtl = getComputedStyle(this).direction === 'rtl';
            const forwards = isRtl ? isLeft : isRight;
            const { focusedTab } = this;
            if (!focusedTab) {
                // If there is not already a tab focused, select the first or last tab
                // based on the direction we're traveling.
                indexToFocus = forwards ? 0 : tabs.length - 1;
            }
            else {
                const focusedIndex = this.tabs.indexOf(focusedTab);
                indexToFocus = forwards ? focusedIndex + 1 : focusedIndex - 1;
                if (indexToFocus >= tabs.length) {
                    // Return to start if moving past the last item.
                    indexToFocus = 0;
                }
                else if (indexToFocus < 0) {
                    // Go to end if moving before the first item.
                    indexToFocus = tabs.length - 1;
                }
            }
        }
        const tabToFocus = tabs[indexToFocus];
        tabToFocus.focus();
        if (this.autoActivate) {
            this.activateTab(tabToFocus);
        }
        else {
            this.updateFocusableTab(tabToFocus);
        }
    }
    // scroll to item on keyup.
    handleKeyup() {
        this.scrollToTab(this.focusedTab ?? this.activeTab);
    }
    handleFocusout() {
        // restore focus to selected item when blurring the tab bar.
        if (this.matches(':focus-within')) {
            return;
        }
        const { activeTab } = this;
        if (activeTab) {
            this.updateFocusableTab(activeTab);
        }
    }
    handleSlotChange() {
        const firstTab = this.tabs[0];
        if (!this.activeTab && firstTab) {
            // If the active tab was removed, auto-select the first one. There should
            // always be a selected tab while the bar has children.
            this.activateTab(firstTab);
        }
        // When children shift, ensure the active tab is visible. For example, if
        // many children are added before the active tab, it'd be pushed off screen.
        // This ensures it stays visible.
        this.scrollToTab(this.activeTab);
    }
}
__decorate([
    queryAssignedElements({ flatten: true, selector: '[md-tab]' })
], Tabs.prototype, "tabs", void 0);
__decorate([
    property({ type: Number, attribute: 'active-tab-index' })
], Tabs.prototype, "activeTabIndex", null);
__decorate([
    property({ type: Boolean, attribute: 'auto-activate' })
], Tabs.prototype, "autoActivate", void 0);
__decorate([
    query('.tabs')
], Tabs.prototype, "tabsScrollerElement", void 0);
__decorate([
    query('slot')
], Tabs.prototype, "slotElement", void 0);
function isTab(element) {
    return element instanceof HTMLElement && element.hasAttribute('md-tab');
}
//# sourceMappingURL=tabs.js.map