mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-05 06:38:00 +01:00
feat(Tabs): control selected index (#490)
This commit is contained in:
22
docs/components/content/examples/TabsExampleChange.vue
Normal file
22
docs/components/content/examples/TabsExampleChange.vue
Normal 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>
|
||||||
34
docs/components/content/examples/TabsExampleVModel.vue
Normal file
34
docs/components/content/examples/TabsExampleVModel.vue
Normal 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>
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user