From 8e4391972b9017ae04e1a78808d0c23890fec07b Mon Sep 17 00:00:00 2001 From: roymondchen Date: Thu, 5 Nov 2026 13:52:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(design):=20=E4=BF=AE=E5=A4=8D=20Popconfirm?= =?UTF-8?q?=20=E6=B5=AE=E5=B1=82=E8=A2=AB=E8=A3=81=E5=89=AA=E5=8F=8A=20Ele?= =?UTF-8?q?ment=20Plus=20=E5=A4=8D=E5=90=88=E6=96=B9=E4=BD=8D=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/design/src/Popconfirm.vue | 6 +++- packages/element-plus-adapter/src/index.ts | 22 +++++++++++- packages/table/tests/popconfirm.spec.ts | 39 ++++++++++++++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/design/src/Popconfirm.vue b/packages/design/src/Popconfirm.vue index 9d4aaf5f..2634c118 100644 --- a/packages/design/src/Popconfirm.vue +++ b/packages/design/src/Popconfirm.vue @@ -24,7 +24,11 @@ defineOptions({ const emit = defineEmits(['confirm', 'cancel']); -const props = defineProps(); +// Boolean 类型的属性缺省时会被 Vue 转成 false,会把浮层强制内联渲染到侧栏等 overflow 容器中, +// 导致 placement 计算出的位置被裁剪。与 PopconfirmProps 注释及底层组件默认值保持一致。 +const props = withDefaults(defineProps(), { + teleported: true, +}); const ui = getDesignConfig('components')?.popconfirm; diff --git a/packages/element-plus-adapter/src/index.ts b/packages/element-plus-adapter/src/index.ts index a021fc86..425c9399 100644 --- a/packages/element-plus-adapter/src/index.ts +++ b/packages/element-plus-adapter/src/index.ts @@ -95,6 +95,26 @@ import type { import Table from './Table.vue'; +/** + * PopconfirmProps 的复合方位沿用 TDesign 命名(如 right-top), + * Element Plus 使用 popper.js 的 *-start / *-end 命名,未转换时这些取值会被 Popper 忽略。 + */ +const elementPlusPlacements: Record = { + 'top-left': 'top-start', + 'top-right': 'top-end', + 'bottom-left': 'bottom-start', + 'bottom-right': 'bottom-end', + 'left-top': 'left-start', + 'left-bottom': 'left-end', + 'right-top': 'right-start', + 'right-bottom': 'right-end', +}; + +const popconfirmProps = (props: PopconfirmProps): PopconfirmProps => { + const placement = props.placement ? elementPlusPlacements[props.placement] : undefined; + return placement ? { ...props, placement: placement as PopconfirmProps['placement'] } : props; +}; + const adapter: DesignPluginOptions = { adapterType: 'element-plus', useZIndex, @@ -329,7 +349,7 @@ const adapter: DesignPluginOptions = { popconfirm: { component: ElPopconfirm as any, - props: (props: PopconfirmProps) => props, + props: popconfirmProps, }, }, loading: ElLoading.directive, diff --git a/packages/table/tests/popconfirm.spec.ts b/packages/table/tests/popconfirm.spec.ts index 14004e02..17843a0e 100644 --- a/packages/table/tests/popconfirm.spec.ts +++ b/packages/table/tests/popconfirm.spec.ts @@ -18,7 +18,9 @@ import { describe, expect, test, vi } from 'vitest'; import { mount } from '@vue/test-utils'; -import { setDesignConfig } from '@tmagic/design'; +// 必须与 setDesignConfig 来自同一模块实例:从 @tmagic/design/src 导入 src/Popconfirm.vue 时, +// 它读取的是源码里独立的 config 模块,未注入 adapter 就会退化成原生的 标签。 +import { setDesignConfig, TMagicPopconfirm } from '@tmagic/design'; import elementPlusAdapter from '../../element-plus-adapter/src/index'; import ActionsColumn from '../src/ActionsColumn.vue'; @@ -101,8 +103,41 @@ describe('ActionsColumn popconfirm (real element-plus)', () => { (e.textContent || '').includes('确定删除?'), ); expect(popper).toBeTruthy(); - expect(popper.style.width).toBe('240px'); + expect(popper?.style.width).toBe('240px'); wrapper.unmount(); }); }); + +describe('TMagicPopconfirm placement(真实 element-plus)', () => { + const mountPopconfirm = (placement: string) => + mount(TMagicPopconfirm, { + props: { title: '确认内容', placement } as any, + slots: { reference: '' }, + attachTo: document.body, + }); + + test.each([ + ['right', 'right'], + ['right-top', 'right-start'], + ['top-left', 'top-start'], + ])('placement=%s 作用到 popper 的 %s', async (placement, expected) => { + const wrapper = mountPopconfirm(placement); + await wrapper.find('.ref').trigger('click'); + await vi.waitFor(() => { + expect(document.querySelector('.el-popper')?.getAttribute('data-popper-placement')).toBe(expected); + }); + wrapper.unmount(); + }); + + test('默认 teleported=true,浮层挂到 body 而非内联渲染', async () => { + const wrapper = mountPopconfirm('right'); + expect(wrapper.props('teleported')).toBe(true); + await wrapper.find('.ref').trigger('click'); + await vi.waitFor(() => { + expect(document.querySelector('.el-popper')).toBeTruthy(); + }); + expect(wrapper.element.contains(document.querySelector('.el-popper')!)).toBe(false); + wrapper.unmount(); + }); +});