/** * Coordinate tabs and their aria-controls panels inside one tab list. * * Tabs identify their logical name with data-tab. Selecting a tab updates the * ARIA state, panel visibility and roving keyboard focus. */ export class TabSet { constructor(element) { this.element = element; this.tabs = Array.from(element.querySelectorAll('[role="tab"]')); this.selectionHandlers = []; this.element.addEventListener("click", (event) => { const tab = event.target.closest('[role="tab"]'); if (tab && this.element.contains(tab)) this.select(tab.dataset.tab); }); this.element.addEventListener("keydown", (event) => this.handleKeydown(event)); } /** Select a named tab and show the panel named by its aria-controls value. */ select(name) { for (const tab of this.tabs) { const selected = tab.dataset.tab === name; tab.setAttribute("aria-selected", String(selected)); tab.tabIndex = selected ? 0 : -1; const panel = document.getElementById(tab.getAttribute("aria-controls")); if (panel) panel.classList.toggle("hidden", !selected); } for (const handler of this.selectionHandlers) handler(name); } onSelect(handler) { this.selectionHandlers.push(handler); return this; } handleKeydown(event) { if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; const current = this.tabs.indexOf(event.target.closest('[role="tab"]')); if (current < 0) return; event.preventDefault(); const next = event.key === "Home" ? 0 : event.key === "End" ? this.tabs.length - 1 : (current + (event.key === "ArrowRight" ? 1 : -1) + this.tabs.length) % this.tabs.length; this.select(this.tabs[next].dataset.tab); this.tabs[next].focus(); } }