Skip to content

Commit

Permalink
Merge pull request #31 from qianmoQ/feature-components
Browse files Browse the repository at this point in the history
[Tabs] Support tabs
  • Loading branch information
qianmoQ committed Aug 30, 2024
2 parents 235acee + f469efd commit aadc80c
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 14 deletions.
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@radix-icons/vue": "^1.0.0",
"@vee-validate/zod": "^4.13.2",
"@vueuse/core": "^10.11.0",
"@vueuse/core": "^10.11.1",
"@vueuse/motion": "^2.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ yes | npx shadcn-vue@latest add table
yes | npx shadcn-vue@latest add textarea
yes | npx shadcn-vue@latest add tooltip
yes | npx shadcn-vue@latest add context-menu
yes | npx shadcn-vue@latest add tabs

echo "========== Done setup script =========="
3 changes: 2 additions & 1 deletion src/data/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const createNavigation = (): void => {
const card = createNavigationItem('common.common.card', undefined, '/components/card', undefined, NavigationPosition.LEFT_TOP)
const tree = createNavigationItem('common.common.tree', undefined, '/components/tree', undefined, NavigationPosition.LEFT_TOP)
const timeline = createNavigationItem('common.common.timeline', undefined, '/components/timeline', undefined, NavigationPosition.LEFT_TOP)
const componentArray = [button, card, tree, timeline]
const tab = createNavigationItem('common.common.tab', undefined, '/components/tab', undefined, NavigationPosition.LEFT_TOP)
const componentArray = [button, card, tree, timeline, tab]
const components = createNavigationItem('common.common.component', componentArray.length.toString(), '/components', Command, NavigationPosition.LEFT_TOP, componentArray)
NavigationService.addNavigation(components)

Expand Down
1 change: 1 addition & 0 deletions src/i18n/langs/en/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default {
navbar: 'Navbar',
tree: 'Tree',
timeline: 'Timeline',
tab: 'Tab'
},
tip: {
signInInfo: 'Enter your information to sign in to your account.',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/langs/zhCn/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default {
navbar: '导航栏',
tree: '树',
timeline: '时间线',
tab: '选项卡'
},
tip: {
signInInfo: '输入您的信息以登录您的帐户。',
Expand Down
5 changes: 5 additions & 0 deletions src/router/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ const createComponentRouter = (router: Router): void => {
name: 'timeline',
path: 'timeline',
component: () => import('@/views/pages/components/TimelineHome.vue')
},
{
name: 'tab',
path: 'tab',
component: () => import('@/views/pages/components/TabHome.vue')
}
]
})
Expand Down
45 changes: 45 additions & 0 deletions src/ui/tab/content.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<div v-if="isActive" class="flex-1">
<TabsContent :value="value">
<slot/>
</TabsContent>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, inject } from 'vue'
import { TabsContent } from '@/components/ui/tabs'
interface TabsContext
{
selectedValue: { value: string }
orientation: 'horizontal' | 'vertical'
setSelectedValue: (val: string) => void
}
export default defineComponent({
name: 'ITabContent',
components: { TabsContent },
props: {
value: {
type: String,
required: true
}
},
setup(props)
{
// Add types to inject and provide default values to avoid type errors
const tabsContext = inject<TabsContext>('tabsContext', {
selectedValue: { value: '' },
orientation: 'horizontal',
setSelectedValue: () => {
}
})
// Make sure tabsContext exists and compare selectedValue
const isActive = computed(() => tabsContext.selectedValue.value === props.value)
return { isActive }
}
})
</script>
93 changes: 93 additions & 0 deletions src/ui/tab/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<template>
<Tabs :default-value="value">
<div :class="cn(
isVertical(orientation)
? 'flex'
: 'flex flex-col space-y-2'
)">
<TabsList :class="cn(
isVertical(orientation)
? 'flex flex-col h-full py-2 space-y-2' // Vertical: column layout
: 'flex flex-row space-x-2 w-fit' // Horizontal: row layout
)">
<slot name="default"/>
</TabsList>

<div :class="cn(
isVertical(orientation)
? 'flex-1 ml-4' // Vertical mode is on the right
: 'flex-1 mt-2' // Horizontal mode is below
)">
<slot name="content"/>
</div>
</div>
</Tabs>
</template>

