import { TailwindElement } from "@/classes/TailwindElement";
import { LitElement, html, css } from "lit";
import { property, queryAsync, customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
const DEFAULT_PANEL_ID = "default-panel";
// Match witch tailwind 4xl max width
// https://tailwindcss.com/docs/max-width
const SCREEN_LG_PX = 896;
/**
* Tab list
*
* Usage example:
* ```ts
*
* One
* Two
*
*
* Tab one content
* Tab two content
* ```
*/
@customElement("btrix-tab-panel")
export class TabPanel extends TailwindElement {
@property({ type: String })
name?: string;
@property({ type: Boolean })
active = false;
render() {
return html`
`;
}
}
@customElement("btrix-tab")
export class Tab extends TailwindElement {
// ID of panel the tab labels/controls
@property({ type: String })
name?: string;
@property({ type: Boolean })
active = false;
@property({ type: Boolean })
disabled = false;
render() {
return html`
`;
}
}
type TabElement = Tab & HTMLElement;
type TabPanelElement = TabPanel & HTMLElement;
@customElement("btrix-tab-list")
export class TabList extends LitElement {
static styles = css`
:host {
--track-width: 4px;
}
.container {
display: grid;
grid-template-areas:
"menu"
"header"
"main";
grid-template-columns: 1fr;
grid-gap: 1rem;
}
@media only screen and (min-width: ${SCREEN_LG_PX}px) {
.container {
grid-template-areas:
". header"
"menu main";
grid-template-columns: auto minmax(auto, 70rem);
}
}
.navWrapper {
grid-area: menu;
}
@media only screen and (min-width: ${SCREEN_LG_PX}px) {
.navWrapper {
overflow: initial;
}
}
.header {
grid-area: header;
font-size: var(--sl-font-size-large);
font-weight: 500;
line-height: 1;
}
.content {
grid-area: main;
}
.nav {
position: relative;
position: -webkit-sticky;
position: sticky;
top: var(--sl-spacing-medium);
}
.tablist {
display: flex;
margin: 0;
list-style: none;
padding: 0;
}
.show-indicator .tablist {
margin-left: var(--track-width);
}
@media only screen and (min-width: ${SCREEN_LG_PX}px) {
.tablist {
display: block;
}
}
.track {
display: none;
position: absolute;
top: 0;
height: 100%;
width: var(--track-width);
border-radius: var(--track-width);
background-color: var(--sl-color-neutral-100);
box-shadow: inset 0 0 2px var(--sl-color-neutral-300);
}
.indicator {
display: none;
position: absolute;
width: var(--track-width);
border-radius: var(--track-width);
background-color: var(--sl-color-blue-500);
}
@media only screen and (min-width: ${SCREEN_LG_PX}px) {
.tablist,
.show-indicator .track,
.show-indicator .indicator {
display: block;
}
}
`;
// ID of visible panel
@property({ type: String })
activePanel: string = DEFAULT_PANEL_ID;
// If panels are linear, the current panel in progress
@property({ type: String })
progressPanel?: string;
@property({ type: Boolean })
hideIndicator = false;
@queryAsync(".track")
private trackElem!: HTMLElement;
@queryAsync(".indicator")
private indicatorElem!: HTMLElement;
updated(changedProperties: Map) {
if (changedProperties.has("activePanel") && this.activePanel) {
this.onActiveChange(!changedProperties.get("activePanel"));
}
if (changedProperties.has("progressPanel") && this.progressPanel) {
this.onProgressChange(!changedProperties.get("progressPanel"));
}
}
private async repositionIndicator(activeTab?: TabElement, animate = true) {
if (!activeTab || this.hideIndicator) return;
const trackElem = await this.trackElem;
const indicatorElem = await this.indicatorElem;
const { top: tabTop, height: tabHeight } =
activeTab.getBoundingClientRect();
const top = tabTop - trackElem.getBoundingClientRect().top;
if (animate) {
indicatorElem.style.transition =
"var(--sl-transition-fast) transform ease, var(--sl-transition-fast) height ease";
} else {
indicatorElem.style.transition = "";
}
if (this.progressPanel) {
indicatorElem.style.height = `${top + tabHeight}px`;
} else {
indicatorElem.style.height = `${tabHeight}px`;
indicatorElem.style.transform = `translateY(${top}px)`;
}
}
render() {
return html`
`;
}
renderNav() {
return html`
this.repositionIndicator(this.getTab(this.progressPanel))}
>
`;
}
private getPanels(): TabPanelElement[] {
const slotElems = (
this.renderRoot.querySelector(
".content slot:not([name])"
) as HTMLSlotElement
).assignedElements();
return ([...slotElems] as TabPanelElement[]).filter(
(el) => el.tagName.toLowerCase() === "btrix-tab-panel"
);
}
private getTabs(): TabElement[] {
const slotElems = (
this.renderRoot.querySelector("slot[name='nav']") as HTMLSlotElement
).assignedElements();
return ([...slotElems] as TabElement[]).filter(
(el) => el.tagName.toLowerCase() === "btrix-tab"
);
}
private getTab(tabName?: string): TabElement | undefined {
if (!tabName) return;
const tabs = this.getTabs();
return tabs.find(({ name }) => name === tabName);
}
private onProgressChange(isFirstChange: boolean) {
const progressTab = this.getTabs().find(
(el) => el.name === this.progressPanel
);
if (progressTab) {
this.repositionIndicator(progressTab, !isFirstChange);
}
}
private onActiveChange(isFirstChange: boolean) {
this.getTabs().forEach((tab) => {
if (tab.name === this.activePanel) {
tab.active = true;
if (!this.progressPanel) {
this.repositionIndicator(tab, !isFirstChange);
}
} else {
tab.active = false;
}
});
this.getPanels().forEach((panel) => {
panel.active = panel.name === this.activePanel;
if (panel.active) {
panel.style.display = "flex";
panel.setAttribute("aria-hidden", "false");
} else {
panel.style.display = "none";
panel.setAttribute("aria-hidden", "true");
}
});
}
}