diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index e8b8ee68ff1c4..de585ffce8f56 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -1,6 +1,23 @@ ## Changelog +### 2.14.6 + +_2026-09-18_ + +#### Features + +- Components [text] add `isTruncated` expose (#24822 by @rzzf) + +#### Bug fixes + +- Components [table] defer multiple filters until confirm (#24749 by @william-xue) +- Components [form] avoid warning for zero label width (#24820 by @wjp980108) +- Components [select/v2] avoid circular tag width calculation (#24743 by @rzzf) +- Components [upload] preserve dropped directory paths (#24838 by @rzzf) +- Components prevent duplicate keyboard handling in dates picker (#24860 by @ixyzorg) +- Components [loading] memory leak (#24865 by @btea) + ### 2.14.5 _2026-08-21_ diff --git a/packages/components/loading/__tests__/loading.test.tsx b/packages/components/loading/__tests__/loading.test.tsx index 67bdaeb82336e..db34250977f58 100644 --- a/packages/components/loading/__tests__/loading.test.tsx +++ b/packages/components/loading/__tests__/loading.test.tsx @@ -191,6 +191,27 @@ describe('Loading', () => { expect(loadingInstance.visible.value).toBeFalsy() }) + test('close service should release detached dom', async () => { + loadingInstance = Loading() + const mask = document.querySelector('.el-loading-mask') as HTMLElement + expect(mask).toBeTruthy() + + vi.useFakeTimers() + loadingInstance.close() + vi.runAllTimers() + vi.useRealTimers() + await nextTick() + + // the mask is removed from the document ... + expect(mask.parentNode).toBeNull() + // ... and no reference on the instance keeps it alive anymore + expect(loadingInstance.$el).toBeNull() + const internalInstance = loadingInstance.vm.$ + expect(internalInstance.vnode.el).toBeNull() + expect(internalInstance.subTree).toBeNull() + expect(internalInstance.appContext.app._container).toBeNull() + }) + test('target service', async () => { const container = document.createElement('div') container.className = 'loading-container' diff --git a/packages/components/loading/src/loading.ts b/packages/components/loading/src/loading.ts index 074de9b44a1b1..15c8d1642ace6 100644 --- a/packages/components/loading/src/loading.ts +++ b/packages/components/loading/src/loading.ts @@ -23,6 +23,7 @@ export function createLoadingComponent( appContext: AppContext | null ) { let afterLeaveTimer: ReturnType + let destroyed = false // IMPORTANT NOTE: this is only a hacking way to expose the injections on an // instance, DO NOT FOLLOW this pattern in your own code. const afterLeaveFlag = ref(false) @@ -38,8 +39,12 @@ export function createLoadingComponent( } function destroySelf() { + if (destroyed) return + destroyed = true const target = data.parent - const ns = (vm as any).ns as UseNamespaceReturn + // Compatible with the instance data format of vue@3.2.12 and earlier versions #12351 + const ns = + ((vm as any).ns as UseNamespaceReturn) || (vm as any)._.exposed.ns if (!target.vLoadingAddClassList) { let loadingNumber: number | string | null = target.getAttribute('loading-number') @@ -54,6 +59,11 @@ export function createLoadingComponent( } removeElLoadingChild() loadingInstance.unmount() + + const internalInstance = vm.$ as any + internalInstance.vnode.el = null + internalInstance.subTree = null + loadingInstance._container = null } function removeElLoadingChild(): void { vm.$el?.parentNode?.removeChild(vm.$el) @@ -165,8 +175,8 @@ export function createLoadingComponent( close, handleAfterLeave, vm, - get $el(): HTMLElement { - return vm.$el + get $el(): HTMLElement | null { + return destroyed ? null : vm.$el }, } } diff --git a/packages/components/loading/src/service.ts b/packages/components/loading/src/service.ts index 89cc3b9812c23..45997ca550e03 100644 --- a/packages/components/loading/src/service.ts +++ b/packages/components/loading/src/service.ts @@ -63,7 +63,7 @@ const Loading = function ( } resolved.parent.setAttribute('loading-number', loadingNumber) - resolved.parent.appendChild(instance.$el) + resolved.parent.appendChild(instance.$el!) // after instance render, then modify visible to trigger transition nextTick(() => (instance.visible.value = resolved.visible)) @@ -140,7 +140,7 @@ const addStyle = async ( instance.originalPosition.value = getStyle(parent, 'position') } for (const [key, value] of Object.entries(maskStyle)) { - instance.$el.style[key] = value + instance.$el!.style[key] = value } }