import { LitElement, html } from 'lit';
import {
customElement,
property,
queryAssignedElements,
} from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import TabsScss from './tabs.scss';
/**
* Tabs.
* @slot unnamed - Slot for kyn-tab-panel components.
* @slot tabs - Slot for kyn-tab components.
* @fires on-change - Emits the new selected Tab ID when switching tabs.
*/
@customElement('kyn-tabs')
export class Tabs extends LitElement {
static override styles = TabsScss;
/** Tab style. `'contained'` or `'line'`. */
@property({ type: String })
tabStyle = 'contained';
/** Size of the tab buttons, `'sm'` or `'md'`. Icon size: 16px. */
@property({ type: String })
tabSize = 'md';
/** Vertical orientation. */
@property({ type: Boolean })
vertical = false;
/** Queries for slotted tabs.
* @internal
*/
@queryAssignedElements({ slot: 'tabs', selector: 'kyn-tab' })
_tabs!: any;
/** Queries for slotted tab panels.
* @internal
*/
@queryAssignedElements({ selector: 'kyn-tab-panel' })
_tabPanels!: any;
override render() {
const wrapperClasses = {
wrapper: true,
vertical: this.vertical,
};
const tabsClasses = {
tabs: true,
contained: this.tabStyle === 'contained',
line: this.tabStyle === 'line',
};
return html`
this._handleKeyboard(e)}
>
`;
}
override connectedCallback() {
super.connectedCallback();
this.addEventListener('tab-activated', (e) => this._handleChange(e));
}
override disconnectedCallback() {
this.removeEventListener('tab-activated', (e) => this._handleChange(e));
super.disconnectedCallback();
}
override willUpdate(changedProps: any) {
if (
changedProps.has('tabSize') ||
changedProps.has('vertical') ||
changedProps.has('tabStyle')
) {
this._updateChildren();
}
}
private _handleSlotChangeTabs() {
this._updateChildren();
}
private _updateChildren() {
this._tabs.forEach((tab: any) => {
tab._size = this.tabSize;
tab._vertical = this.vertical;
tab._tabStyle = this.tabStyle;
});
this._tabPanels.forEach((tabPanel: any) => {
tabPanel._vertical = this.vertical;
});
}
/**
* Updates children and emits a change event based on the provided
* event details when a child kyn-tab is clicked.
* @param {any} e - The parameter "e" is an event object that contains information about the event
* that triggered the handleChange function.
*/
private _handleChange(e: any) {
e.stopPropagation();
this._updateChildrenSelection(e.detail.tabId);
this._emitChangeEvent(e.detail.origEvent, e.detail.tabId);
}
/**
* Updates the selected property of tabs and the visible property of tab panels based on
* the selected tab ID.
* @param {string} selectedTabId - The selectedTabId parameter is a string that represents the ID of
* the tab that is currently selected.
*/
private _updateChildrenSelection(selectedTabId: string) {
// update tabs selected prop
this._tabs.forEach((tab: any) => {
tab.selected = tab.id === selectedTabId;
});
// update tab-panels visible prop
this._tabPanels.forEach((tabPanel: any) => {
tabPanel.visible = tabPanel.tabId === selectedTabId;
});
}
/**
* Creates and dispatches a custom event called 'on-change' with the provided original event and
* selected tab ID as details.
* @param {any} origEvent - The origEvent parameter is the original event object that triggered the
* change event. It could be any type of event object, such as a click event or a keydown event.
* @param {string} selectedTabId - The selectedTabId parameter is a string that represents the ID of
* the selected tab.
*/
private _emitChangeEvent(origEvent: any, selectedTabId: string) {
const event = new CustomEvent('on-change', {
detail: { origEvent: origEvent, selectedTabId: selectedTabId },
});
this.dispatchEvent(event);
}
/**
* Handles keyboard events for navigating between tabs.
* @param {any} e - The parameter `e` is an event object that represents the keyboard event. It
* contains information about the keyboard event, such as the key code of the pressed key.
* @returns In this code, the function `_handleKeyboard` returns nothing in all cases
* except when the `keyCode` matches the left or right arrow key codes.
*/
private _handleKeyboard(e: any) {
const LEFT_ARROW_KEY_CODE = 37;
const UP_ARROW_KEY_CODE = 38;
const RIGHT_ARROW_KEY_CODE = 39;
const DOWN_ARROW_KEY_CODE = 40;
const TabCount = this._tabs.length;
const SelectedTabIndex = this._tabs.findIndex((tab: any) => tab.selected);
switch (e.keyCode) {
case LEFT_ARROW_KEY_CODE:
case UP_ARROW_KEY_CODE: {
// activate previous tab
let prevIndex =
SelectedTabIndex === 0 ? TabCount - 1 : SelectedTabIndex - 1;
let prevTab = this._tabs[prevIndex];
if (prevTab.disabled) {
prevIndex = prevIndex === 0 ? TabCount - 1 : prevIndex - 1;
prevTab = this._tabs[prevIndex];
}
prevTab.focus();
this._updateChildrenSelection(prevTab.id);
this._emitChangeEvent(e, prevTab.id);
return;
}
case RIGHT_ARROW_KEY_CODE:
case DOWN_ARROW_KEY_CODE: {
// activate next tab
let nextIndex =
SelectedTabIndex === TabCount - 1 ? 0 : SelectedTabIndex + 1;
let nextTab = this._tabs[nextIndex];
if (nextTab.disabled) {
nextIndex = nextIndex === TabCount - 1 ? 0 : nextIndex + 1;
nextTab = this._tabs[nextIndex];
}
nextTab.focus();
this._updateChildrenSelection(nextTab.id);
this._emitChangeEvent(e, nextTab.id);
return;
}
default: {
return;
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
'kyn-tabs': Tabs;
}
}