Skip to content

Commit

Permalink
feat(Tabs): control selected index (#490)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamincanac committed Aug 4, 2023
1 parent 5c0b0cb commit 409f20a
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 5 deletions.
22 changes: 22 additions & 0 deletions docs/components/content/examples/TabsExampleChange.vue
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions docs/components/content/examples/TabsExampleVModel.vue
Original file line number Diff line number Diff line change
@@ -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>
72 changes: 72 additions & 0 deletions docs/content/5.navigation/4.tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

You can use slots to customize the buttons and items content of the Accordion.
Expand Down
42 changes: 37 additions & 5 deletions src/runtime/components/navigation/Tabs.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<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
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']"
:style="[orientation === 'horizontal' && `grid-template-columns: repeat(${items.length}, minmax(0, 1fr))`]"
>
Expand Down Expand Up @@ -40,9 +41,10 @@
</template>

<script lang="ts">
import { ref, computed, onMounted, defineComponent } from 'vue'
import { ref, computed, watch, onMounted, defineComponent } 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 { useResizeObserver } from '@vueuse/core'
import { defu } from 'defu'
import type { TabItem } from '../../types/tabs'
import { useAppConfig } from '#imports'
Expand All @@ -61,6 +63,10 @@ export default defineComponent({
HTabPanel
},
props: {
modelValue: {
type: Number,
default: undefined
},
orientation: {
type: String as PropType<'horizontal' | 'vertical'>,
default: 'horizontal',
Expand All @@ -79,18 +85,22 @@ export default defineComponent({
default: () => appConfig.ui.tabs
}
},
setup (props) {
emits: ['update:modelValue', 'change'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs))
const listRef = ref<HTMLElement>()
const itemRefs = ref<HTMLElement[]>([])
const markerRef = ref<HTMLElement>()
const selectedIndex = ref(props.modelValue || props.defaultIndex)
// Methods
function onChange (index) {
function calcMarkerSize (index: number) {
// @ts-ignore
const tab = itemRefs.value[index]?.$el
if (!tab) {
Expand All @@ -103,13 +113,35 @@ export default defineComponent({
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 {
// eslint-disable-next-line vue/no-dupe-keys
ui,
listRef,
itemRefs,
markerRef,
selectedIndex,
onChange
}
}
Expand Down

1 comment on commit 409f20a

@vercel
Copy link

@vercel vercel bot commented on 409f20a Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./

ui.nuxtlabs.com
ui-nuxtlabs.vercel.app
ui-git-dev-nuxtlabs.vercel.app

Please sign in to comment.