Newer
Older
vmk-ui-kit / src / vue / components / GnTabs.js
/**
 * GnTabs - Accessible tab interface for Vue.
 *
 * @slots default - GnTab components
 * @slots panels - GnTabPanel components (optional)
 * @emits change
 */

import { defineComponent, h, provide, ref, computed, onMounted, watch } from "vue";
import { cx, normalizeVariant } from "../utils.js";

export const tabsKey = Symbol("vmk-ui-kit-tabs");

export default defineComponent({
	name: "GnTabs",
	props: {
		modelValue: { type: [String, Number], default: null },
		variant: { type: String, default: "default" },
		vertical: { type: Boolean, default: false }
	},
	emits: ["update:modelValue", "change"],
	setup(props, { emit, slots }) {
		const activeKey = ref(props.modelValue);
		const tablistRef = ref(null);
		let tabItems = [];
		let panelItems = [];

		const registerTab = (key, label) => {
			tabItems.push({ key, label });
		};

		const registerPanel = key => {
			panelItems.push({ key });
		};

		const select = key => {
			activeKey.value = key;
			emit("update:modelValue", key);
			emit("change", key);
		};

		const isActive = key => activeKey.value === key;

		const classes = computed(() => cx("tabs", {
			"tabs-compact": props.variant === "compact",
			"tabs-vertical": props.vertical
		}));

		const focusTab = direction => {
			const enabled = tabItems.filter((_, i) => {
				const el = tablistRef.value?.children[i];
				return el && !el.disabled && el.getAttribute("aria-disabled") !== "true";
			});
			const current = enabled.findIndex(item => item.key === activeKey.value);
			const nextIndex = (current + direction + enabled.length) % enabled.length;
			const next = enabled[nextIndex];
			if (next) select(next.key);
		};

		watch(() => props.modelValue, value => {
			activeKey.value = value;
		});

		provide(tabsKey, {
			activeKey,
			registerTab,
			registerPanel,
			select,
			isActive
		});

		return () => h("div", { class: classes.value }, [
			h("div", {
				ref: tablistRef,
				class: "tabs-list",
				role: "tablist",
				ariaOrientation: props.vertical ? "vertical" : "horizontal",
				onKeydown: event => {
					if (event.key === "ArrowRight" || event.key === "ArrowDown") {
						event.preventDefault();
						focusTab(1);
					} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
						event.preventDefault();
						focusTab(-1);
					} else if (event.key === "Home") {
						event.preventDefault();
						focusTab(1 - tabItems.length);
					} else if (event.key === "End") {
						event.preventDefault();
						focusTab(-1);
					}
				}
			}, slots.default?.()),
			slots.panels && h("div", { class: "tabs-panels" }, slots.panels())
		]);
	}
});