feat(Tabs): control selected index (#490)

This commit is contained in:
Benjamin Canac
2023-08-04 10:16:36 +02:00
parent ad0fe230ba
commit aaf09ad555
4 changed files with 165 additions and 5 deletions

View File

@@ -0,0 +1,22 @@
<script setup>
const items = [{
label: 'Tab1',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
content: 'Finally, this is the content for Tab3'
}]
function onChange (index) {
const item = items[index]
alert(`${item.label} was clicked!`)
}
</script>
<template>
<UTabs :items="items" @change="onChange" />
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
const items = [{
label: 'Tab1',
content: 'This is the content shown for Tab1'
}, {
label: 'Tab2',
content: 'And, this is the content for Tab2'
}, {
label: 'Tab3',
content: 'Finally, this is the content for Tab3'
}]
const route = useRoute()
const router = useRouter()
const selected = computed({
get () {
const index = items.findIndex((item) => item.label === route.query.tab)
if (index === -1) {
return 0
}
return index
},
set (value) {
// Hash is specified here to prevent the page from scrolling to the top
router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
}
})
</script>
<template>
<UTabs v-model="selected" :items="items" />
</template>

View File

@@ -83,6 +83,78 @@ const items = [...]
``` ```
:: ::
::callout{icon="i-heroicons-exclamation-triangle"}
This will have no effect if you are using a `v-model` to control the selected index.
::
### Listen to changes :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
You can listen to changes by using the `@change` event. The event will emit the index of the selected item.
::component-example
#default
:tabs-example-change{class="w-full"}
#code
```vue
<script setup>
const items = [...]
function onChange (index) {
const item = items[index]
alert(`${item.label} was clicked!`)
}
</script>
<template>
<UTabs :items="items" @change="onChange" />
</template>
```
::
### Control the selected index :u-badge{label="Edge" class="ml-2 align-text-bottom !rounded-full" variant="subtle"}
Use a `v-model` to control the selected index.
::component-example
#default
:tabs-example-v-model{class="w-full"}
#code
```vue
<script setup>
const items = [...]
const route = useRoute()
const router = useRouter()
const selected = computed({
get () {
const index = items.findIndex((item) => item.label === route.query.tab)
if (index === -1) {
return 0
}
return index
},
set (value) {
// Hash is specified here to prevent the page from scrolling to the top
router.replace({ query: { tab: items[value].label }, hash: '#control-the-selected-index' })
}
})
</script>
<template>
<UTabs v-model="selected" :items="items" />
</template>
```
::
::callout{icon="i-heroicons-information-circle"}
In this example, we are binding tabs to the route query. Refresh the page to see the selected tab change.
::
## Slots ## Slots
You can use slots to customize the buttons and items content of the Accordion. You can use slots to customize the buttons and items content of the Accordion.

View File

@@ -1,6 +1,7 @@
<template> <template>
<HTabGroup :vertical="orientation === 'vertical'" :default-index="defaultIndex" as="div" :class="ui.wrapper" @change="onChange"> <HTabGroup :vertical="orientation === 'vertical'" :selected-index="selectedIndex" as="div" :class="ui.wrapper" @change="onChange">
<HTabList <HTabList
ref="listRef"
:class="[ui.list.base, ui.list.background, ui.list.rounded, ui.list.shadow, ui.list.padding, ui.list.width, orientation === 'horizontal' && ui.list.height, orientation === 'horizontal' && 'inline-grid items-center']" :class="[ui.list.base, ui.list.background, ui.list.rounded, ui.list.shadow, ui.list.padding, ui.list.width, orientation === 'horizontal' && ui.list.height, orientation === 'horizontal' && 'inline-grid items-center']"
:style="[orientation === 'horizontal' && `grid-template-columns: repeat(${items.length}, minmax(0, 1fr))`]" :style="[orientation === 'horizontal' && `grid-template-columns: repeat(${items.length}, minmax(0, 1fr))`]"
> >
@@ -40,9 +41,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, computed, onMounted, defineComponent } from 'vue' import { ref, computed, watch, onMounted, defineComponent } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue' import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
import { useResizeObserver } from '@vueuse/core'
import { defu } from 'defu' import { defu } from 'defu'
import type { TabItem } from '../../types/tabs' import type { TabItem } from '../../types/tabs'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
@@ -61,6 +63,10 @@ export default defineComponent({
HTabPanel HTabPanel
}, },
props: { props: {
modelValue: {
type: Number,
default: undefined
},
orientation: { orientation: {
type: String as PropType<'horizontal' | 'vertical'>, type: String as PropType<'horizontal' | 'vertical'>,
default: 'horizontal', default: 'horizontal',
@@ -79,18 +85,22 @@ export default defineComponent({
default: () => appConfig.ui.tabs default: () => appConfig.ui.tabs
} }
}, },
setup (props) { emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
// TODO: Remove // TODO: Remove
const appConfig = useAppConfig() const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs)) const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs))
const listRef = ref<HTMLElement>()
const itemRefs = ref<HTMLElement[]>([]) const itemRefs = ref<HTMLElement[]>([])
const markerRef = ref<HTMLElement>() const markerRef = ref<HTMLElement>()
const selectedIndex = ref(props.modelValue || props.defaultIndex)
// Methods // Methods
function onChange (index) { function calcMarkerSize (index: number) {
// @ts-ignore // @ts-ignore
const tab = itemRefs.value[index]?.$el const tab = itemRefs.value[index]?.$el
if (!tab) { if (!tab) {
@@ -103,13 +113,35 @@ export default defineComponent({
markerRef.value.style.height = `${tab.offsetHeight}px` markerRef.value.style.height = `${tab.offsetHeight}px`
} }
onMounted(() => onChange(props.defaultIndex)) function onChange (index) {
selectedIndex.value = index
emit('change', index)
if (props.modelValue !== undefined) {
emit('update:modelValue', index)
}
calcMarkerSize(index)
}
useResizeObserver(listRef, () => {
calcMarkerSize(selectedIndex.value)
})
watch(() => props.modelValue, (value) => {
selectedIndex.value = value
})
onMounted(() => calcMarkerSize(selectedIndex.value))
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
listRef,
itemRefs, itemRefs,
markerRef, markerRef,
selectedIndex,
onChange onChange
} }
} }