Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Tabs): control selected index #490

Merged
merged 1 commit into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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