From a37b793faed4ecd6a9004c2ea87b0b13fad67508 Mon Sep 17 00:00:00 2001 From: w2xi <57785259+w2xi@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:00:39 +0800 Subject: [PATCH] fix(components): [tabs] update tabs order correctly when reordering (#21064) * fix(components): [tabs] update tabs order correctly when reordering * fix * perf: reorder only if child components have been moved * chore: tweak the test --------- Co-authored-by: dopamine --- packages/components/carousel/src/constants.ts | 3 +- .../carousel/src/use-carousel-item.ts | 1 + .../components/splitter/src/split-panel.vue | 1 + packages/components/splitter/src/type.ts | 3 +- packages/components/steps/src/item.vue | 4 +- .../components/tabs/__tests__/tabs.test.tsx | 41 ++++++++++++++++ packages/components/tabs/src/constants.ts | 10 +++- packages/components/tabs/src/tab-pane.vue | 1 + packages/hooks/use-ordered-children/index.ts | 48 ++++++++++++++++++- 9 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/components/carousel/src/constants.ts b/packages/components/carousel/src/constants.ts index 84809c0dfe..aba8b809d4 100644 --- a/packages/components/carousel/src/constants.ts +++ b/packages/components/carousel/src/constants.ts @@ -1,4 +1,4 @@ -import type { InjectionKey, Ref } from 'vue' +import type { InjectionKey, Ref, VNode } from 'vue' import type { CarouselItemProps } from './carousel-item' export type CarouselItemStates = { @@ -15,6 +15,7 @@ export type CarouselItemContext = { props: CarouselItemProps states: CarouselItemStates uid: number + getVnode: () => VNode translateItem: (index: number, activeIndex: number, oldIndex?: number) => void } diff --git a/packages/components/carousel/src/use-carousel-item.ts b/packages/components/carousel/src/use-carousel-item.ts index ab3f7b9aeb..fda5bd7d92 100644 --- a/packages/components/carousel/src/use-carousel-item.ts +++ b/packages/components/carousel/src/use-carousel-item.ts @@ -144,6 +144,7 @@ export const useCarouselItem = (props: CarouselItemProps) => { animating, }), uid: instance.uid, + getVnode: () => instance.vnode, translateItem, } diff --git a/packages/components/splitter/src/split-panel.vue b/packages/components/splitter/src/split-panel.vue index 3c9df09f00..d8c7fb3bc3 100644 --- a/packages/components/splitter/src/split-panel.vue +++ b/packages/components/splitter/src/split-panel.vue @@ -155,6 +155,7 @@ watch( const _panel = reactive({ el: panelEl.value!, uid, + getVnode: () => instance.vnode, setIndex, ...props, collapsible: getCollapsible(props.collapsible), diff --git a/packages/components/splitter/src/type.ts b/packages/components/splitter/src/type.ts index 1bd03752ce..1fcf37b55d 100644 --- a/packages/components/splitter/src/type.ts +++ b/packages/components/splitter/src/type.ts @@ -1,9 +1,10 @@ -import type { InjectionKey, UnwrapRef } from 'vue' +import type { InjectionKey, UnwrapRef, VNode } from 'vue' export type Layout = 'horizontal' | 'vertical' export type PanelItemState = UnwrapRef<{ uid: number + getVnode: () => VNode el: HTMLElement collapsible: { start?: boolean; end?: boolean } max?: number | string diff --git a/packages/components/steps/src/item.vue b/packages/components/steps/src/item.vue index 81ef10752c..60723f96ad 100644 --- a/packages/components/steps/src/item.vue +++ b/packages/components/steps/src/item.vue @@ -62,11 +62,12 @@ import { isNumber } from '@element-plus/utils' import { stepProps } from './item' import { STEPS_INJECTION_KEY } from './tokens' +import type { CSSProperties, Ref, VNode } from 'vue' import type { StepsProps } from './steps' -import type { CSSProperties, Ref } from 'vue' export interface StepItemState { uid: number + getVnode: () => VNode currentStatus: string setIndex: (val: number) => void calcProgress: (status: string) => void @@ -192,6 +193,7 @@ const updateStatus = (activeIndex: number) => { const stepItemState = reactive({ uid: currentInstance.uid, + getVnode: () => currentInstance.vnode, currentStatus, setIndex, calcProgress, diff --git a/packages/components/tabs/__tests__/tabs.test.tsx b/packages/components/tabs/__tests__/tabs.test.tsx index 4204208a20..9db8b8e020 100644 --- a/packages/components/tabs/__tests__/tabs.test.tsx +++ b/packages/components/tabs/__tests__/tabs.test.tsx @@ -950,4 +950,45 @@ describe('Tabs.vue', () => { // Verify the model value has been updated expect(activeName.value).toBe('tab2') }) + + test('tab order should update when v-for array is reordered', async () => { + const itemList = ref([ + { key: 'a', value: 'A' }, + { key: 'b', value: 'B' }, + { key: 'c', value: 'C' }, + { key: 'd', value: 'D' }, + ]) + + const wrapper = mount(() => ( + + {itemList.value.map((item) => ( + + ))} + + )) + + await nextTick() + + const navWrapper = wrapper.findComponent(TabNav) + let navItemsWrapper = navWrapper.findAll('.el-tabs__item') + + // Check initial order + expect(navItemsWrapper[0].text()).toContain('A') + expect(navItemsWrapper[1].text()).toContain('B') + expect(navItemsWrapper[2].text()).toContain('C') + expect(navItemsWrapper[3].text()).toContain('D') + + // Reverse the array + itemList.value.reverse() + + await nextTick() + + navItemsWrapper = navWrapper.findAll('.el-tabs__item') + + // Check that the order has updated + expect(navItemsWrapper[0].text()).toContain('D') + expect(navItemsWrapper[1].text()).toContain('C') + expect(navItemsWrapper[2].text()).toContain('B') + expect(navItemsWrapper[3].text()).toContain('A') + }) }) diff --git a/packages/components/tabs/src/constants.ts b/packages/components/tabs/src/constants.ts index c891311a96..3ebdf87fa2 100644 --- a/packages/components/tabs/src/constants.ts +++ b/packages/components/tabs/src/constants.ts @@ -1,4 +1,11 @@ -import type { ComputedRef, InjectionKey, Ref, Slots, UnwrapRef } from 'vue' +import type { + ComputedRef, + InjectionKey, + Ref, + Slots, + UnwrapRef, + VNode, +} from 'vue' import type { TabsProps } from './tabs' import type { TabPaneProps } from './tab-pane' @@ -6,6 +13,7 @@ export type TabPaneName = string | number export type TabsPaneContext = UnwrapRef<{ uid: number + getVnode: () => VNode slots: Slots props: TabPaneProps paneName: ComputedRef diff --git a/packages/components/tabs/src/tab-pane.vue b/packages/components/tabs/src/tab-pane.vue index 4bf88ccaaa..e1908e6c5e 100644 --- a/packages/components/tabs/src/tab-pane.vue +++ b/packages/components/tabs/src/tab-pane.vue @@ -67,6 +67,7 @@ watch(active, (val) => { const pane = reactive({ uid: instance.uid, + getVnode: () => instance.vnode, slots, props, paneName, diff --git a/packages/hooks/use-ordered-children/index.ts b/packages/hooks/use-ordered-children/index.ts index 8eb281fc31..fb0d89c993 100644 --- a/packages/hooks/use-ordered-children/index.ts +++ b/packages/hooks/use-ordered-children/index.ts @@ -1,8 +1,20 @@ -import { defineComponent, h, isVNode, shallowRef, triggerRef } from 'vue' +import { + defineComponent, + h, + isVNode, + onMounted, + shallowRef, + triggerRef, +} from 'vue' import { flattedChildren } from '@element-plus/utils' import type { ComponentInternalInstance, VNode } from 'vue' +type ChildEssential = { + uid: number + getVnode: () => VNode +} + const getOrderedChildren = ( vm: ComponentInternalInstance, childComponentName: string, @@ -18,21 +30,53 @@ const getOrderedChildren = ( return uids.map((uid) => children[uid]).filter((p) => !!p) } -export const useOrderedChildren = ( +export const useOrderedChildren = ( vm: ComponentInternalInstance, childComponentName: string ) => { const children = shallowRef>({}) const orderedChildren = shallowRef([]) + const nodesMap = new WeakMap() const addChild = (child: T) => { children.value[child.uid] = child triggerRef(children) + + onMounted(() => { + const childNode = child.getVnode().el! as Node + const parentNode = childNode.parentNode! + + if (!nodesMap.has(parentNode)) { + nodesMap.set(parentNode, []) + + const originalFn = parentNode.insertBefore.bind(parentNode) + parentNode.insertBefore = ( + node: T, + anchor: Node | null + ) => { + // Schedule a job to update `orderedChildren` if the root element of child components is moved + const shouldSortChildren = nodesMap + .get(parentNode)! + .some((el) => node === el || anchor === el) + if (shouldSortChildren) triggerRef(children) + + return originalFn(node, anchor) + } + } + + nodesMap.get(parentNode)!.push(childNode) + }) } const removeChild = (child: T) => { delete children.value[child.uid] triggerRef(children) + + const childNode = child.getVnode().el! as Node + const parentNode = childNode.parentNode! + const childNodes = nodesMap.get(parentNode)! + const index = childNodes.indexOf(childNode) + childNodes.splice(index, 1) } const sortChildren = () => {