/**
* GnTabs - Accessible tab switcher with keyboard navigation.
*
* Supports two modes:
* - `items` mode (reference API): pass an array of { id, label, icon?, disabled? }
* and use named slots for panels.
* - child mode: place GnTab / GnTabPanel children directly.
*
* @emits update:modelValue, change
*/
import { defineComponent, h, provide, ref, computed, onMounted, watch } from "vue";
import { cx, iconNode } from "../utils.js";
export const tabsKey = Symbol("vmk-ui-kit-tabs");
export default defineComponent({
name: "GnTabs",
props: {
modelValue: { type: [String, Number], default: null },
items: { type: Array, default: null },
variant: { type: String, default: "default" },
compact: { type: Boolean, default: false },
vertical: { type: Boolean, default: false },
ariaLabel: { type: String, default: "Tabs" }
},
emits: ["update:modelValue", "change"],
setup(props, { emit, slots }) {
const activeKey = ref(props.modelValue);
const tablistRef = ref(null);
let tabItems = [];
let panelItems = [];
const hasItems = computed(() => Array.isArray(props.items) && props.items.length > 0);
const activeId = computed(() => {
if (!hasItems.value) return activeKey.value;
return props.modelValue || props.items.find(item => !item.disabled)?.id || props.items[0]?.id;
});
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.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
});
// Items mode: matches the gnexus-ui-kit reference API.
const renderItemsMode = () => {
const enabledItems = () => props.items.filter(item => !item.disabled);
const activate = item => {
if (!item.disabled) {
emit("update:modelValue", item.id);
emit("change", item.id);
}
};
const move = (item, direction) => {
const items = enabledItems();
const index = items.findIndex(enabled => enabled.id === item.id);
const next = items[(index + direction + items.length) % items.length];
activate(next);
};
const handleKeydown = (event, item) => {
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
event.preventDefault();
move(item, 1);
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
event.preventDefault();
move(item, -1);
} else if (event.key === "Home") {
event.preventDefault();
activate(enabledItems()[0]);
} else if (event.key === "End") {
event.preventDefault();
const items = enabledItems();
activate(items[items.length - 1]);
}
};
return h("div", { class: classes.value }, [
h("div", {
class: "tabs-list",
role: "tablist",
"aria-label": props.ariaLabel
}, props.items.map(item => {
const active = item.id === activeId.value;
const panelId = `${item.id}-panel`;
return h("button", {
class: cx("tab", { "tab-active": active }),
type: "button",
role: "tab",
"aria-selected": active ? "true" : "false",
"aria-controls": panelId,
"aria-disabled": item.disabled ? "true" : undefined,
tabindex: active ? "0" : "-1",
onClick: () => activate(item),
onKeydown: event => handleKeydown(event, item)
}, [
iconNode(item.icon),
item.label
]);
})),
h("div", { class: "tabs-panels" }, props.items.map(item => {
const active = item.id === activeId.value;
return h("div", {
id: `${item.id}-panel`,
class: cx("tab-panel", { "tab-panel-active": active }),
role: "tabpanel",
hidden: !active
}, slots[item.id]?.({ item, active }) || (active && slots.default?.({ item, active })));
}))
]);
};
// Child mode: GnTab / GnTabPanel children register themselves via provide.
const renderChildMode = () => h("div", { class: classes.value }, [
h("div", {
ref: tablistRef,
class: "tabs-list",
role: "tablist",
ariaOrientation: props.vertical ? "vertical" : "horizontal",
"aria-label": props.ariaLabel,
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())
]);
return () => hasItems.value ? renderItemsMode() : renderChildMode();
}
});