Newer
Older
gnexus-ui-kit / src / js / components / tabs.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 16 hours ago 3 KB Add tabs component and refresh modal/logo UI
const initializedRoots = new WeakSet();

function getTabs(root) {
	return [...root.querySelectorAll('[role="tab"], .tab')];
}

function getPanels(root) {
	return [...root.querySelectorAll('[role="tabpanel"], .tab-panel')];
}

function getPanel(root, tab) {
	const panelId = tab.getAttribute("aria-controls");

	if(!panelId) {
		return null;
	}

	return root.querySelector(`#${CSS.escape(panelId)}`);
}

function setTabState(tab, isActive) {
	tab.classList.toggle("tab-active", isActive);
	tab.setAttribute("aria-selected", isActive ? "true" : "false");
	tab.setAttribute("tabindex", isActive ? "0" : "-1");
}

function setPanelState(panel, isActive) {
	panel.classList.toggle("tab-panel-active", isActive);
	panel.toggleAttribute("hidden", !isActive);
}

function activate(tab, options = {}) {
	if(!tab || tab.disabled || tab.getAttribute("aria-disabled") === "true") {
		return;
	}

	const root = tab.closest(".tabs") || tab.closest('[role="tablist"]')?.parentElement;

	if(!root) {
		return;
	}

	getTabs(root).forEach(item => setTabState(item, item === tab));
	getPanels(root).forEach(panel => setPanelState(panel, false));

	const panel = getPanel(root, tab);

	if(panel) {
		setPanelState(panel, true);
	}

	if(options.focus !== false) {
		tab.focus();
	}
}

function getNextEnabledTab(tabs, activeIndex, direction) {
	for(let offset = 1; offset <= tabs.length; offset++) {
		const index = (activeIndex + (offset * direction) + tabs.length) % tabs.length;
		const tab = tabs[index];

		if(!tab.disabled && tab.getAttribute("aria-disabled") !== "true") {
			return tab;
		}
	}

	return tabs[activeIndex];
}

function handleKeydown(event) {
	const tab = event.target.closest('[role="tab"], .tab');

	if(!tab) {
		return;
	}

	const root = tab.closest(".tabs") || tab.closest('[role="tablist"]')?.parentElement;
	const tabs = root ? getTabs(root) : [];
	const activeIndex = tabs.indexOf(tab);

	if(activeIndex < 0) {
		return;
	}

	let nextTab = null;

	if(event.key === "ArrowRight" || event.key === "ArrowDown") {
		nextTab = getNextEnabledTab(tabs, activeIndex, 1);
	} else if(event.key === "ArrowLeft" || event.key === "ArrowUp") {
		nextTab = getNextEnabledTab(tabs, activeIndex, -1);
	} else if(event.key === "Home") {
		nextTab = getNextEnabledTab(tabs, -1, 1);
	} else if(event.key === "End") {
		nextTab = getNextEnabledTab(tabs, 0, -1);
	}

	if(!nextTab) {
		return;
	}

	event.preventDefault();
	activate(nextTab);
}

function prepare(root) {
	const tabs = getTabs(root);
	const activeTab = tabs.find(tab => tab.classList.contains("tab-active") || tab.getAttribute("aria-selected") === "true")
		|| tabs.find(tab => !tab.disabled && tab.getAttribute("aria-disabled") !== "true");

	tabs.forEach(tab => {
		tab.setAttribute("role", "tab");
		setTabState(tab, tab === activeTab);
	});

	root.querySelectorAll(".tabs-list").forEach(list => {
		list.setAttribute("role", "tablist");
	});

	getPanels(root).forEach(panel => {
		panel.setAttribute("role", "tabpanel");
		setPanelState(panel, activeTab ? panel === getPanel(root, activeTab) : panel.classList.contains("tab-panel-active"));
	});
}

function init(root = document) {
	if(initializedRoots.has(root)) {
		return;
	}

	root.querySelectorAll(".tabs").forEach(prepare);

	root.addEventListener("click", event => {
		const tab = event.target.closest('[role="tab"], .tab');

		if(!tab || !root.contains(tab)) {
			return;
		}

		event.preventDefault();
		activate(tab, { focus: false });
	});

	root.addEventListener("keydown", handleKeydown);

	initializedRoots.add(root);
}

export default {
	init,
	activate
};