From 0dad84b7664587553daf9d51b69cfe5c839f1cf6 Mon Sep 17 00:00:00 2001 From: danranVm Date: Mon, 17 Apr 2023 10:33:52 +0800 Subject: [PATCH] feat(comp:tabs): supoort dataSource, removable, onAdd (#1536) --- packages/cdk/resize/demo/Basic.vue | 1 + packages/components/alert/style/index.less | 2 +- .../button/style/themes/default.variable.less | 2 +- .../header/style/themes/default.variable.less | 2 +- .../style/themes/default.variable.less | 2 +- packages/components/select/src/Select.tsx | 1 - packages/components/style/themes/default.less | 1 - .../__tests__/__snapshots__/tabs.spec.ts.snap | 4 +- .../components/tabs/__tests__/tabs.spec.ts | 2 +- packages/components/tabs/demo/Card.vue | 12 +- packages/components/tabs/demo/CustomTab.md | 2 +- packages/components/tabs/demo/CustomTab.vue | 47 +-- packages/components/tabs/demo/Disabled.md | 4 +- packages/components/tabs/demo/Disabled.vue | 25 +- packages/components/tabs/demo/Dynamic.md | 14 + packages/components/tabs/demo/Dynamic.vue | 23 ++ packages/components/tabs/demo/Line.md | 2 +- packages/components/tabs/demo/Line.vue | 12 +- packages/components/tabs/demo/Placement.md | 6 +- packages/components/tabs/demo/Placement.vue | 24 +- packages/components/tabs/demo/Scroll.md | 2 +- packages/components/tabs/demo/Scroll.vue | 37 +- packages/components/tabs/demo/Segment.md | 2 +- packages/components/tabs/demo/Segment.vue | 20 +- packages/components/tabs/demo/Size.md | 4 +- packages/components/tabs/demo/Size.vue | 38 +- packages/components/tabs/docs/Api.zh.md | 1 - packages/components/tabs/index.ts | 16 +- packages/components/tabs/src/InternalTabs.tsx | 375 ------------------ packages/components/tabs/src/Tab.tsx | 24 -- packages/components/tabs/src/TabNav.tsx | 55 --- packages/components/tabs/src/Tabs.tsx | 131 +++++- .../tabs/src/composables/useDataSource.ts | 51 +++ .../tabs/src/composables/useOffset.ts | 38 -- .../tabs/src/composables/useSize.ts | 84 ---- .../tabs/src/composables/useSizeObservable.ts | 150 +++++++ .../components/tabs/src/contents/TabNav.tsx | 77 ++++ .../tabs/src/contents/TabNavWrapper.tsx | 154 +++++++ .../components/tabs/src/contents/TabPane.tsx | 39 ++ packages/components/tabs/src/tab.ts | 17 + packages/components/tabs/src/tokens.ts | 10 +- packages/components/tabs/src/types.ts | 67 +++- packages/components/tabs/style/index.less | 16 +- 43 files changed, 828 insertions(+), 768 deletions(-) create mode 100644 packages/components/tabs/demo/Dynamic.md create mode 100644 packages/components/tabs/demo/Dynamic.vue delete mode 100644 packages/components/tabs/src/InternalTabs.tsx delete mode 100644 packages/components/tabs/src/Tab.tsx delete mode 100644 packages/components/tabs/src/TabNav.tsx create mode 100644 packages/components/tabs/src/composables/useDataSource.ts delete mode 100644 packages/components/tabs/src/composables/useOffset.ts delete mode 100644 packages/components/tabs/src/composables/useSize.ts create mode 100644 packages/components/tabs/src/composables/useSizeObservable.ts create mode 100644 packages/components/tabs/src/contents/TabNav.tsx create mode 100644 packages/components/tabs/src/contents/TabNavWrapper.tsx create mode 100644 packages/components/tabs/src/contents/TabPane.tsx create mode 100644 packages/components/tabs/src/tab.ts diff --git a/packages/cdk/resize/demo/Basic.vue b/packages/cdk/resize/demo/Basic.vue index 9f3919d23..362213fc3 100644 --- a/packages/cdk/resize/demo/Basic.vue +++ b/packages/cdk/resize/demo/Basic.vue @@ -11,6 +11,7 @@ const textareaRef = ref() const text = ref('') useResizeObserver(textareaRef, entry => { + console.log(entry) const { contentRect } = entry text.value = `height: ${contentRect.height}` }) diff --git a/packages/components/alert/style/index.less b/packages/components/alert/style/index.less index c35f6dd05..4e4c62b75 100644 --- a/packages/components/alert/style/index.less +++ b/packages/components/alert/style/index.less @@ -41,7 +41,7 @@ &-icon, &-close-icon { - font-size: var(--ix-font-size-icon); + font-size: var(--ix-font-size-lg); } &-content { diff --git a/packages/components/button/style/themes/default.variable.less b/packages/components/button/style/themes/default.variable.less index 9d79a1d7f..c03074c54 100644 --- a/packages/components/button/style/themes/default.variable.less +++ b/packages/components/button/style/themes/default.variable.less @@ -44,4 +44,4 @@ @button-ghost-background-color-disabled: rgba(255, 255, 255, 0.4); @button-icon-color: inherit; -@button-icon-font-size: var(--ix-font-size-icon); +@button-icon-font-size: var(--ix-font-size-lg); diff --git a/packages/components/header/style/themes/default.variable.less b/packages/components/header/style/themes/default.variable.less index 3c265d0d0..28e60cc67 100644 --- a/packages/components/header/style/themes/default.variable.less +++ b/packages/components/header/style/themes/default.variable.less @@ -25,4 +25,4 @@ @header-description-color: var(--ix-text-color-info); -@header-icon-font-size: var(--ix-font-size-icon); +@header-icon-font-size: var(--ix-font-size-lg); diff --git a/packages/components/popconfirm/style/themes/default.variable.less b/packages/components/popconfirm/style/themes/default.variable.less index 4d2e8a368..13ff206f1 100644 --- a/packages/components/popconfirm/style/themes/default.variable.less +++ b/packages/components/popconfirm/style/themes/default.variable.less @@ -4,7 +4,7 @@ @popconfirm-border-radius: var(--ix-border-radius-md); @popconfirm-box-shadow: @shadow-bottom-md; -@popconfirm-icon-size: var(--ix-font-size-icon); +@popconfirm-icon-size: var(--ix-font-size-lg); @popconfirm-icon-color: var(--ix-color-brown); @popconfirm-title-font-size: var(--ix-font-size-md); diff --git a/packages/components/select/src/Select.tsx b/packages/components/select/src/Select.tsx index f2754a96e..64501e64d 100644 --- a/packages/components/select/src/Select.tsx +++ b/packages/components/select/src/Select.tsx @@ -102,7 +102,6 @@ export default defineComponent({ }) const handleOptionClick = (option: SelectData) => { - console.log('handleOptionClick', option) changeSelected(getKey.value(option)) props.allowInput && clearInput() if (!props.multiple) { diff --git a/packages/components/style/themes/default.less b/packages/components/style/themes/default.less index 1297875d1..94923acc4 100644 --- a/packages/components/style/themes/default.less +++ b/packages/components/style/themes/default.less @@ -73,7 +73,6 @@ --ix-font-size-xl: 20px; --ix-font-size-2xl: 24px; --ix-font-size-3xl: 30px; - --ix-font-size-icon: 16px; // Font-Weight --ix-font-weight-xs: 200; diff --git a/packages/components/tabs/__tests__/__snapshots__/tabs.spec.ts.snap b/packages/components/tabs/__tests__/__snapshots__/tabs.spec.ts.snap index b24173571..8dc94bc95 100644 --- a/packages/components/tabs/__tests__/__snapshots__/tabs.spec.ts.snap +++ b/packages/components/tabs/__tests__/__snapshots__/tabs.spec.ts.snap @@ -4,7 +4,9 @@ exports[`Tabs > render work 1`] = ` "
-
+
+ +
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 @@ 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 @@ 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 @@ @@ -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 @@ + + + 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 @@ 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 @@ 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 @@ @@ -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 @@ 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 @@ - 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; + } }