<script lang="ts">
import { defineComponent, onMounted, PropType, provide, ref } from 'vue'
import { Tabs, TabsList } from '@/components/ui/tabs'
import { cn } from '@/lib/utils.ts'
import { isVertical } from '@/ui/tab/utils.ts'
export default defineComponent({
name: 'ITabs',
components: { Tabs, TabsList },
props: {
value: {
type: String,
required: true
},
orientation: {
type: String as PropType<'horizontal' | 'vertical'>,
default: 'vertical',
validator: (value: string) => ['horizontal', 'vertical'].includes(value)
}
},
setup(props, { emit, slots })
{
const selectedValue = ref(props.value)
// Provide orientation and selected value by providing
provide('tabsContext', {
selectedValue,
orientation: props.orientation,
setSelectedValue: (val: string) => {
selectedValue.value = val
emit('update:value', val)
}
})
// Slot type checking function
const validateSlots = () => {
if (slots.default) {
const defaultSlotChildren = slots.default()
defaultSlotChildren.forEach((child) => {
// Check if the child is a component and has a 'name' property
if (typeof child.type === 'object' && 'name' in child.type) {
if (child.type.name !== 'ITab') {
console.warn('The default slot can only contain ITab components')
}
}
})
}
if (slots.content) {
const contentSlotChildren = slots.content()
contentSlotChildren.forEach((child) => {
if (typeof child.type === 'object' && 'name' in child.type) {
if (child.type.name !== 'ITabContent') {
console.warn('The content slot can only contain ITabContent components')
}
}
})
}
}
// Check slot contents when component is mounted
onMounted(validateSlots)
return { cn, isVertical }
}
})
</script>
61 changes: 61 additions & 0 deletions src/ui/tab/tab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<TabsTrigger :value="value"
:class="cn(isVertical(orientation) && 'vertical-text p-1.5')"
@click="handleClick">
<slot/>
</TabsTrigger>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue'
import { cn } from '@/lib/utils.ts'
import { isVertical } from '@/ui/tab/utils.ts'
import { TabsTrigger } from '@/components/ui/tabs'
interface TabsContext
{
selectedValue: { value: string }
orientation: 'horizontal' | 'vertical'
setSelectedValue: (val: string) => void
}
export default defineComponent({
name: 'ITab',
components: { TabsTrigger },
props: {
value: {
type: String,
required: true
}
},
setup(props)
{
// Specify the type when injecting the context and provide a default value to prevent injection failure
const tabsContext = inject<TabsContext>('tabsContext', {
selectedValue: { value: '' },
orientation: 'horizontal',
setSelectedValue: () => {
}
})
// Get direction value
const orientation = tabsContext.orientation
const handleClick = () => {
if (tabsContext) {
tabsContext.setSelectedValue(props.value)
}
}
return { cn, isVertical, handleClick, orientation }
}
})
</script>

<style scoped>
.vertical-text {
writing-mode: vertical-rl;
text-orientation: upright;
white-space: nowrap;
}
</style>
4 changes: 4 additions & 0 deletions src/ui/tab/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function isVertical(position: string): boolean
{
return position === 'vertical'
}
48 changes: 48 additions & 0 deletions src/views/pages/components/TabHome.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<ICard>
<template #title>Horizontal</template>
<div class="mt-2">
<ITabs value="tab1" orientation="horizontal">
<template #default>
<ITab value="tab1">Tab 1</ITab>
<ITab value="tab2">Tab 2</ITab>
</template>
<template #content>
<ITabContent value="tab1">Content 1</ITabContent>
<ITabContent value="tab2">Content 2</ITabContent>
</template>
</ITabs>
</div>
</ICard>
<ICard>
<template #title>Vertical</template>
<div class="mt-2">
<ITabs value="tab1" orientation="vertical">
<template #default>
<ITab value="tab1">Tab 1</ITab>
<ITab value="tab2">Tab 2</ITab>
</template>
<template #content>
<ITabContent value="tab1">Content 1</ITabContent>
<ITabContent value="tab2">Content 2</ITabContent>
</template>
</ITabs>
</div>
</ICard>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import ICard from '@/ui/card/card.vue'
import ITab from '@/ui/tab/tab.vue'
import ITabs from '@/ui/tab/index.vue'
import ITabContent from '@/ui/tab/content.vue'
export default defineComponent({
name: 'TabHome',
components: {
ITabContent, ITabs, ITab,
ICard
}
})
</script>

0 comments on commit aadc80c

Please sign in to comment.