refactor(ui): allow routerLink/routerView in tabs, enabling browser navigation
This commit is contained in:
parent
0b4d5b3acd
commit
abf29eeffe
|
@ -1,24 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { type RouterLinkProps } from 'vue-router'
|
||||||
import { TABS_INJECTION_KEY } from '~/injection-keys'
|
import { TABS_INJECTION_KEY } from '~/injection-keys'
|
||||||
import { whenever } from '@vueuse/core'
|
import { whenever } from '@vueuse/core'
|
||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
|
|
||||||
const { title, icon } = defineProps<{ title:string, icon?:string }>()
|
export type Props = {
|
||||||
|
title: string,
|
||||||
|
to?: RouterLinkProps['to']
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
const { currentTab, tabs, icons } = inject(TABS_INJECTION_KEY, {
|
const props = defineProps<Props>()
|
||||||
currentTab: ref(title),
|
|
||||||
|
const { currentTitle, tabs } = inject(TABS_INJECTION_KEY, {
|
||||||
|
currentTitle: ref(props.title),
|
||||||
tabs: [],
|
tabs: [],
|
||||||
icons: [],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
whenever(() => !tabs.includes(title), () => {
|
whenever(() => !tabs.includes(props), () => {
|
||||||
tabs.push(title)
|
tabs.push(props)
|
||||||
icons.push(icon)
|
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="currentTab === title" class="tab-content">
|
<div v-if="currentTitle === title" class="tab-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TABS_INJECTION_KEY } from '~/injection-keys'
|
import { TABS_INJECTION_KEY } from '~/injection-keys'
|
||||||
import { provide, reactive, ref, watch } from 'vue'
|
import { computed, provide, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { type Props as TabProps } from '~/components/ui/Tab.vue'
|
||||||
|
|
||||||
import Button from "~/components/ui/Button.vue";
|
import Button from "~/components/ui/Button.vue";
|
||||||
|
import Link from "~/components/ui/Link.vue";
|
||||||
|
|
||||||
const currentTab = ref()
|
const currentTitle = ref<TabProps['title']>('')
|
||||||
const tabs = reactive([] as string[])
|
const tabs = reactive<TabProps[]>([])
|
||||||
const icons = reactive([] as string[])
|
|
||||||
|
|
||||||
provide(TABS_INJECTION_KEY, {
|
provide(TABS_INJECTION_KEY, {
|
||||||
currentTab,
|
currentTitle: currentTitle,
|
||||||
tabs,
|
tabs: tabs,
|
||||||
icons
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => tabs.length, (to, from) => {
|
const currentTab = computed(() =>
|
||||||
|
tabs.find(({title}) => title === currentTitle.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentIndex = computed(() =>
|
||||||
|
tabs.findIndex(({title}) => title === currentTitle.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(() => tabs.length, (_, from) => {
|
||||||
if (from === 0) {
|
if (from === 0) {
|
||||||
currentTab.value = tabs[0]
|
currentTitle.value = tabs[0].title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -24,16 +33,17 @@ watch(() => tabs.length, (to, from) => {
|
||||||
<template>
|
<template>
|
||||||
<div class="funkwhale tabs">
|
<div class="funkwhale tabs">
|
||||||
<div class="tabs-header">
|
<div class="tabs-header">
|
||||||
<Button
|
<component ghost :is="tab.to ? Link : Button"
|
||||||
v-for="(tab, index) in tabs" :key="tab" :class="{ 'is-active': currentTab === tab }"
|
v-for="(tab, _) in tabs" :class="{ 'is-active': currentTitle === tab.title }"
|
||||||
ghost
|
v-bind="tab"
|
||||||
:icon="icons[index]"
|
:onClick="() => { currentTitle = tab.title }"
|
||||||
@click="currentTab = tab"
|
@keydown.left="currentTitle = tabs[(currentIndex - 1 + tabs.length) % tabs.length]"
|
||||||
@keydown.left="currentTab = tabs[(tabs.findIndex(t => t === currentTab) - 1 + tabs.length) % tabs.length]"
|
@keydown.right="currentTitle = tabs[(currentIndex + 1) % tabs.length]"
|
||||||
@keydown.right="currentTab = tabs[(tabs.findIndex(t => t === currentTab) + 1) % tabs.length]" class="tabs-item">
|
class="tabs-item"
|
||||||
<div class="is-spacing">{{ tab }}</div>
|
>
|
||||||
<label>{{ tab }}</label>
|
<div class="is-spacing">{{ tab.title }}</div>
|
||||||
</Button>
|
<label>{{ tab.title }}</label>
|
||||||
|
</component>
|
||||||
|
|
||||||
<div class="tabs-right">
|
<div class="tabs-right">
|
||||||
<slot name="tabs-right" />
|
<slot name="tabs-right" />
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { InjectionKey, Ref } from "vue"
|
import { type InjectionKey, type Ref } from "vue"
|
||||||
|
import { type Props as TabProps } from '~/components/ui/Tab.vue'
|
||||||
|
|
||||||
export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{
|
export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{
|
||||||
tabs: string[]
|
tabs: TabProps[]
|
||||||
icons: (string|undefined)[]
|
currentTitle: Ref<TabProps['title']>
|
||||||
currentTab: Ref<string>
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export interface PopoverContext {
|
export interface PopoverContext {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
import Tabs from '~/components/ui/Tabs.vue'
|
import Tabs from '~/components/ui/Tabs.vue'
|
||||||
import Tab from '~/components/ui/Tab.vue'
|
import Tab from '~/components/ui/Tab.vue'
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import Tabs from "~/components/ui/Tabs.vue"
|
import Tabs from "~/components/ui/Tabs.vue";
|
||||||
```
|
```
|
||||||
|
|
||||||
# Tabs
|
# Tabs
|
||||||
|
@ -72,6 +76,12 @@ You can add a template to the right side of the tabs using the `#tabs-right` dir
|
||||||
<Tab title="Activity">Activity content</Tab>
|
<Tab title="Activity">Activity content</Tab>
|
||||||
|
|
||||||
<template #tabs-right>
|
<template #tabs-right>
|
||||||
<Input icon="bi-search" placeholder="Search" />
|
<Input v-model="search" icon="bi-search" placeholder="Search" />
|
||||||
</template>
|
</template>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Tabs and routes
|
||||||
|
|
||||||
|
If the tab content covers most of the page, users will expect that they can navigate to the previous tab with the "back" button of the browser ([See Navigation](./navigation.md)).
|
||||||
|
|
||||||
|
In this case, add the `:to` prop to each tab, and place a `RouterView` with the intended props of the route's component into its default slot.
|
||||||
|
|
Loading…
Reference in New Issue