Files
kantarci 37c2942d1d
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Add device visibility and responsive navigation for 1.10.0
2026-09-14 14:46:55 +02:00

71 lines
3.0 KiB
JavaScript

/**
* 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);
})();