/** * Upgrades server-rendered tab sections to a real tab widget. * * Without this script the page shows an anchor list followed by every panel, headings and all: * complete, readable and keyboard operable. The script only changes presentation, never content, * and it is loaded exclusively on pages that actually contain a tabs block. */ (() => { const setup = group => { const list = group.querySelector('.tabs-nav'); const links = [...list.querySelectorAll('a')]; const panels = links.map(link => document.getElementById(link.getAttribute('href').slice(1))); if (links.length < 2 || panels.some(panel => panel === null)) return; list.setAttribute('role', 'tablist'); list.querySelectorAll('li').forEach(item => item.setAttribute('role', 'presentation')); links.forEach((link, index) => { link.setAttribute('role', 'tab'); link.id = `${panels[index].id}-tab`; link.setAttribute('aria-controls', panels[index].id); panels[index].setAttribute('role', 'tabpanel'); panels[index].setAttribute('aria-labelledby', link.id); panels[index].setAttribute('tabindex', '0'); // The heading stays in the document for the no-script case; the tab now names the panel. panels[index].querySelector('.tabs-heading')?.classList.add('visually-hidden'); }); const available = () => links.map((link, i) => getComputedStyle(link.parentElement).display !== 'none' ? i : -1).filter(i => i >= 0); let selected = -1; const select = (index, moveFocus = true) => { const visible = available(); if (!visible.includes(index)) index = visible[0] ?? -1; selected = index; links.forEach((link, i) => { const current = i === index; link.setAttribute('aria-selected', String(current)); link.tabIndex = current ? 0 : -1; panels[i].hidden = !current; }); if (moveFocus && index >= 0) links[index].focus(); }; list.addEventListener('click', event => { const index = links.indexOf(event.target.closest('a')); if (index === -1) return; // Keep the address bar clean: selecting a tab is not a navigation. event.preventDefault(); select(index); }); list.addEventListener('keydown', event => { const index = links.indexOf(event.target.closest('a')); if (index === -1) return; const visible = available(), position = visible.indexOf(index); if (!visible.length) return; const moves = { ArrowRight: position + 1, ArrowLeft: position - 1, Home: 0, End: visible.length - 1 }; if (!(event.key in moves)) return; event.preventDefault(); select(visible[(moves[event.key] + visible.length) % visible.length]); }); // A link from elsewhere may point straight at one panel. const deepLink = panels.findIndex(panel => panel.id === location.hash.slice(1)); select(deepLink === -1 ? 0 : deepLink, false); window.addEventListener('resize', () => select(selected, false)); group.classList.add('tabs-ready'); }; document.querySelectorAll('[data-tabs]').forEach(setup); })();