-
+
+
+
diff --git a/packages/components/tabs/__tests__/tabs.spec.ts b/packages/components/tabs/__tests__/tabs.spec.ts
index 9537019e0..cdb59b7b4 100644
--- a/packages/components/tabs/__tests__/tabs.spec.ts
+++ b/packages/components/tabs/__tests__/tabs.spec.ts
@@ -3,8 +3,8 @@ import { h } from 'vue'
import { renderWork, wait } from '@tests'
-import Tab from '../src/Tab'
import Tabs from '../src/Tabs'
+import { Tab } from '../src/tab'
import { TabsProps } from '../src/types'
const defaultSlots = [
diff --git a/packages/components/tabs/demo/Card.vue b/packages/components/tabs/demo/Card.vue
index cba27c5a8..bc905de38 100644
--- a/packages/components/tabs/demo/Card.vue
+++ b/packages/components/tabs/demo/Card.vue
@@ -1,13 +1,11 @@
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+ Content of Tab {{ key }}
diff --git a/packages/components/tabs/demo/CustomTab.md b/packages/components/tabs/demo/CustomTab.md
index 240f8007d..e847745e1 100644
--- a/packages/components/tabs/demo/CustomTab.md
+++ b/packages/components/tabs/demo/CustomTab.md
@@ -2,7 +2,7 @@
title:
zh: 自定义标签
en: Custom tab
-order: 5
+order: 12
---
## zh
diff --git a/packages/components/tabs/demo/CustomTab.vue b/packages/components/tabs/demo/CustomTab.vue
index 0897743c9..c3d6a36bc 100644
--- a/packages/components/tabs/demo/CustomTab.vue
+++ b/packages/components/tabs/demo/CustomTab.vue
@@ -1,31 +1,32 @@
-
-
-
-
- Tab 1
-
- Content of Tab 1
-
-
-
-
- Tab 2
-
- Content of Tab 2
-
-
-
-
- Tab 3
-
- Content of Tab 3
-
+
+ Default {{ title }}
+ Default content of Tab {{ key }}
+ Custom {{ title }}
+ Custom content of Tab {{ key }}
diff --git a/packages/components/tabs/demo/Disabled.md b/packages/components/tabs/demo/Disabled.md
index 89a45be05..1736d5d99 100644
--- a/packages/components/tabs/demo/Disabled.md
+++ b/packages/components/tabs/demo/Disabled.md
@@ -2,12 +2,12 @@
title:
zh: 禁用
en: Disabled
-order: 3
+order: 5
---
## zh
-使用`disabled`控制标签是否禁用
+使用 `disabled` 控制标签是否禁用
## en
diff --git a/packages/components/tabs/demo/Disabled.vue b/packages/components/tabs/demo/Disabled.vue
index 4c0754e22..f199b4e36 100644
--- a/packages/components/tabs/demo/Disabled.vue
+++ b/packages/components/tabs/demo/Disabled.vue
@@ -1,19 +1,13 @@
-
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+
+ Content of Tab {{ key }}
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+ Content of Tab {{ key }}
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+ Content of Tab {{ key }}
@@ -21,5 +15,8 @@
diff --git a/packages/components/tabs/demo/Dynamic.md b/packages/components/tabs/demo/Dynamic.md
new file mode 100644
index 000000000..b1f4a0855
--- /dev/null
+++ b/packages/components/tabs/demo/Dynamic.md
@@ -0,0 +1,14 @@
+---
+title:
+ zh: 新增和关闭
+ en: Add and Close
+order: 10
+---
+
+## zh
+
+卡片类型标签页,默认为此类型
+
+## en
+
+The simplest usage.
diff --git a/packages/components/tabs/demo/Dynamic.vue b/packages/components/tabs/demo/Dynamic.vue
new file mode 100644
index 000000000..8dda5afca
--- /dev/null
+++ b/packages/components/tabs/demo/Dynamic.vue
@@ -0,0 +1,23 @@
+
+
+ Content of Tab {{ key }}
+
+
+
+
diff --git a/packages/components/tabs/demo/Line.md b/packages/components/tabs/demo/Line.md
index 0d36dce64..dd730c141 100644
--- a/packages/components/tabs/demo/Line.md
+++ b/packages/components/tabs/demo/Line.md
@@ -7,7 +7,7 @@ order: 1
## zh
-下划线类型标签页
+通过设置 `type` 为 `line`, 展示下划线类型标签页
## en
diff --git a/packages/components/tabs/demo/Line.vue b/packages/components/tabs/demo/Line.vue
index 84ac75956..eaf0dff35 100644
--- a/packages/components/tabs/demo/Line.vue
+++ b/packages/components/tabs/demo/Line.vue
@@ -1,13 +1,11 @@
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+ Content of Tab {{ key }}
diff --git a/packages/components/tabs/demo/Placement.md b/packages/components/tabs/demo/Placement.md
index 2b670bb09..92c2b9346 100644
--- a/packages/components/tabs/demo/Placement.md
+++ b/packages/components/tabs/demo/Placement.md
@@ -1,13 +1,13 @@
---
title:
- zh: 标签页位置
+ zh: 位置
en: Basic usage
-order: 4
+order: 6
---
## zh
-提供了`top`、`bottom`、`start`、`bottom`四种标签页位置,默认为`top`,其他类型仅在type为`line`生效
+提供了 `top`、`bottom`、`start`、`bottom` 四种标签页位置,默认为 `top`,其他类型仅在 `type` 为 `line` 生效
## en
diff --git a/packages/components/tabs/demo/Placement.vue b/packages/components/tabs/demo/Placement.vue
index d20913f30..9622ebee6 100644
--- a/packages/components/tabs/demo/Placement.vue
+++ b/packages/components/tabs/demo/Placement.vue
@@ -1,15 +1,10 @@
-
- top
- start
- end
- bottom
-
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
-
+
+
+
+ Content of Tab {{ key }}
+
+
diff --git a/packages/components/tabs/demo/Scroll.md b/packages/components/tabs/demo/Scroll.md
index e1aa63a7e..2ee3539c2 100644
--- a/packages/components/tabs/demo/Scroll.md
+++ b/packages/components/tabs/demo/Scroll.md
@@ -2,7 +2,7 @@
title:
zh: 滑动
en: Basic usage
-order: 6
+order: 10
---
## zh
diff --git a/packages/components/tabs/demo/Scroll.vue b/packages/components/tabs/demo/Scroll.vue
index 74cb716ba..64f84fb8c 100644
--- a/packages/components/tabs/demo/Scroll.vue
+++ b/packages/components/tabs/demo/Scroll.vue
@@ -1,21 +1,16 @@
-
-
- Content of Tab {{ panel }}
+
+
+ Content of Tab {{ key }}
-
- Content of Tab {{ panel }}
+
+ Content of Tab {{ key }}
-
- Content of Tab {{ panel }}
-
-
- Content of Tab {{ panel }}
+
+ Content of Tab {{ key }}
- addTab
- closeTab
- 移动到第几个:
+
@@ -23,17 +18,9 @@
diff --git a/packages/components/tabs/demo/Segment.md b/packages/components/tabs/demo/Segment.md
index 8124a1345..434874afe 100644
--- a/packages/components/tabs/demo/Segment.md
+++ b/packages/components/tabs/demo/Segment.md
@@ -7,7 +7,7 @@ order: 2
## zh
-分段类型标签页,当type为`segment`时,可以使用`mode`属性,修改按钮的类型
+通过设置 `type` 为 `line`, 展示分段类型标签页,此时可以使用 `mode` 属性,修改按钮的颜色
## en
diff --git a/packages/components/tabs/demo/Segment.vue b/packages/components/tabs/demo/Segment.vue
index 13681988b..551e3f2ff 100644
--- a/packages/components/tabs/demo/Segment.vue
+++ b/packages/components/tabs/demo/Segment.vue
@@ -1,20 +1,16 @@
-
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+
+ Content of Tab {{ key }}
-
- Content of Tab 1
- Content of Tab 2
- Content of Tab 3
+
+ Content of Tab {{ key }}
diff --git a/packages/components/tabs/demo/Size.md b/packages/components/tabs/demo/Size.md
index 6f9ba3e28..131feb0cd 100644
--- a/packages/components/tabs/demo/Size.md
+++ b/packages/components/tabs/demo/Size.md
@@ -2,11 +2,11 @@
title:
zh: 尺寸
en: Size
-order: 7
+order: 8
---
## zh
-提供了`lg` 和 `md` 两种尺寸
+提供了 `lg` 和 `md` 两种尺寸
## en
diff --git a/packages/components/tabs/demo/Size.vue b/packages/components/tabs/demo/Size.vue
index b5663e394..f1f62ee9e 100644
--- a/packages/components/tabs/demo/Size.vue
+++ b/packages/components/tabs/demo/Size.vue
@@ -1,37 +1,27 @@
-
-
- 大
- 中
-
-
-
-
-
+
+
+
+ Content of Tab {{ key }}
-
-
-
-
+
+ Content of Tab {{ key }}
-
-
-
-
-
-
-
-
-
+
+ Content of Tab {{ key }}
-
diff --git a/packages/components/tabs/docs/Api.zh.md b/packages/components/tabs/docs/Api.zh.md
index 8eb9ba277..fa194f476 100644
--- a/packages/components/tabs/docs/Api.zh.md
+++ b/packages/components/tabs/docs/Api.zh.md
@@ -11,7 +11,6 @@
| `placement` | 标签的方位 | `'top' \| 'start' \| 'end' \| 'bottom'` | `'top'` | - | 其他类型仅在type为`line`生效 |
| `type` | 标签的类型 | `'card' \| 'line' \| 'segment'` | `'card'`| - | - |
| `size` | 标签页的尺寸 | `'lg' \| 'md'` | `'md'` | ✅ | - |
-| `onTabClick` | 标签被点击的回调 | `(key: VKey, evt: Event) => void`| - | - | - |
| `onPreClick` | 滚动状态下,Pre按钮被点击的回调 | `(evt: Event) => void`| - | - | - |
| `onNextClick` | 滚动状态下,Next按钮被点击的回调 | `(evt: Event) => void`| - | - | - |
| `onBeforeLeave` | 切换标签之前的钩子函数,返回 `false` 或 promise resolve `false` 或 promise reject 会阻止切换 | `(key: VKey, oldKey?: VKey) => boolean \| Promise
`| - | - | - |
diff --git a/packages/components/tabs/index.ts b/packages/components/tabs/index.ts
index 3d85ba026..63294537c 100644
--- a/packages/components/tabs/index.ts
+++ b/packages/components/tabs/index.ts
@@ -5,23 +5,25 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import type { TabComponent, TabsComponent } from './src/types'
+import type { TabsComponent } from './src/types'
-import Tab from './src/Tab'
import Tabs from './src/Tabs'
+import { Tab } from './src/tab'
const IxTabs = Tabs as unknown as TabsComponent
-const IxTab = Tab as unknown as TabComponent
+const IxTab = Tab
export { IxTabs, IxTab }
export type {
- TabsInstance,
+ TabsPublicProps as TabsProps,
TabsComponent,
- TabInstance,
+ TabsInstance,
+ TabProps,
TabComponent,
- TabsPublicProps as TabsProps,
- TabPublicProps as TabProps,
+ TabsMode,
TabsPlacement,
TabsSize,
+ TabsType,
+ TabsData,
} from './src/types'
diff --git a/packages/components/tabs/src/InternalTabs.tsx b/packages/components/tabs/src/InternalTabs.tsx
deleted file mode 100644
index 35f68665f..000000000
--- a/packages/components/tabs/src/InternalTabs.tsx
+++ /dev/null
@@ -1,375 +0,0 @@
-/**
- * @license
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
- */
-
-import type { TabProps, TabsProps } from './types'
-import type { VKey } from '@idux/cdk/utils'
-import type { IconInstance } from '@idux/components/icon'
-import type { CSSProperties, ComputedRef, PropType, Ref, VNode } from 'vue'
-
-import { computed, defineComponent, normalizeClass, provide, ref, vShow, watch, withDirectives } from 'vue'
-
-import { curry, isNil } from 'lodash-es'
-
-import { useResizeObserver } from '@idux/cdk/resize'
-import { addClass, callEmit, removeClass, useControlledProp, useState } from '@idux/cdk/utils'
-import { useGlobalConfig } from '@idux/components/config'
-import { IxIcon } from '@idux/components/icon'
-
-import TabNav from './TabNav'
-import { useSelectedElOffset } from './composables/useOffset'
-import { useNavRelatedElSize, useSelectedElVisibleSize } from './composables/useSize'
-import { tabsToken } from './tokens'
-import { tabsProps } from './types'
-
-function useNavPreNextClasses(
- props: TabsProps,
- mergedPrefixCls: ComputedRef,
- type: 'pre' | 'next',
- disabled: Ref,
-) {
- return computed(() => {
- const { placement } = props
- const prefixCls = mergedPrefixCls.value
- return normalizeClass({
- [`${prefixCls}-nav-${type}`]: true,
- [`${prefixCls}-nav-${type}-disabled`]: disabled.value,
- [`${prefixCls}-nav-${type}-${placement}`]: true,
- })
- })
-}
-
-function filterTabVNodes(
- props: TabsProps,
- tabVNodes: VNode[],
- selectedKey: ComputedRef,
- defaultSelectedKey: VKey | undefined,
-): VNode[] {
- const renderTabVNodes: VNode[] = []
- tabVNodes.forEach(vNode => {
- const { key } = vNode
- const { forceRender, disabled } = vNode.props as TabProps
- const _disabled = !isNil(disabled)
- const useVShow = forceRender ?? props.forceRender
- const show = (selectedKey.value ?? defaultSelectedKey) === key && !_disabled
- if (useVShow) {
- renderTabVNodes.push(withDirectives(vNode, [[vShow, show]]))
- } else if (show) {
- renderTabVNodes.push(vNode)
- }
- })
- return renderTabVNodes
-}
-
-export default defineComponent({
- props: {
- ...tabsProps,
- tabs: { type: Array as PropType, default: undefined },
- },
- setup(props) {
- const common = useGlobalConfig('common')
- const mergedPrefixCls = computed(() => `${common.prefixCls}-tabs`)
- const config = useGlobalConfig('tabs')
-
- const navWrapperElRef = ref(null)
- const navElRef = ref(null)
- const navBarElRef = ref(null)
- const navPreElRef = ref(null)
- const selectedElRef = ref(null)
-
- const [selectedKey, setSelectedKey] = useControlledProp(props, 'selectedKey')
-
- const isLineType = computed(() => props.type === 'line')
- const isSegmentType = computed(() => props.type === 'segment')
-
- const horizontalPlacement = ['top', 'bottom']
- const isHorizontal = computed(() => horizontalPlacement.includes(props.placement))
-
- const mergedSize = computed(() => props.size ?? config.size)
-
- const [navOffset, setNavOffset] = useState(0)
- const [barStyle, setBarStyle] = useState({})
- const {
- navSize,
- navWrapperSize,
- navPreNextSize,
- selectedElSize,
- setNavElSize,
- setSelectedElSize,
- setNavPreNextElSize,
- } = useNavRelatedElSize(isHorizontal, navWrapperElRef, navElRef, navPreElRef, selectedElRef)
- const { selectedElOffset, setSelectedElOffset } = useSelectedElOffset(isHorizontal, navPreNextSize, selectedElRef)
-
- const hasScroll = computed(() => {
- return navSize.value > navWrapperSize.value
- })
-
- watch(
- hasScroll,
- () => {
- setNavPreNextElSize()
- updateNavBarStyle()
- updateSelectedOffset()
- },
- {
- flush: 'post',
- },
- )
-
- const selectedElVisibleSize = useSelectedElVisibleSize(navWrapperSize, selectedElOffset, navOffset)
-
- // 处理存在滚动状态下,滚动到被选中的tab,并修正其位置
- const updateSelectedOffset = () => {
- if (hasScroll.value) {
- const size = selectedElVisibleSize.value / navWrapperSize.value
- const inVisibleRange = size < 2
- if (inVisibleRange) {
- // 可视范围内需要处理展示不全的问题,需要修正
- if (selectedElVisibleSize.value < selectedElSize.value) {
- // 即可视范围内最后一个tab没有展示完全
- setNavOffset(navOffset.value + selectedElSize.value - selectedElVisibleSize.value + navPreNextSize.value)
- } else if (selectedElVisibleSize.value / navWrapperSize.value > 1) {
- // 即可视范围内第一个tab没有展示完全
- setNavOffset(
- navOffset.value - ((selectedElVisibleSize.value % navWrapperSize.value) + navPreNextSize.value),
- )
- }
- } else {
- setNavOffset(selectedElOffset.value - navPreNextSize.value)
- }
- }
- }
-
- const preReached = ref(false)
- const nextReached = ref(false)
-
- const classes = computed(() => {
- const { type, placement, mode } = props
- const prefixCls = mergedPrefixCls.value
- const size = mergedSize.value
- return normalizeClass({
- [prefixCls]: true,
- [`${prefixCls}-${size}`]: true,
- [`${prefixCls}-${type}`]: true,
- [`${prefixCls}-nav-${placement}`]: placement === 'top' || type === 'line',
- [`${prefixCls}-nav-${mode}`]: type === 'segment',
- })
- })
-
- const navWrapperClass = computed(() => {
- const prefixCls = mergedPrefixCls.value
- return normalizeClass({
- [`${prefixCls}-nav-wrapper`]: true,
- [`${prefixCls}-nav-wrapper-has-scroll`]: hasScroll.value,
- })
- })
-
- const curryNavPreNextClasses = curry(useNavPreNextClasses)(props, mergedPrefixCls)
- const navPreClasses = curryNavPreNextClasses('pre', preReached)
- const navNextClasses = curryNavPreNextClasses('next', nextReached)
-
- const handleTabClick = async (key: VKey, evt: Event) => {
- const leaveResult = callEmit(props.onBeforeLeave, key, selectedKey.value)
- const result = await leaveResult
- if (result !== false) {
- callEmit(props.onTabClick, key, evt)
- // 处理当前被选中元素再次被点击,需要修正其位置
- if (key === selectedKey.value) {
- updateSelectedOffset()
- }
- setSelectedKey(key)
- }
- }
-
- const updateNavBarStyle = () => {
- if (isLineType.value && navBarElRef.value) {
- const isBarDisabled = selectedElRef.value?.classList.contains(`${mergedPrefixCls.value}-nav-tab-disabled`)
- const barDisabledClassName = `${mergedPrefixCls.value}-nav-bar-disabled`
- const barOffset = selectedElOffset.value - navOffset.value + 'px'
- const barSize = selectedElSize.value + 'px'
-
- setBarStyle({
- width: isHorizontal.value ? barSize : '',
- left: isHorizontal.value ? barOffset : '',
- top: isHorizontal.value ? '' : barOffset,
- height: isHorizontal.value ? '' : barSize,
- })
- if (isBarDisabled) {
- addClass(navBarElRef.value, barDisabledClassName)
- } else {
- removeClass(navBarElRef.value, barDisabledClassName)
- }
- }
- }
-
- const handlePreClick = (evt: Event) => {
- if (!preReached.value) {
- callEmit(props.onPreClick, evt)
- const mergedOffset = navOffset.value + navPreNextSize.value
- const offset = mergedOffset < navWrapperSize.value ? 0 : mergedOffset - navWrapperSize.value
- setNavOffset(offset)
- }
- }
-
- const handleNextClick = (evt: Event) => {
- if (!nextReached.value) {
- callEmit(props.onNextClick, evt)
- const mergedNavSize = navSize.value! + navPreNextSize.value * 2
- const _offset = navOffset.value + navWrapperSize.value
- let offset
- if (mergedNavSize - _offset < navWrapperSize.value) {
- offset = mergedNavSize - navWrapperSize.value
- } else {
- offset = _offset
- }
- setNavOffset(offset)
- }
- }
-
- const judgePreNextStatus = () => {
- preReached.value = navOffset.value === 0
- nextReached.value = navSize.value - navOffset.value <= navWrapperSize.value
- }
-
- const update = () => {
- setNavElSize()
- setNavPreNextElSize()
- setSelectedElSize()
- setSelectedElOffset()
- updateNavBarStyle()
- judgePreNextStatus()
- }
-
- watch(
- navOffset,
- val => {
- if (navElRef.value) {
- navElRef.value.style.transform = `translate${isHorizontal.value ? 'X' : 'Y'}(-${val}px)`
- judgePreNextStatus()
- updateNavBarStyle()
- }
- },
- {
- flush: 'post',
- },
- )
-
- let isAddTabs = false
-
- watch(
- () => props.tabs,
- (val = [], oldVal = []) => {
- update()
- isAddTabs = val.length > oldVal.length
- },
- {
- flush: 'post',
- },
- )
-
- watch(
- navSize,
- (currentSize, oldSize) => {
- let offset = navOffset.value
- if (currentSize < oldSize && !isAddTabs) {
- offset += currentSize - oldSize
- setNavOffset(offset > 0 ? offset : 0)
- }
- },
- {
- flush: 'post',
- },
- )
-
- watch(selectedKey, val => {
- const hasSelectedKey = props.tabs?.find(item => {
- return val === item.key
- })
- if (!hasSelectedKey) {
- selectedElRef.value = null
- }
- })
-
- watch(
- selectedElRef,
- () => {
- setSelectedElSize()
- setSelectedElOffset()
- setSelectedElOffset()
- updateSelectedOffset()
- updateNavBarStyle()
- },
- {
- flush: 'post',
- },
- )
-
- useResizeObserver(navWrapperElRef, update)
-
- provide(tabsToken, {
- selectedKey,
- selectedElRef,
- mergedPrefixCls,
- handleTabClick,
- })
-
- return () => {
- let defaultSelectedKey: VKey = 1
-
- const tabVNodes =
- props.tabs?.map((item, index) => {
- if (isNil(item.key)) {
- item.key = index + 1
- } else if (index === 0) {
- defaultSelectedKey = item.key as VKey
- }
- return item
- }) ?? []
-
- return (
-
-
- {hasScroll.value && (
-
- )}
-
- {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- tabVNodes.map((vnode: any) => (
-
- ))
- }
-
- {hasScroll.value && (
-
- )}
- {!isSegmentType.value &&
}
- {isLineType.value && (
-
- )}
-
-
- {filterTabVNodes(props, tabVNodes, selectedKey, defaultSelectedKey)}
-
-
- )
- }
- },
-})
diff --git a/packages/components/tabs/src/Tab.tsx b/packages/components/tabs/src/Tab.tsx
deleted file mode 100644
index d0549e6b7..000000000
--- a/packages/components/tabs/src/Tab.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * @license
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
- */
-
-import { defineComponent, inject } from 'vue'
-
-import { tabsToken } from './tokens'
-import { tabProps } from './types'
-
-export default defineComponent({
- __IDUX_TAB: true,
- name: 'IxTab',
- props: tabProps,
- setup(_, { slots }) {
- const { mergedPrefixCls } = inject(tabsToken)!
-
- return () => {
- return {slots.default?.()}
- }
- },
-})
diff --git a/packages/components/tabs/src/TabNav.tsx b/packages/components/tabs/src/TabNav.tsx
deleted file mode 100644
index 4a48cdff3..000000000
--- a/packages/components/tabs/src/TabNav.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
- */
-
-import { computed, defineComponent, inject, normalizeClass, ref, watchEffect } from 'vue'
-
-import { useKey } from '@idux/components/utils'
-
-import { tabsToken } from './tokens'
-import { tabNavProps } from './types'
-
-export default defineComponent({
- name: 'IxTabNav',
- props: tabNavProps,
- setup(props, { slots }) {
- const key = useKey()
-
- const { selectedKey, selectedElRef, mergedPrefixCls, handleTabClick } = inject(tabsToken)!
-
- const selfElRef = ref(null)
- const isSelected = computed(() => (selectedKey.value ?? props.defaultSelectedKey) === key)
- const prefixCls = computed(() => `${mergedPrefixCls.value}-nav`)
- const classes = computed(() => {
- return normalizeClass({
- [`${prefixCls.value}-tab`]: true,
- [`${prefixCls.value}-tab-selected`]: isSelected.value,
- [`${prefixCls.value}-tab-disabled`]: props.disabled,
- })
- })
-
- watchEffect(() => {
- if (isSelected.value && selfElRef.value) {
- selectedElRef.value = selfElRef.value
- }
- })
-
- const onClick = (evt: Event) => {
- if (!props.disabled) {
- handleTabClick(key, evt)
- }
- }
-
- return () => {
- const tab = {slots.title?.() ?? props.title}
- return (
-
- {tab}
-
- )
- }
- },
-})
diff --git a/packages/components/tabs/src/Tabs.tsx b/packages/components/tabs/src/Tabs.tsx
index a09df9faf..b848ef135 100644
--- a/packages/components/tabs/src/Tabs.tsx
+++ b/packages/components/tabs/src/Tabs.tsx
@@ -5,34 +5,135 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
-import { defineComponent } from 'vue'
+import { computed, defineComponent, normalizeClass, provide } from 'vue'
-import { type VKey, flattenNode, useControlledProp } from '@idux/cdk/utils'
+import { isNil, isString } from 'lodash-es'
-import InternalTabs from './InternalTabs'
-import { tabsProps } from './types'
+import { type VKey, callEmit, useControlledProp, useState } from '@idux/cdk/utils'
+import { useGlobalConfig } from '@idux/components/config'
+
+import { useDataSource } from './composables/useDataSource'
+import TabNavWrapper from './contents/TabNavWrapper'
+import TabPane from './contents/TabPane'
+import { tabsToken } from './tokens'
+import { type TabsData, tabsProps } from './types'
export default defineComponent({
name: 'IxTabs',
- inheritAttrs: false,
props: tabsProps,
- setup(props, { attrs, slots }) {
- return () => {
- const tabVNodes = flattenNode(slots.default?.(), { key: '__IDUX_TAB' })
+ setup(props, { slots }) {
+ const common = useGlobalConfig('common')
+ const config = useGlobalConfig('tabs')
+
+ const mergedPrefixCls = computed(() => `${common.prefixCls}-tabs`)
+ const mergedSize = computed(() => props.size ?? config.size)
- const [, setSelectedKey] = useControlledProp(props, 'selectedKey')
+ const mergedDataSource = useDataSource(props, slots)
+ const horizontalPlacement = ['top', 'bottom']
+ const isHorizontal = computed(() => horizontalPlacement.includes(props.placement))
- const handleChange = (key: VKey) => {
+ const [selectedKey, setSelectedKey] = useControlledProp(props, 'selectedKey')
+ const [closedKeys, setClosedKeys] = useState([])
+
+ const handleTabClick = async (key: VKey, evt: Event) => {
+ const result = await callEmit(props.onBeforeLeave, key, selectedKey.value)
+ if (result !== false) {
setSelectedKey(key)
+ /**
+ * @deprecated
+ */
+ callEmit(props.onTabClick, key, evt)
}
+ }
- const internalTabsProps = {
- ...props,
- tabs: tabVNodes,
- 'onUpdate:selectedKey': handleChange,
+ const handleTabClose = async (key: VKey) => {
+ const result = await callEmit(props.onClose, key)
+ if (result !== false) {
+ const currSelectedKey = selectedKey.value
+ const currClosedKeys = closedKeys.value
+
+ if (key === selectedKey.value) {
+ setSelectedKey(getNextSelectedKey(mergedDataSource.value, currClosedKeys, currSelectedKey))
+ }
+
+ setClosedKeys([...currClosedKeys, key])
}
+ }
+
+ provide(tabsToken, {
+ props,
+ mergedPrefixCls,
+ mergedDataSource,
+ isHorizontal,
+ closedKeys,
+ handleTabClick,
+ handleTabClose,
+ })
+
+ const classes = computed(() => {
+ const { type, placement, mode } = props
+ const prefixCls = mergedPrefixCls.value
+ const size = mergedSize.value
+ return normalizeClass({
+ [prefixCls]: true,
+ [`${prefixCls}-${size}`]: true,
+ [`${prefixCls}-${type}`]: true,
+ [`${prefixCls}-nav-${placement}`]: placement === 'top' || type === 'line',
+ [`${prefixCls}-nav-${mode}`]: type === 'segment',
+ })
+ })
- return
+ return () => {
+ const dataSource = mergedDataSource.value
+ const currClosedKeys = closedKeys.value
+ const currSelectedKey = selectedKey.value ?? getNextSelectedKey(dataSource, currClosedKeys)
+ return (
+
+
+
+ {dataSource.map(data => {
+ const { key, content, forceRender, customContent = 'content' } = data
+ const contentSlot = isString(customContent) ? slots[customContent] : customContent
+ return (
+
+ )
+ })}
+
+
+ )
}
},
})
+
+function getNextSelectedKey(dataSource: TabsData[], closedKeys: VKey[], currSelectedKey?: VKey) {
+ const isValidNext = (data: TabsData) => !data.disabled && !closedKeys.includes(data.key)
+
+ const currSelectedIndex = isNil(currSelectedKey) ? -1 : dataSource.findIndex(item => item.key === currSelectedKey)
+
+ if (currSelectedIndex === -1) {
+ return dataSource.find(isValidNext)?.key
+ }
+
+ for (let index = currSelectedIndex + 1; index < dataSource.length; index++) {
+ const data = dataSource[index]
+ if (isValidNext(data)) {
+ return data.key
+ }
+ }
+
+ for (let index = currSelectedIndex - 1; index > 0; index--) {
+ const data = dataSource[index]
+ if (isValidNext(data)) {
+ return data.key
+ }
+ }
+
+ return
+}
diff --git a/packages/components/tabs/src/composables/useDataSource.ts b/packages/components/tabs/src/composables/useDataSource.ts
new file mode 100644
index 000000000..8580f15a3
--- /dev/null
+++ b/packages/components/tabs/src/composables/useDataSource.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { TabsData, TabsProps } from '../types'
+import type { ComputedRef, Slots, VNode } from 'vue'
+
+import { computed } from 'vue'
+
+import { flattenNode, uniqueId } from '@idux/cdk/utils'
+
+import { tabKey } from '../tab'
+
+export function useDataSource(props: TabsProps, slots: Slots): ComputedRef {
+ return computed(() => {
+ return props.dataSource ?? convertData(slots.default?.())
+ })
+}
+
+function convertData(nodes: VNode[] | undefined): TabsData[] {
+ const convertedData: TabsData[] = []
+
+ flattenNode(nodes, { key: tabKey }).forEach(node => {
+ const props = node.props ?? ({} as any)
+ const slots = node.children ?? ({} as any)
+
+ const { key = uniqueId('__IDUX_TAB_'), closable, content, disabled, forceRender, title, ...rest } = props
+ const { title: customTitle, default: customContent } = slots
+
+ const option: TabsData = {
+ key,
+ content,
+ closable: closable === '' || closable,
+ disabled: disabled === '' || disabled,
+ forceRender: forceRender === '' || forceRender,
+ title,
+ customTitle,
+ customContent,
+ ...rest,
+ }
+
+ convertedData.push(option)
+ })
+
+ return convertedData
+}
diff --git a/packages/components/tabs/src/composables/useOffset.ts b/packages/components/tabs/src/composables/useOffset.ts
deleted file mode 100644
index cb79a1fd8..000000000
--- a/packages/components/tabs/src/composables/useOffset.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @license
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
- */
-
-import { type ComputedRef, type Ref, computed } from 'vue'
-
-import { useState } from '@idux/cdk/utils'
-
-export interface Offset {
- selectedElOffset: ComputedRef
- setSelectedElOffset: () => void
-}
-
-export function useSelectedElOffset(
- isHorizontal: ComputedRef,
- navPreNextSize: ComputedRef,
- selectedElRef: Ref,
-): Offset {
- const [selectedLeft, setSelectedLeft] = useState(0)
- const [selectedTop, setSelectedTop] = useState(0)
-
- const selectedElOffset = computed(
- () => (isHorizontal.value ? selectedLeft.value : selectedTop.value) + navPreNextSize.value,
- )
-
- const setSelectedElOffset = () => {
- setSelectedLeft(selectedElRef.value?.offsetLeft ?? 0)
- setSelectedTop(selectedElRef.value?.offsetTop ?? 0)
- }
-
- return {
- selectedElOffset,
- setSelectedElOffset,
- }
-}
diff --git a/packages/components/tabs/src/composables/useSize.ts b/packages/components/tabs/src/composables/useSize.ts
deleted file mode 100644
index a3dd481d4..000000000
--- a/packages/components/tabs/src/composables/useSize.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * @license
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
- */
-
-import type { IconInstance } from '@idux/components/icon'
-
-import { type ComputedRef, type Ref, computed } from 'vue'
-
-import { useState } from '@idux/cdk/utils'
-
-export interface NavRelatedElSize {
- navSize: ComputedRef
- navWrapperSize: ComputedRef
- navPreNextSize: ComputedRef
- selectedElSize: ComputedRef
- setNavElSize: () => void
- setSelectedElSize: () => void
- setNavPreNextElSize: () => void
-}
-
-export function useNavRelatedElSize(
- isHorizontal: ComputedRef,
- navWrapperElRef: Ref,
- navElRef: Ref,
- navPreElRef: Ref,
- selectedElRef: Ref,
-): NavRelatedElSize {
- const [navWidth, setNavWidth] = useState(0)
- const [navHeight, setNavHeight] = useState(0)
-
- const [navWrapperWidth, setNavWrapperWidth] = useState(0)
- const [navWrapperHeight, setNavWrapperHeight] = useState(0)
- const [navPreNextWidth, setNavPreNextWidth] = useState(0)
- const [navPreNextHeight, setNavPreNextHeight] = useState(0)
- const [selectedWidth, setSelectedWidth] = useState(0)
- const [selectedHeight, setSelectedHeight] = useState(0)
-
- const navSize = computed(() => (isHorizontal.value ? navWidth.value : navHeight.value))
- const navPreNextSize = computed(() => (isHorizontal.value ? navPreNextWidth.value : navPreNextHeight.value))
- const navWrapperSize = computed(() => (isHorizontal.value ? navWrapperWidth.value : navWrapperHeight.value))
- const selectedElSize = computed(() => (isHorizontal.value ? selectedWidth.value : selectedHeight.value))
-
- // dom 的size无法响应式获取,只能手动获取
- const setNavElSize = () => {
- setNavWrapperWidth(navWrapperElRef.value?.offsetWidth ?? 0)
- setNavWrapperHeight(navWrapperElRef.value?.offsetHeight ?? 0)
-
- setNavWidth(navElRef.value?.offsetWidth ?? 0)
- setNavHeight(navElRef.value?.offsetHeight ?? 0)
- }
-
- const setSelectedElSize = () => {
- setSelectedWidth(selectedElRef.value?.offsetWidth ?? 0)
- setSelectedHeight(selectedElRef.value?.offsetHeight ?? 0)
- }
-
- const setNavPreNextElSize = () => {
- setNavPreNextWidth(navPreElRef.value?.$el.offsetWidth ?? 0)
- setNavPreNextHeight(navPreElRef.value?.$el.offsetHeight ?? 0)
- }
-
- return {
- navSize,
- navWrapperSize,
- navPreNextSize,
- selectedElSize,
- setNavElSize,
- setSelectedElSize,
- setNavPreNextElSize,
- }
-}
-
-export function useSelectedElVisibleSize(
- navWrapperSize: ComputedRef,
- selectedElOffset: ComputedRef,
- navOffset: ComputedRef,
-): ComputedRef {
- return computed(() => {
- return navWrapperSize.value + navOffset.value - selectedElOffset.value
- })
-}
diff --git a/packages/components/tabs/src/composables/useSizeObservable.ts b/packages/components/tabs/src/composables/useSizeObservable.ts
new file mode 100644
index 000000000..fba10c15a
--- /dev/null
+++ b/packages/components/tabs/src/composables/useSizeObservable.ts
@@ -0,0 +1,150 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { type ComputedRef, type ShallowRef, computed, nextTick, onMounted, watch } from 'vue'
+
+import { useResizeObserver } from '@idux/cdk/resize'
+import { useState } from '@idux/cdk/utils'
+
+export interface SizeObservableContext {
+ wrapperSize: ComputedRef
+ prevNextSize: ComputedRef
+ navSize: ComputedRef
+ selectedNavSize: ComputedRef
+ navOffset: ComputedRef
+ selectedNavOffset: ComputedRef
+ hasScroll: ComputedRef
+ calcPrevOffset: () => void
+ calcNextOffset: () => void
+ calcNavSize: () => void
+}
+
+export function useSizeObservable(
+ wrapperRef: ShallowRef,
+ prevNextRef: ShallowRef,
+ navRef: ShallowRef,
+ selectedNavRef: ShallowRef,
+ isHorizontal: ComputedRef,
+): SizeObservableContext {
+ const [wrapperSize, setWrapperSize] = useState(0)
+ const [prevNextSize, setPreNextSize] = useState(0)
+ const [navSize, setNavSize] = useState(0)
+ const [selectedNavSize, setSelectedNavSize] = useState(0)
+
+ const [navOffset, setNavOffset] = useState(0)
+ const [selectedNavOffset, setSelectedNavOffset] = useState(0)
+
+ const sizeProp = computed(() => (isHorizontal.value ? 'offsetWidth' : 'offsetHeight'))
+
+ useResizeObserver(wrapperRef, entry => {
+ nextTick(() => {
+ const target = entry.target as HTMLElement
+ setWrapperSize(target[sizeProp.value])
+ })
+ })
+
+ useResizeObserver(prevNextRef, entry => {
+ nextTick(() => {
+ const target = entry.target as HTMLElement
+ setPreNextSize(target[sizeProp.value])
+ })
+ })
+
+ useResizeObserver(navRef, entry => {
+ nextTick(() => {
+ const target = entry.target as HTMLElement
+ setNavSize(target[sizeProp.value])
+ })
+ })
+
+ useResizeObserver(selectedNavRef, entry => {
+ nextTick(() => {
+ const target = entry.target as HTMLElement
+ setSelectedNavSize(target[sizeProp.value])
+ setSelectedNavOffset((target as HTMLElement)[isHorizontal.value ? 'offsetLeft' : 'offsetTop'])
+ updateNavOffset()
+ })
+ })
+
+ const hasScroll = computed(() => navSize.value > wrapperSize.value)
+ const selectedNavVisibleSize = computed(
+ () => wrapperSize.value + navOffset.value - prevNextSize.value - selectedNavOffset.value,
+ )
+
+ const updateNavOffset = () => {
+ if (hasScroll.value) {
+ const _selectedNavVisibleSize = selectedNavVisibleSize.value
+ const _wrapperSize = wrapperSize.value
+ const _prevNextSize = prevNextSize.value
+ const _selectedNavSize = selectedNavSize.value
+ const _navOffset = navOffset.value
+
+ // 判断是否在可视范围内
+ const inVisibleRange = _selectedNavVisibleSize / _wrapperSize < 2
+ if (inVisibleRange) {
+ // 可视范围内需要处理展示不全的问题,需要修正
+ if (_selectedNavVisibleSize < _selectedNavSize) {
+ // 即可视范围内最后一个tab没有展示完全
+ setNavOffset(_navOffset + _selectedNavSize - _selectedNavVisibleSize + _prevNextSize)
+ } else if (_selectedNavVisibleSize / _wrapperSize > 1) {
+ // 即可视范围内第一个tab没有展示完全
+ setNavOffset(_navOffset - ((_selectedNavVisibleSize % _wrapperSize) + _prevNextSize))
+ }
+ } else {
+ setNavOffset(prevNextSize.value + selectedNavOffset.value - _prevNextSize)
+ }
+ } else {
+ setNavOffset(0)
+ }
+ }
+
+ watch(hasScroll, updateNavOffset, { flush: 'post' })
+
+ onMounted(() => {
+ // 需要等 DOM 渲染完成后,重新计算一次,才是最准确的
+ setTimeout(() => updateNavOffset())
+ })
+
+ const calcPrevOffset = () => {
+ const mergedOffset = navOffset.value + prevNextSize.value
+ const offset = mergedOffset < wrapperSize.value ? 0 : mergedOffset - wrapperSize.value
+ setNavOffset(offset)
+ }
+
+ const calcNextOffset = () => {
+ const mergedNavSize = navSize.value + prevNextSize.value * 2
+ const mergedOffset = navOffset.value + wrapperSize.value
+ let offset
+ if (mergedNavSize - mergedOffset < wrapperSize.value) {
+ offset = mergedNavSize - wrapperSize.value
+ } else {
+ offset = mergedOffset
+ }
+ setNavOffset(offset)
+ }
+
+ const calcNavSize = () => {
+ const element = navRef.value
+ if (!element) {
+ return
+ }
+ setPreNextSize(element[sizeProp.value])
+ }
+
+ return {
+ wrapperSize,
+ prevNextSize,
+ navSize,
+ selectedNavSize,
+ navOffset,
+ selectedNavOffset,
+ hasScroll,
+ calcPrevOffset,
+ calcNextOffset,
+ calcNavSize,
+ }
+}
diff --git a/packages/components/tabs/src/contents/TabNav.tsx b/packages/components/tabs/src/contents/TabNav.tsx
new file mode 100644
index 000000000..85dbbc3f2
--- /dev/null
+++ b/packages/components/tabs/src/contents/TabNav.tsx
@@ -0,0 +1,77 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { computed, defineComponent, inject, normalizeClass, shallowRef, watchEffect } from 'vue'
+
+import { IxIcon } from '@idux/components/icon'
+import { useKey } from '@idux/components/utils'
+
+import { tabsToken } from '../tokens'
+import { tabNavProps } from '../types'
+
+export default defineComponent({
+ name: 'IxTabNav',
+ props: tabNavProps,
+ setup(props, { slots }) {
+ const key = useKey()
+
+ const { props: tabsProps, mergedPrefixCls, handleTabClick, handleTabClose } = inject(tabsToken)!
+
+ const prefixCls = computed(() => `${mergedPrefixCls.value}-nav-tab`)
+ const mergedClosable = computed(() => props.closable ?? tabsProps.closable)
+
+ const elementRef = shallowRef()
+
+ const classes = computed(() => {
+ const { disabled, selected } = props
+ return normalizeClass({
+ [`${prefixCls.value}`]: true,
+ [`${prefixCls.value}-disabled`]: disabled,
+ [`${prefixCls.value}-selected`]: selected,
+ })
+ })
+
+ watchEffect(() => {
+ const element = elementRef.value
+ if (element && props.selected) {
+ props.onSelected(element)
+ }
+ })
+
+ const handleClick = (evt: Event) => {
+ if (!props.disabled) {
+ handleTabClick(key, evt)
+ }
+ }
+
+ const handleClose = (evt: Event) => {
+ if (!props.disabled) {
+ evt.stopPropagation()
+ handleTabClose(key)
+ }
+ }
+
+ return () => {
+ if (props.closed) {
+ return null
+ }
+ const titleNode = slots.title
+ ? slots.title({ key, disabled: props.disabled, selected: props.selected, title: props.title })
+ : props.title
+ return (
+
+ {titleNode}
+ {mergedClosable.value && (
+
+
+
+ )}
+
+ )
+ }
+ },
+})
diff --git a/packages/components/tabs/src/contents/TabNavWrapper.tsx b/packages/components/tabs/src/contents/TabNavWrapper.tsx
new file mode 100644
index 000000000..df3d60f5b
--- /dev/null
+++ b/packages/components/tabs/src/contents/TabNavWrapper.tsx
@@ -0,0 +1,154 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { type ComputedRef, type Ref, computed, defineComponent, inject, normalizeClass, shallowRef } from 'vue'
+
+import { isString } from 'lodash-es'
+
+import { callEmit, convertCssPixel } from '@idux/cdk/utils'
+import { IxIcon } from '@idux/components/icon'
+
+import TabNav from './TabNav'
+import { useSizeObservable } from '../composables/useSizeObservable'
+import { tabsToken } from '../tokens'
+import { TabsProps } from '../types'
+
+export default defineComponent({
+ props: { selectedKey: { type: [Number, String, Symbol] } },
+ setup(props, { slots }) {
+ const { props: tabsProps, mergedPrefixCls, mergedDataSource, isHorizontal, closedKeys } = inject(tabsToken)!
+
+ const wrapperRef = shallowRef()
+ const prevNextRef = shallowRef()
+ const navRef = shallowRef()
+ const selectedNavRef = shallowRef()
+
+ const {
+ wrapperSize,
+ prevNextSize,
+ navSize,
+ selectedNavSize,
+ navOffset,
+ selectedNavOffset,
+ hasScroll,
+ calcPrevOffset,
+ calcNextOffset,
+ } = useSizeObservable(wrapperRef, prevNextRef, navRef, selectedNavRef, isHorizontal)
+
+ const prevNavDisabled = computed(() => navOffset.value === 0)
+ const nextNavDisabled = computed(() => navSize.value - navOffset.value <= wrapperSize.value)
+
+ const classes = computed(() => {
+ const prefixCls = mergedPrefixCls.value
+ return normalizeClass({
+ [`${prefixCls}-nav-wrapper`]: true,
+ [`${prefixCls}-nav-wrapper-has-scroll`]: hasScroll.value,
+ })
+ })
+
+ const preClasses = usePreNextClasses(tabsProps, mergedPrefixCls, 'pre', prevNavDisabled)
+ const nextClasses = usePreNextClasses(tabsProps, mergedPrefixCls, 'next', nextNavDisabled)
+
+ const navStyle = computed(() => {
+ return `transform: translate${isHorizontal.value ? 'X' : 'Y'}(-${navOffset.value}px)`
+ })
+
+ const navBarStyle = computed(() => {
+ const size = convertCssPixel(selectedNavSize.value)
+ const offset = convertCssPixel(prevNextSize.value + selectedNavOffset.value - navOffset.value)
+ if (isHorizontal.value) {
+ return { width: size, left: offset }
+ } else {
+ return { height: size, top: offset }
+ }
+ })
+
+ const handlePrevClick = (evt: Event) => {
+ if (!prevNavDisabled.value) {
+ callEmit(tabsProps.onPreClick, evt)
+ calcPrevOffset()
+ }
+ }
+
+ const handleNextClick = (evt: Event) => {
+ if (!nextNavDisabled.value) {
+ callEmit(tabsProps.onNextClick, evt)
+ calcNextOffset()
+ }
+ }
+
+ const handleSelectedNavChange = (element: HTMLElement) => {
+ selectedNavRef.value = element
+ }
+
+ const handleAdd = () => callEmit(tabsProps.onAdd)
+
+ return () => {
+ const { selectedKey } = props
+ const { addable, type } = tabsProps
+ const dataSource = mergedDataSource.value
+ const currClosedKeys = closedKeys.value
+ return (
+
+ {hasScroll.value && (
+
+ )}
+
+ {dataSource.map(data => {
+ const { key, content, customContent, customTitle = 'title', ...navProps } = data
+ const titleSlot = isString(customTitle) ? slots[customTitle] : customTitle
+ return (
+
+ )
+ })}
+ {addable && (
+
+
+
+
+
+ )}
+
+ {hasScroll.value && (
+
+ )}
+ {type !== 'segment' &&
}
+ {type === 'line' &&
}
+
+ )
+ }
+ },
+})
+
+function usePreNextClasses(
+ tabsProps: TabsProps,
+ mergedPrefixCls: ComputedRef,
+ type: 'pre' | 'next',
+ disabled: Ref,
+) {
+ return computed(() => {
+ const { placement } = tabsProps
+ const prefixCls = mergedPrefixCls.value
+ return normalizeClass({
+ [`${prefixCls}-nav-${type}`]: true,
+ [`${prefixCls}-nav-${type}-disabled`]: disabled.value,
+ [`${prefixCls}-nav-${type}-${placement}`]: true,
+ })
+ })
+}
diff --git a/packages/components/tabs/src/contents/TabPane.tsx b/packages/components/tabs/src/contents/TabPane.tsx
new file mode 100644
index 000000000..f9a2f3b4f
--- /dev/null
+++ b/packages/components/tabs/src/contents/TabPane.tsx
@@ -0,0 +1,39 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+import { computed, defineComponent, inject } from 'vue'
+
+import { useKey } from '@idux/components/utils'
+
+import { tabsToken } from '../tokens'
+import { tabPaneProps } from '../types'
+
+export default defineComponent({
+ name: 'IxTabPane',
+ props: tabPaneProps,
+ setup(props, { slots }) {
+ const key = useKey()
+ const { props: tabsProps, mergedPrefixCls } = inject(tabsToken)!
+ const mergedForceRender = computed(() => props.forceRender ?? tabsProps.forceRender)
+
+ let rendered = false
+ return () => {
+ if (props.closed || !(rendered || props.selected || mergedForceRender.value)) {
+ return null
+ }
+ rendered = true
+ const contentNode = slots.content
+ ? slots.content({ key, content: props.content, selected: props.selected })
+ : props.content
+ return (
+
+ {contentNode}
+
+ )
+ }
+ },
+})
diff --git a/packages/components/tabs/src/tab.ts b/packages/components/tabs/src/tab.ts
new file mode 100644
index 000000000..7ef91ee84
--- /dev/null
+++ b/packages/components/tabs/src/tab.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import type { TabComponent } from './types'
+
+const tabKey = Symbol('IxTab')
+const Tab = (() => {}) as TabComponent
+Tab.displayName = 'IxTab'
+;(Tab as any)[tabKey] = true
+
+export { Tab, tabKey }
diff --git a/packages/components/tabs/src/tokens.ts b/packages/components/tabs/src/tokens.ts
index 726994f6e..976652733 100644
--- a/packages/components/tabs/src/tokens.ts
+++ b/packages/components/tabs/src/tokens.ts
@@ -5,14 +5,18 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/
+import type { TabsData, TabsProps } from './types'
import type { VKey } from '@idux/cdk/utils'
-import type { ComputedRef, InjectionKey, Ref } from 'vue'
+import type { ComputedRef, InjectionKey } from 'vue'
export interface TabsContext {
- selectedKey: Ref
- selectedElRef: Ref
+ props: TabsProps
mergedPrefixCls: ComputedRef
+ mergedDataSource: ComputedRef
+ isHorizontal: ComputedRef
+ closedKeys: ComputedRef
handleTabClick: (key: VKey, evt: Event) => Promise
+ handleTabClose: (key: VKey) => Promise
}
export const tabsToken: InjectionKey = Symbol('tabsToken')
diff --git a/packages/components/tabs/src/types.ts b/packages/components/tabs/src/types.ts
index 73a03d327..75b6569d9 100644
--- a/packages/components/tabs/src/types.ts
+++ b/packages/components/tabs/src/types.ts
@@ -8,48 +8,73 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } from '@idux/cdk/utils'
-import type { DefineComponent, HTMLAttributes, PropType } from 'vue'
+import type { DefineComponent, FunctionalComponent, HTMLAttributes, PropType, VNodeChild } from 'vue'
-export type TabsType = 'card' | 'line' | 'segment'
-export type TabsPlacement = 'top' | 'bottom' | 'start' | 'end'
export type TabsMode = 'default' | 'primary'
+export type TabsPlacement = 'top' | 'bottom' | 'start' | 'end'
export type TabsSize = 'lg' | 'md'
+export type TabsType = 'card' | 'line' | 'segment'
export const tabsProps = {
selectedKey: { type: [Number, String, Symbol] as PropType, default: undefined },
- type: { type: String as PropType, default: 'card' },
+
+ addable: { type: Boolean, default: false },
+ closable: { type: Boolean, default: false },
+ dataSource: { type: Array as PropType, default: undefined },
forceRender: { type: Boolean, default: false },
- placement: { type: String as PropType, default: 'top' },
mode: { type: String as PropType, default: 'default' },
- size: String as PropType,
+ placement: { type: String as PropType, default: 'top' },
+ size: { type: String as PropType, default: undefined },
+ type: { type: String as PropType, default: 'card' },
'onUpdate:selectedKey': [Function, Array] as PropType void>>,
+ onAdd: [Function, Array] as PropType void | boolean | Promise>>,
+ onClose: [Function, Array] as PropType void | boolean | Promise>>,
+ /**
+ * @deprecated
+ */
onTabClick: [Function, Array] as PropType void>>,
onPreClick: [Function, Array] as PropType void>>,
onNextClick: [Function, Array] as PropType void>>,
- onBeforeLeave: [Function, Array] as PropType boolean | Promise>>,
-} as const
-
-export const tabProps = {
- title: { type: String, default: undefined },
- forceRender: { type: Boolean, default: undefined },
- disabled: { type: Boolean, default: false },
+ onBeforeLeave: [Function, Array] as PropType<
+ MaybeArray<(key: any, oldKey?: any) => void | boolean | Promise>
+ >,
} as const
export type TabsProps = ExtractInnerPropTypes
-export type TabsPublicProps = ExtractPublicPropTypes
+export type TabsPublicProps = Omit, 'onTabClick'>
export type TabsComponent = DefineComponent & TabsPublicProps>
export type TabsInstance = InstanceType>
-export type TabProps = ExtractInnerPropTypes
-export type TabPublicProps = ExtractPublicPropTypes
-export type TabComponent = DefineComponent & TabPublicProps>
-export type TabInstance = InstanceType>
+export interface TabProps {
+ closable?: boolean
+ content?: string
+ disabled?: boolean
+ forceRender?: boolean
+ title?: string
+}
+export type TabComponent = FunctionalComponent & TabProps>
-// private
+export interface TabsData extends TabProps {
+ key: K
+ customContent?: string | ((data: { key: VKey; content?: string; selected?: boolean }) => VNodeChild)
+ customTitle?: string | ((data: { key: VKey; disabled?: boolean; selected?: boolean; title?: string }) => VNodeChild)
+ [key: string]: any
+}
+// private
export const tabNavProps = {
- defaultSelectedKey: { type: [Number, String, Symbol] as PropType, default: undefined },
- title: { type: String, default: undefined },
+ closable: { type: Boolean, default: undefined },
+ closed: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
+ selected: { type: Boolean, default: undefined },
+ title: { type: String, default: undefined },
+ onSelected: { type: Function, required: true },
+} as const
+
+export const tabPaneProps = {
+ closed: { type: Boolean, default: undefined },
+ content: { type: String, default: undefined },
+ forceRender: { type: Boolean, default: undefined },
+ selected: { type: Boolean, default: undefined },
} as const
diff --git a/packages/components/tabs/style/index.less b/packages/components/tabs/style/index.less
index f906f3ddf..94465bf07 100644
--- a/packages/components/tabs/style/index.less
+++ b/packages/components/tabs/style/index.less
@@ -57,7 +57,7 @@
&&-has-scroll {
> .@{tabs-prefix}-nav {
- margin: 0 @tabs-nav-pre-next-width;;
+ margin: 0 @tabs-nav-pre-next-width;
}
> .@{icon-prefix} {
@@ -148,11 +148,13 @@
&-pre-top,
&-pre-bottom {
left: 0;
+ box-shadow: 3px 0 8px 0 rgb(0 0 0 / 8%);
}
&-next-top,
&-next-bottom {
right: 0;
+ box-shadow: -3px 0 8px 0 rgb(0 0 0 / 8%);
}
&-pre-start,
@@ -244,7 +246,8 @@
&-bar {
position: absolute;
background-color: @tabs-nav-bar-color;
- transition: width @transition-duration-base @ease-in-out, height @transition-duration-base @ease-in-out, left @transition-duration-base @ease-in-out, top @transition-duration-base @ease-in-out;
+ transition: width @transition-duration-base @ease-in-out, height @transition-duration-base @ease-in-out,
+ left @transition-duration-base @ease-in-out, top @transition-duration-base @ease-in-out;
bottom: 0;
height: @tabs-nav-bar-height;
border-radius: @tabs-border-radius @tabs-border-radius 0 0;
@@ -469,4 +472,13 @@
}
}
}
+
+ &-nav-add-icon,
+ &-nav-close-icon {
+ font-size: var(--ix-font-size-lg);
+ }
+
+ &-nav-close-icon {
+ margin-left: 8px;
+ }
}