278 lines
6.3 KiB
Vue
278 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
import { useStorage } from '@vueuse/core'
|
|
import { onMounted, ref, shallowRef, watch } from 'vue'
|
|
|
|
interface TabProps extends Record<string, unknown> {
|
|
id: string
|
|
}
|
|
const props = withDefaults(defineProps<{
|
|
id: string
|
|
tabId?: string
|
|
active?: number
|
|
data: TabProps[]
|
|
}>(), { active: 0, tabId: '' })
|
|
|
|
const TAB_STORE_NAME = 'VUEPRESS_TAB_STORE'
|
|
|
|
const tabStore = useStorage<Record<string, string>>(TAB_STORE_NAME, {})
|
|
|
|
// Index of current active item
|
|
const activeIndex = ref(props.active)
|
|
|
|
// Refs of the tab buttons
|
|
const tabRefs = shallowRef<HTMLUListElement[]>([])
|
|
|
|
// Update store
|
|
function updateStore(): void {
|
|
if (props.tabId)
|
|
tabStore.value[props.tabId] = props.data[activeIndex.value].id
|
|
}
|
|
|
|
// Activate next tab
|
|
function activateNext(index = activeIndex.value): void {
|
|
activeIndex.value = index < tabRefs.value.length - 1 ? index + 1 : 0
|
|
tabRefs.value[activeIndex.value].focus()
|
|
}
|
|
|
|
// Activate previous tab
|
|
function activatePrev(index = activeIndex.value): void {
|
|
activeIndex.value = index > 0 ? index - 1 : tabRefs.value.length - 1
|
|
tabRefs.value[activeIndex.value].focus()
|
|
}
|
|
|
|
// Handle keyboard event
|
|
function keyboardHandler(event: KeyboardEvent, index: number): void {
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
event.preventDefault()
|
|
activeIndex.value = index
|
|
}
|
|
else if (event.key === 'ArrowRight') {
|
|
event.preventDefault()
|
|
activateNext()
|
|
}
|
|
else if (event.key === 'ArrowLeft') {
|
|
event.preventDefault()
|
|
activatePrev()
|
|
}
|
|
|
|
updateStore()
|
|
}
|
|
|
|
function getInitialIndex(): number {
|
|
if (props.tabId) {
|
|
const valueIndex = props.data.findIndex(
|
|
({ id }) => tabStore.value[props.tabId] === id,
|
|
)
|
|
|
|
if (valueIndex !== -1)
|
|
return valueIndex
|
|
}
|
|
|
|
return props.active
|
|
}
|
|
|
|
onMounted(() => {
|
|
activeIndex.value = getInitialIndex()
|
|
|
|
watch(
|
|
() => tabStore.value[props.tabId],
|
|
(newValue, oldValue) => {
|
|
if (props.tabId && newValue !== oldValue) {
|
|
const index = props.data.findIndex(({ id }) => id === newValue)
|
|
|
|
if (index !== -1)
|
|
activeIndex.value = index
|
|
}
|
|
},
|
|
)
|
|
})
|
|
|
|
function onTabNavClick(index: number): void {
|
|
activeIndex.value = index
|
|
updateStore()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="data.length" class="vp-tabs">
|
|
<div class="vp-tabs-nav" role="tablist">
|
|
<button
|
|
v-for="(item, index) in data"
|
|
:key="index"
|
|
:ref="(el) => el && (tabRefs[index] = el as HTMLUListElement)"
|
|
class="vp-tab-nav" :class="{ active: index === activeIndex }"
|
|
type="button" role="tab"
|
|
:aria-controls="`tab-${id}-${index}`"
|
|
:aria-selected="index === activeIndex"
|
|
@click="() => onTabNavClick(index)"
|
|
@keydown="(e) => keyboardHandler(e, index)"
|
|
>
|
|
<slot :name="`title${index}`" :value="item.id" :is-active="index === activeIndex" />
|
|
</button>
|
|
</div>
|
|
<div
|
|
v-for="(item, index) in data"
|
|
:id="`tab-${id}-${index}`" :key="index"
|
|
class="vp-tab" :class="{ active: index === activeIndex }"
|
|
role="tabpanel" :aria-expanded="index === activeIndex"
|
|
>
|
|
<div class="vp-tab-title">
|
|
<slot :name="`title${index}`" :value="item.id" :is-active="index === activeIndex" />
|
|
</div>
|
|
<slot :name="`tab${index}`" :value="item.id" :is-active="index === activeIndex" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
.vp-tabs {
|
|
margin: 16px 0;
|
|
overflow: hidden;
|
|
border: 1px solid var(--vp-c-divider);
|
|
border-radius: 6px;
|
|
transition: border var(--vp-t-color);
|
|
}
|
|
|
|
@media (max-width: 419px) {
|
|
.vp-tabs {
|
|
margin: 16px -24px;
|
|
border: none;
|
|
border-bottom: 1px solid var(--vp-c-divider);
|
|
border-radius: 0;
|
|
}
|
|
}
|
|
|
|
.vp-doc .vp-tabs-nav {
|
|
padding: 0 12px;
|
|
overflow-x: auto;
|
|
white-space: nowrap;
|
|
background-color: var(--vp-code-tab-bg);
|
|
box-shadow: inset 0 -1px var(--vp-code-tab-divider);
|
|
transition: background-color var(--vp-t-color), box-shadow var(--vp-t-color);
|
|
}
|
|
|
|
@media print {
|
|
.vp-doc .vp-tabs-nav {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.vp-doc .vp-tab-nav {
|
|
position: relative;
|
|
padding: 0 12px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
line-height: 48px;
|
|
color: var(--vp-code-tab-text-color);
|
|
white-space: nowrap;
|
|
border-bottom: 1px solid transparent;
|
|
transition: color var(--vp-t-color);
|
|
}
|
|
|
|
.vp-doc .vp-tab-nav:hover {
|
|
color: var(--vp-code-tab-text-hover-color);
|
|
}
|
|
|
|
.vp-doc .vp-tab-nav::after {
|
|
position: absolute;
|
|
right: 8px;
|
|
bottom: -1px;
|
|
left: 8px;
|
|
z-index: 1;
|
|
display: block;
|
|
width: auto;
|
|
height: 2px;
|
|
content: "";
|
|
background: transparent;
|
|
border-radius: 2px;
|
|
transition: background var(--vp-t-color);
|
|
}
|
|
|
|
.vp-doc .vp-tab-nav.active {
|
|
color: var(--vp-code-tab-active-text-color);
|
|
background: transparent;
|
|
}
|
|
|
|
.vp-doc .vp-tab-nav.active::after {
|
|
background: var(--vp-code-tab-active-bar-color);
|
|
}
|
|
|
|
.vp-doc .vp-tab {
|
|
display: none;
|
|
padding: 16px;
|
|
}
|
|
|
|
.vp-doc .vp-tab > :nth-child(2) {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.vp-doc .vp-tab > :last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.vp-doc .vp-tab.active {
|
|
display: block;
|
|
}
|
|
|
|
.vp-doc .vp-tab-title {
|
|
display: none;
|
|
padding: 4px;
|
|
font-weight: 500;
|
|
color: var(--vp-code-tab-text-color);
|
|
border-top: 1px solid var(--vp-c-divider);
|
|
transition: color var(--vp-t-color);
|
|
}
|
|
|
|
.vp-doc .vp-tab:nth-child(n+2) .vp-tab-title {
|
|
border-top: none;
|
|
}
|
|
|
|
@media print {
|
|
.vp-doc .vp-tab-title {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.vp-doc .hint-container .vp-tabs {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
@media (max-width: 419px) {
|
|
.vp-doc .hint-container .vp-tabs {
|
|
margin: 8px -16px;
|
|
}
|
|
}
|
|
|
|
.vp-doc .hint-container .vp-tab-nav {
|
|
line-height: 40px;
|
|
}
|
|
|
|
.vp-doc .hint-container.info .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-info-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.note .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-note-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.tip .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-tip-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.warning .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-warning-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.danger .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-danger-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.caution .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-caution-code-bg);
|
|
}
|
|
|
|
.vp-doc .hint-container.important .vp-tabs .vp-tabs-nav {
|
|
--vp-code-tab-bg: var(--vp-custom-block-important-code-bg);
|
|
}
|
|
</style>
|