({})
+ 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..10e5ee5f9 100644
--- a/scripts/gulp/build/apiParse.ts
+++ b/scripts/gulp/build/apiParse.ts
@@ -247,7 +247,7 @@ export const migrateToProAPIs = async (proApis: JSONType, distPath: string): Pro
const bindings = pick(proApis, bindingsFlag)[bindingsFlag]
const components = await readJSON(distPath)
- forIn(bindings, (component: JSONType, name: string) => {
+ forIn(bindings, (component: any, name: string) => {
forIn(component, (attrs: Array, proName: string) => {
for (const attrName of attrs) {
langs