195 lines
5.3 KiB
JavaScript
195 lines
5.3 KiB
JavaScript
/**
|
|
* Content Tabs Component
|
|
* Tabbed interface for displaying different content sections
|
|
*/
|
|
class ContentTabs extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: "open" });
|
|
this.activeTab = 0;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
get tabs() {
|
|
const tabsAttr = this.getAttribute("tabs");
|
|
return tabsAttr ? tabsAttr.split(",").map((t) => t.trim()) : [];
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const tabButtons = this.shadowRoot.querySelectorAll(".tab-button");
|
|
tabButtons.forEach((button, index) => {
|
|
button.addEventListener("click", () => {
|
|
this.setActiveTab(index);
|
|
});
|
|
|
|
// Keyboard navigation
|
|
button.addEventListener("keydown", (e) => {
|
|
const tabCount = this.tabs.length;
|
|
let newIndex = this.activeTab;
|
|
|
|
switch (e.key) {
|
|
case "ArrowLeft":
|
|
newIndex = (this.activeTab - 1 + tabCount) % tabCount;
|
|
break;
|
|
case "ArrowRight":
|
|
newIndex = (this.activeTab + 1) % tabCount;
|
|
break;
|
|
case "Home":
|
|
newIndex = 0;
|
|
break;
|
|
case "End":
|
|
newIndex = tabCount - 1;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
this.setActiveTab(newIndex);
|
|
this.shadowRoot.querySelectorAll(".tab-button")[newIndex]?.focus();
|
|
});
|
|
});
|
|
}
|
|
|
|
setActiveTab(index) {
|
|
this.activeTab = index;
|
|
this.updateTabs();
|
|
this.dispatchEvent(
|
|
new CustomEvent("tab-change", {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: { index, tab: this.tabs[index] },
|
|
})
|
|
);
|
|
}
|
|
|
|
updateTabs() {
|
|
// Update tab buttons
|
|
const tabButtons = this.shadowRoot.querySelectorAll(".tab-button");
|
|
tabButtons.forEach((button, index) => {
|
|
const isActive = index === this.activeTab;
|
|
button.classList.toggle("active", isActive);
|
|
button.setAttribute("aria-selected", isActive.toString());
|
|
button.setAttribute("tabindex", isActive ? "0" : "-1");
|
|
});
|
|
|
|
// Update panels in shadow DOM
|
|
const shadowPanels = this.shadowRoot.querySelectorAll("[role='tabpanel']");
|
|
shadowPanels.forEach((panel, index) => {
|
|
panel.style.display = index === this.activeTab ? "block" : "none";
|
|
});
|
|
|
|
// Update slotted panels
|
|
const panels = this.querySelectorAll("[slot^='panel-']");
|
|
panels.forEach((panel, index) => {
|
|
panel.style.display = index === this.activeTab ? "block" : "none";
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const tabsHtml = this.tabs
|
|
.map(
|
|
(tab, index) => `
|
|
<button
|
|
class="tab-button ${index === this.activeTab ? "active" : ""}"
|
|
type="button"
|
|
role="tab"
|
|
id="tab-${index}"
|
|
aria-selected="${index === this.activeTab}"
|
|
aria-controls="panel-${index}"
|
|
tabindex="${index === this.activeTab ? "0" : "-1"}"
|
|
>
|
|
${tab}
|
|
</button>
|
|
`
|
|
)
|
|
.join("");
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
:host {
|
|
display: block;
|
|
margin: var(--spacing-xl, 2rem) 0;
|
|
}
|
|
|
|
.tabs-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.tab-list {
|
|
display: flex;
|
|
gap: var(--spacing-md, 1rem);
|
|
margin-top: var(--spacing-lg, 1.5rem);
|
|
border-bottom: 1px solid var(--color-border, #e2e8f0);
|
|
}
|
|
|
|
.tab-button {
|
|
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 4px;
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
font-family: var(--font-family-outfit, "Outfit", sans-serif);
|
|
font-size: 16px;
|
|
font-weight: 400;
|
|
line-height: 24px;
|
|
color: var(--color-purple, #951d51);
|
|
text-decoration: underline;
|
|
text-underline-offset: 3px;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast, 150ms ease);
|
|
}
|
|
|
|
.tab-button.active {
|
|
background: var(--color-border, #e2e8f0);
|
|
color: var(--color-text, #1e293b);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.tab-button:not(.active):hover {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.tab-button:focus {
|
|
outline: 2px solid var(--color-purple, #951d51);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.tab-panels {
|
|
min-height: 100px;
|
|
background: var(--color-border, #e2e8f0);
|
|
padding: var(--spacing-lg, 1.5rem) var(--spacing-md, 1rem);
|
|
border-radius: var(--radius-sm, 0.25rem);
|
|
}
|
|
</style>
|
|
<div class="tabs-container">
|
|
<div class="tab-list" role="tablist">
|
|
${tabsHtml}
|
|
</div>
|
|
<div class="tab-panels">
|
|
<div id="panel-0" role="tabpanel" aria-labelledby="tab-0">
|
|
<slot name="panel-0"></slot>
|
|
</div>
|
|
<div id="panel-1" role="tabpanel" aria-labelledby="tab-1">
|
|
<slot name="panel-1"></slot>
|
|
</div>
|
|
<div id="panel-2" role="tabpanel" aria-labelledby="tab-2">
|
|
<slot name="panel-2"></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Initialize panel visibility
|
|
setTimeout(() => this.updateTabs(), 0);
|
|
}
|
|
}
|
|
|
|
customElements.define("content-tabs", ContentTabs);
|