23 lines
2.4 KiB
Vue
23 lines
2.4 KiB
Vue
<script setup lang="ts">
|
|
import { nextTick, ref } from 'vue'
|
|
export interface KbxTabItem{key:string;label:string;disabled?:boolean;badge?:string|number}
|
|
const props=defineProps<{modelValue:string;items:KbxTabItem[];ariaLabel?:string}>()
|
|
const emit=defineEmits<{ 'update:modelValue':[string] }>()
|
|
const tablist=ref<HTMLElement|null>(null)
|
|
function enabledItems(){return props.items.filter(x=>!x.disabled)}
|
|
function activate(key:string){emit('update:modelValue',key);nextTick(()=>focusKey(key))}
|
|
function focusKey(key:string){tablist.value?.querySelector<HTMLElement>(`[data-tab-key="${CSS.escape(key)}"]`)?.focus()}
|
|
function move(event:KeyboardEvent,key:string){
|
|
const items=enabledItems();const index=items.findIndex(x=>x.key===key);if(index<0)return
|
|
let target=index
|
|
if(event.key==='ArrowRight')target=(index+1)%items.length
|
|
else if(event.key==='ArrowLeft')target=(index-1+items.length)%items.length
|
|
else if(event.key==='Home')target=0
|
|
else if(event.key==='End')target=items.length-1
|
|
else return
|
|
event.preventDefault();activate(items[target].key)
|
|
}
|
|
</script>
|
|
<template><div class="kbx-tabs"><nav ref="tablist" role="tablist" :aria-label="ariaLabel??'탭'"><button v-for="item in items" :id="`kbx-tab-${item.key}`" :key="item.key" type="button" role="tab" :data-tab-key="item.key" :aria-selected="modelValue===item.key" :aria-controls="`kbx-panel-${item.key}`" :tabindex="modelValue===item.key?0:-1" :disabled="item.disabled" @click="activate(item.key)" @keydown="move($event,item.key)"><span>{{item.label}}</span><b v-if="item.badge!=null">{{item.badge}}</b></button></nav><div :id="`kbx-panel-${modelValue}`" class="kbx-tabs__panel" role="tabpanel" :aria-labelledby="`kbx-tab-${modelValue}`"><slot :active-key="modelValue"/></div></div></template>
|
|
<style scoped>.kbx-tabs>nav{display:flex;align-items:end;gap:var(--kbx-border-width);min-height:var(--kbx-control-lg);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-tabs>nav button{height:var(--kbx-control-md);border:var(--kbx-border-width) solid transparent;border-bottom:0;background:transparent;padding:0 var(--kbx-space-3);color:var(--kbx-color-text-muted)}.kbx-tabs>nav button[aria-selected="true"]{border-color:var(--kbx-color-border);background:var(--kbx-color-surface);color:var(--kbx-color-text);font-weight:600}.kbx-tabs>nav b{margin-left:var(--kbx-space-1);font-size:var(--kbx-font-xs);color:var(--kbx-color-primary)}.kbx-tabs__panel{padding-top:var(--kbx-space-2)}</style>
|