diff --git a/packages/components/tabs/demo/Scroll.vue b/packages/components/tabs/demo/Scroll.vue index 7d0c20c25..ff76f5247 100644 --- a/packages/components/tabs/demo/Scroll.vue +++ b/packages/components/tabs/demo/Scroll.vue @@ -1,24 +1,35 @@ diff --git a/packages/components/tabs/src/Tabs.tsx b/packages/components/tabs/src/Tabs.tsx index bfd494feb..2fab91d63 100644 --- a/packages/components/tabs/src/Tabs.tsx +++ b/packages/components/tabs/src/Tabs.tsx @@ -8,27 +8,30 @@ import type { TabProps, TabsProps } from './types' import type { VKey } from '@idux/cdk/utils' import type { IconInstance } from '@idux/components/icon' -import type { ComputedRef, Ref, VNode } from 'vue' +import type { CSSProperties, ComputedRef, PropType, Ref, VNode } from 'vue' -import { computed, defineComponent, nextTick, normalizeClass, provide, ref, vShow, watch, withDirectives } 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, flattenNode, removeClass, useControlledProp } from '@idux/cdk/utils' +import { addClass, callEmit, flattenNode, 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, useVisibleSize } from './composables/useSize' +import { useNavRelatedElSize, useSelectedElVisibleSize } from './composables/useSize' import { tabsToken } from './tokens' import { tabsProps } from './types' -export default defineComponent({ +const InternalTabs = defineComponent({ name: 'IxTabs', - props: tabsProps, - setup(props, { slots }) { + props: { + ...tabsProps, + tabs: { type: Array as PropType, default: undefined }, + }, + setup(props) { const common = useGlobalConfig('common') const mergedPrefixCls = computed(() => `${common.prefixCls}-tabs`) @@ -46,28 +49,37 @@ export default defineComponent({ const horizontalPlacement = ['top', 'bottom'] const isHorizontal = computed(() => horizontalPlacement.includes(props.placement)) - const { navSize, navWrapperSize, navPreNextSize, selectedElSize, syncNavRelatedElSize } = useNavRelatedElSize( - isHorizontal, - navWrapperElRef, - navElRef, - navPreElRef, - selectedElRef, - ) + const [navOffset, setNavOffset] = useState(0) + const [barStyle, setBarStyle] = useState({}) + const { navSize, navWrapperSize, navPreNextSize, selectedElSize, syncNavElSize, syncSelectedElSize } = + useNavRelatedElSize(isHorizontal, navWrapperElRef, navElRef, navPreElRef, selectedElRef) + const { selectedElOffset, syncSelectedElOffset } = useSelectedElOffset(isHorizontal, navPreNextSize, selectedElRef) - const navOffset = ref(0) - const { selectedElOffset } = useSelectedElOffset(isHorizontal, selectedElRef) + const hasScroll = computed(() => { + return navSize.value! > navWrapperSize.value + }) - const visibleSize = useVisibleSize(navWrapperSize, selectedElOffset, navOffset) - const hasScroll = computed(() => navSize.value > navWrapperSize.value) + const selectedElVisibleSize = useSelectedElVisibleSize(navWrapperSize, selectedElOffset, navOffset) - // 处理存在滚动状态下,手动点击tab时nav位置偏移(在可视范围内第一个和最后一个tab没有展示完全,需要进行偏移使其展示完全;) - const updateNavOffset = () => { - if (visibleSize.value < selectedElSize.value) { - // 即可视范围内最后一个tab没有展示完全 - navOffset.value += selectedElSize.value - visibleSize.value - } else if (visibleSize.value / navWrapperSize.value > 1) { - // 即可视范围内第一个tab没有展示完全 - navOffset.value -= visibleSize.value % navWrapperSize.value + // 处理存在滚动状态下,滚动到被选中的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) + } } } @@ -94,6 +106,10 @@ export default defineComponent({ const result = await leaveResult if (result !== false) { callEmit(props.onTabClick, key, evt) + // 处理当前被选中元素再次被点击,需要修正其位置 + if (key === selectedKey.value) { + updateSelectedOffset() + } setSelectedKey(key) } } @@ -102,20 +118,15 @@ export default defineComponent({ 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 + navPreNextSize.value + 'px' + const barOffset = selectedElOffset.value - navOffset.value + 'px' const barSize = selectedElSize.value + 'px' - if (isHorizontal.value) { - navBarElRef.value.style.left = barOffset - navBarElRef.value.style.width = barSize - navBarElRef.value.style.top = '' - navBarElRef.value.style.height = '' - } else { - navBarElRef.value.style.top = barOffset - navBarElRef.value.style.height = barSize - navBarElRef.value.style.left = '' - navBarElRef.value.style.width = '' - } + setBarStyle({ + width: isHorizontal.value ? barSize : '', + left: isHorizontal.value ? barOffset : '', + top: isHorizontal.value ? '' : barOffset, + height: isHorizontal.value ? '' : barSize, + }) if (isBarDisabled) { addClass(navBarElRef.value, barDisabledClassName) } else { @@ -127,22 +138,24 @@ export default defineComponent({ const handlePreClick = (evt: Event) => { if (!preReached.value) { callEmit(props.onPreClick, evt) - const offset = navOffset.value < navWrapperSize.value ? 0 : navOffset.value - navWrapperSize.value - navOffset.value = offset + 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 (navSize.value - _offset < navWrapperSize.value) { - offset = navSize.value - navWrapperSize.value + if (mergedNavSize - _offset < navWrapperSize.value) { + offset = mergedNavSize - navWrapperSize.value } else { offset = _offset } - navOffset.value = offset + setNavOffset(offset) } } @@ -151,37 +164,86 @@ export default defineComponent({ nextReached.value = navSize.value - navOffset.value <= navWrapperSize.value } - watch(navOffset, val => { - if (navElRef.value) { - navElRef.value.style.transform = `translate${isHorizontal.value ? 'X' : 'Y'}(-${val}px)` - judgePreNextStatus() - updateNavBarStyle() - } - }) + const update = () => { + syncNavElSize() + syncSelectedElSize() + syncSelectedElOffset() + updateNavBarStyle() + judgePreNextStatus() + } - watch(selectedElRef, () => { - if (hasScroll.value) { - updateNavOffset() + 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, + (val, oldSize) => { + let offset = navOffset.value + const currentSize = val! + if (currentSize > oldSize && isAddTabs) { + offset += currentSize - oldSize + if (hasScroll.value) { + setNavOffset(offset) + } + } else if (currentSize < oldSize && !isAddTabs) { + offset += currentSize - oldSize + if (offset >= 0) { + setNavOffset(offset) + } + } + }, + { + flush: 'post', + }, + ) + + watch(selectedKey, val => { + const hasSelectedKey = props.tabs?.find(item => { + return val === item.key + }) + if (!hasSelectedKey) { + selectedElRef.value = null } - updateNavBarStyle() }) - const onTabsResize = () => { - syncNavRelatedElSize() - if (hasScroll.value) { - //存在滚动状态时,因为会增加前进、后退两个按钮,所以需要重新获取navWrapper宽度 - nextTick(() => { - syncNavRelatedElSize() - updateNavOffset() - updateNavBarStyle() - judgePreNextStatus() - }) - } else { + watch( + selectedElRef, + () => { + syncSelectedElSize() + syncSelectedElOffset() + updateSelectedOffset() updateNavBarStyle() - } - } + }, + { + flush: 'post', + }, + ) - useResizeObserver(navWrapperElRef, onTabsResize) + useResizeObserver(navWrapperElRef, update) provide(tabsToken, { selectedKey, @@ -193,14 +255,15 @@ export default defineComponent({ return () => { let defaultSelectedKey: VKey = 1 - const tabVNodes = flattenTabVNodes(slots.default?.()).map((item, index) => { - if (isNil(item.key)) { - item.key = index + 1 - } else if (index === 0) { - defaultSelectedKey = item.key - } - return item - }) + 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 (
@@ -239,7 +302,9 @@ export default defineComponent({ /> )} {!isSegmentType.value &&
} - {isLineType.value &&
} + {isLineType.value && ( +
+ )}
{filterTabVNodes(props, tabVNodes, selectedKey, defaultSelectedKey)} @@ -267,10 +332,6 @@ function useNavPreNextClasses( }) } -function flattenTabVNodes(tabVNodes: VNode[] | undefined): VNode[] { - return flattenNode(tabVNodes, { key: '__IDUX_TAB' }) -} - function filterTabVNodes( props: TabsProps, tabVNodes: VNode[], @@ -292,3 +353,28 @@ function filterTabVNodes( }) return renderTabVNodes } + +export default defineComponent({ + name: 'IxTabs', + inheritAttrs: false, + props: tabsProps, + setup(props, { attrs, slots }) { + return () => { + const tabVNodes = flattenNode(slots.default?.(), { key: '__IDUX_TAB' }) + + const [, setSelectedKey] = useControlledProp(props, 'selectedKey') + + const handleChange = (key: VKey) => { + setSelectedKey(key) + } + + const internalTabsProps = { + ...props, + tabs: tabVNodes, + 'onUpdate:selectedKey': handleChange, + } + + return + } + }, +}) diff --git a/packages/components/tabs/src/composables/useOffset.ts b/packages/components/tabs/src/composables/useOffset.ts index 8ceb18b42..0d7505757 100644 --- a/packages/components/tabs/src/composables/useOffset.ts +++ b/packages/components/tabs/src/composables/useOffset.ts @@ -5,27 +5,34 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ComputedRef, Ref } from 'vue' +import { type ComputedRef, type Ref, computed } from 'vue' -import { computed } from 'vue' +import { useState } from '@idux/cdk/utils' export interface Offset { selectedElOffset: ComputedRef + syncSelectedElOffset: () => void } export function useSelectedElOffset( isHorizontal: ComputedRef, + navPreNextSize: ComputedRef, selectedElRef: Ref, ): Offset { - const selectedElOffset = computed(() => { - if (isHorizontal.value) { - return selectedElRef.value?.offsetLeft ?? 0 - } else { - return selectedElRef.value?.offsetTop ?? 0 - } - }) + const [selectedLeft, setSelectedLeft] = useState(0) + const [selectedTop, setSelectedTop] = useState(0) + + const selectedElOffset = computed( + () => (isHorizontal.value ? selectedLeft.value : selectedTop.value) + navPreNextSize.value, + ) + + const syncSelectedElOffset = () => { + setSelectedLeft(selectedElRef.value?.offsetLeft ?? 0) + setSelectedTop(selectedElRef.value?.offsetTop ?? 0) + } return { selectedElOffset, + syncSelectedElOffset, } } diff --git a/packages/components/tabs/src/composables/useSize.ts b/packages/components/tabs/src/composables/useSize.ts index a583d5bd1..b8bb1eb5d 100644 --- a/packages/components/tabs/src/composables/useSize.ts +++ b/packages/components/tabs/src/composables/useSize.ts @@ -6,70 +6,78 @@ */ import type { IconInstance } from '@idux/components/icon' -import type { ComputedRef, Ref } from 'vue' -import { computed, ref } from 'vue' +import { type ComputedRef, type Ref, computed, watchEffect } from 'vue' + +import { useState } from '@idux/cdk/utils' export interface NavRelatedElSize { navSize: ComputedRef navWrapperSize: ComputedRef navPreNextSize: ComputedRef selectedElSize: ComputedRef - syncNavRelatedElSize: () => void + syncNavElSize: () => void + syncSelectedElSize: () => void } export function useNavRelatedElSize( - isHorizontal: ComputedRef, + isHorizontal: ComputedRef, navWrapperElRef: Ref, navElRef: Ref, navPreElRef: Ref, selectedElRef: Ref, ): NavRelatedElSize { - const navWrapperWidth = ref(0) - const navWidth = ref(0) - const navWrapperHeight = ref(0) - const navHeight = ref(0) - const navPreNextWidth = ref(0) - const navPreNextHeight = ref(0) + const [navWidth, setNavWidth] = useState(0) + const [navHeight, setNavHeight] = useState(0) - const navSize = computed(() => (isHorizontal.value ? navWidth.value : navHeight.value)) + 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)) - const navPreNextSize = computed(() => (isHorizontal.value ? navPreNextWidth.value : navPreNextHeight.value)) + // dom 的size无法响应式获取,只能手动获取 + const syncNavElSize = () => { + setNavWrapperWidth(navWrapperElRef.value?.offsetWidth ?? 0) + setNavWrapperHeight(navWrapperElRef.value?.offsetHeight ?? 0) - const selectedElSize = computed(() => { - if (isHorizontal.value) { - return selectedElRef.value?.offsetWidth ?? 0 - } else { - return selectedElRef.value?.offsetHeight ?? 0 - } - }) + setNavWidth(navElRef.value?.offsetWidth ?? 0) + setNavHeight(navElRef.value?.offsetHeight ?? 0) + } - const syncNavRelatedElSize = () => { - navPreNextWidth.value = navPreElRef.value?.$el.offsetWidth ?? 0 - navPreNextHeight.value = navPreElRef.value?.$el.offsetHeight ?? 0 - navWrapperWidth.value = (navWrapperElRef.value?.offsetWidth ?? 0) - navPreNextSize.value * 2 - navWidth.value = navElRef.value?.offsetWidth ?? 0 - navWrapperHeight.value = (navWrapperElRef.value?.offsetHeight ?? 0) - navPreNextSize.value * 2 - navHeight.value = navElRef.value?.offsetHeight ?? 0 + const syncSelectedElSize = () => { + setSelectedWidth(selectedElRef.value?.offsetWidth ?? 0) + setSelectedHeight(selectedElRef.value?.offsetHeight ?? 0) } + // 向前、向后按钮是动态渲染的,所以可以使用 watchEffect 获取其size + watchEffect(() => { + setNavPreNextWidth(navPreElRef.value?.$el.offsetWidth ?? 0) + setNavPreNextHeight(navPreElRef.value?.$el.offsetHeight ?? 0) + }) + return { navSize, navWrapperSize, navPreNextSize, selectedElSize, - syncNavRelatedElSize, + syncNavElSize, + syncSelectedElSize, } } -export function useVisibleSize( +export function useSelectedElVisibleSize( navWrapperSize: ComputedRef, selectedElOffset: ComputedRef, - navOffset: Ref, + navOffset: ComputedRef, ): ComputedRef { return computed(() => { - return navWrapperSize.value - (selectedElOffset.value - navOffset.value) + return navWrapperSize.value + navOffset.value - selectedElOffset.value }) } diff --git a/packages/components/tabs/style/index.less b/packages/components/tabs/style/index.less index acbda7296..8b7e7c4b0 100644 --- a/packages/components/tabs/style/index.less +++ b/packages/components/tabs/style/index.less @@ -25,7 +25,9 @@ font-size: @tabs-nav-font-size; &&-has-scroll { - padding: 0 @tabs-nav-pre-next-width; + > .@{tabs-prefix}-nav { + margin: 0 @tabs-nav-pre-next-width;; + } > .@{icon-prefix} { font-size: @tabs-icon-font-size; @@ -175,13 +177,10 @@ > .@{tabs-prefix}-nav { z-index: 1; > .@{tabs-prefix}-nav-tab { - margin-right: 4px; border-radius: @tabs-border-radius @tabs-border-radius 0 0; border-bottom: 1px solid @tabs-nav-border-color; + box-sizing: border-box; - &:last-child { - margin-right: 0; - } &-selected:not(.@{tabs-prefix}-nav-tab-disabled) { border: 1px solid @tabs-nav-border-color; border-bottom-color: @tabs-card-nav-tab-selected-background-color; @@ -215,7 +214,7 @@ &-bar { position: absolute; background-color: @tabs-nav-bar-color; - transition: 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; diff --git a/scripts/gulp/build/apiParse.ts b/scripts/gulp/build/apiParse.ts index 210b322ea..f442a7f62 100644 --- a/scripts/gulp/build/apiParse.ts +++ b/scripts/gulp/build/apiParse.ts @@ -247,7 +247,11 @@ export const migrateToProAPIs = async (proApis: JSONType, distPath: string): Pro const bindings = pick(proApis, bindingsFlag)[bindingsFlag] const components = await readJSON(distPath) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore forIn(bindings, (component: JSONType, name: string) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore forIn(component, (attrs: Array, proName: string) => { for (const attrName of attrs) { langs