You've already forked AstralRinth
98 lines
2.5 KiB
Vue
98 lines
2.5 KiB
Vue
<template>
|
|
<div
|
|
v-if="tabs.length > 0"
|
|
class="inline-flex w-fit items-center overflow-x-auto rounded-xl border border-solid border-surface-5 p-0.5 shadow-sm gap-1 h-[38px]"
|
|
role="tablist"
|
|
>
|
|
<button
|
|
v-for="(tab, index) in tabs"
|
|
:key="tab.value"
|
|
ref="tabButtons"
|
|
type="button"
|
|
class="flex min-h-6 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-[10px] border border-solid px-2.5 h-full text-sm font-medium outline-none transition-all active:scale-[0.97] focus-visible:ring-4 focus-visible:ring-brand-shadow"
|
|
:class="
|
|
tab.value === value
|
|
? 'border-green bg-highlight-green text-green'
|
|
: 'border-transparent bg-transparent text-primary hover:bg-surface-4'
|
|
"
|
|
role="tab"
|
|
:aria-selected="tab.value === value"
|
|
:tabindex="tab.value === value || (!hasSelectedTab && index === 0) ? 0 : -1"
|
|
@click="selectTab(tab)"
|
|
@keydown="onTabKeydown($event, index)"
|
|
>
|
|
<component
|
|
:is="tab.icon"
|
|
v-if="tab.icon"
|
|
class="size-5 shrink-0"
|
|
:class="tab.value === value ? 'text-green' : 'text-secondary'"
|
|
/>
|
|
<span v-if="tab.label" class="text-nowrap">{{ tab.label }}</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Component } from 'vue'
|
|
import { computed, ref } from 'vue'
|
|
|
|
export type TabsValue = string | number
|
|
|
|
export interface TabsTab {
|
|
value: TabsValue
|
|
label: string
|
|
icon?: Component
|
|
}
|
|
|
|
const props = defineProps<{
|
|
value: TabsValue
|
|
tabs: TabsTab[]
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:value': [value: TabsValue]
|
|
change: [tab: TabsTab]
|
|
}>()
|
|
|
|
const tabButtons = ref<HTMLButtonElement[]>()
|
|
|
|
const hasSelectedTab = computed(() => props.tabs.some((tab) => tab.value === props.value))
|
|
|
|
function selectTab(tab: TabsTab) {
|
|
emit('update:value', tab.value)
|
|
emit('change', tab)
|
|
}
|
|
|
|
function selectTabAtIndex(index: number) {
|
|
const tab = props.tabs[index]
|
|
if (!tab) return
|
|
|
|
selectTab(tab)
|
|
requestAnimationFrame(() => {
|
|
tabButtons.value?.[index]?.focus()
|
|
})
|
|
}
|
|
|
|
function onTabKeydown(event: KeyboardEvent, index: number) {
|
|
if (props.tabs.length === 0) return
|
|
|
|
const lastIndex = props.tabs.length - 1
|
|
let nextIndex: number | undefined
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
nextIndex = index === lastIndex ? 0 : index + 1
|
|
} else if (event.key === 'ArrowLeft') {
|
|
nextIndex = index === 0 ? lastIndex : index - 1
|
|
} else if (event.key === 'Home') {
|
|
nextIndex = 0
|
|
} else if (event.key === 'End') {
|
|
nextIndex = lastIndex
|
|
}
|
|
|
|
if (nextIndex === undefined) return
|
|
|
|
event.preventDefault()
|
|
selectTabAtIndex(nextIndex)
|
|
}
|
|
</script>
|