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 <coderzyou@gmail.com>
This commit is contained in:
w2xi
2025-07-04 19:00:39 +08:00
committed by GitHub
parent 419b3e02f4
commit a37b793fae
9 changed files with 106 additions and 6 deletions

View File

@@ -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
}

View File

@@ -144,6 +144,7 @@ export const useCarouselItem = (props: CarouselItemProps) => {
animating,
}),
uid: instance.uid,
getVnode: () => instance.vnode,
translateItem,
}

View File

@@ -155,6 +155,7 @@ watch(
const _panel = reactive({
el: panelEl.value!,
uid,
getVnode: () => instance.vnode,
setIndex,
...props,
collapsible: getCollapsible(props.collapsible),

View File

@@ -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

View File

@@ -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,

View File

@@ -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(() => (
<Tabs>
{itemList.value.map((item) => (
<TabPane key={item.key} label={item.value}></TabPane>
))}
</Tabs>
))
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')
})
})

View File

@@ -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<TabPaneName | undefined>

View File

@@ -67,6 +67,7 @@ watch(active, (val) => {
const pane = reactive({
uid: instance.uid,
getVnode: () => instance.vnode,
slots,
props,
paneName,

View File

@@ -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 = <T>(
vm: ComponentInternalInstance,
childComponentName: string,
@@ -18,21 +30,53 @@ const getOrderedChildren = <T>(
return uids.map((uid) => children[uid]).filter((p) => !!p)
}
export const useOrderedChildren = <T extends { uid: number }>(
export const useOrderedChildren = <T extends ChildEssential>(
vm: ComponentInternalInstance,
childComponentName: string
) => {
const children = shallowRef<Record<number, T>>({})
const orderedChildren = shallowRef<T[]>([])
const nodesMap = new WeakMap<ParentNode, Node[]>()
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 = <T extends Node>(
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 = () => {