fix(form): 避免 Tooltip 无合法触发器时报 ElOnlyChild

对比模式删除按钮与表格未就绪时,ElTooltip 默认插槽会变成 Comment,触发 Element Plus 警告。
This commit is contained in:
roymondchen 2026-09-01 15:09:59 +08:00
parent a7999f50d5
commit 90e4bec70b
5 changed files with 56 additions and 7 deletions

View File

@ -1,5 +1,11 @@
<template>
<component class="tmagic-design-tooltip" :is="uiComponent" v-bind="uiProps">
<!-- ElTooltip ElOnlyChild 要求默认插槽至少有一个真实节点v-if false 时插槽是 Comment会报 no valid child node found -->
<component
v-if="isLegitTooltipTrigger($slots.default?.())"
class="tmagic-design-tooltip"
:is="uiComponent"
v-bind="uiProps"
>
<template #content>
<slot name="content"></slot>
</template>
@ -8,7 +14,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Comment, computed, Fragment, Text, type VNode } from 'vue';
import { getDesignConfig } from './config';
import type { TooltipProps } from './types';
@ -24,4 +30,30 @@ const ui = getDesignConfig('components')?.tooltip;
const uiComponent = ui?.component || 'el-tooltip';
const uiProps = computed<TooltipProps>(() => ui?.props(props) || props);
/** 与 Element Plus `findFirstLegitChild` 对齐Comment / 空文本不算合法 trigger。 */
const isLegitTooltipTrigger = (nodes: VNode[] | undefined): boolean => {
if (!nodes?.length) return false;
for (const child of nodes) {
if (!child) continue;
switch (child.type) {
case Comment:
continue;
case Text: {
const text = typeof child.children === 'string' ? child.children.trim() : '';
if (text) return true;
continue;
}
case Fragment:
if (isLegitTooltipTrigger(child.children as VNode[])) return true;
continue;
default:
return true;
}
}
return false;
};
</script>

View File

@ -80,9 +80,9 @@
</div>
</TMagicPopover>
<TMagicTooltip :content="`删除 ${title}`">
<!-- v-if 必须挂在 Tooltip 子节点为 false ElOnlyChild 会报 no valid child node found -->
<TMagicTooltip v-if="!isCompare" :content="`删除 ${title}`">
<TMagicButton
v-if="!isCompare"
v-show="showDelete"
size="default"
link

View File

@ -8,13 +8,14 @@
>
<div class="m-fields-table" :class="{ 'm-fields-table-item-extra': config.itemExtra }">
<span v-if="config.extra" style="color: rgba(0, 0, 0, 0.45)" v-html="config.extra"></span>
<!-- v-if 必须挂在 Tooltip 表格未就绪时 ElOnlyChild 会报 no valid child node found -->
<TMagicTooltip
v-if="model[modelName]"
content="拖拽可排序"
placement="left-start"
:disabled="config.dropSort !== true || config.dropSortHandle"
>
<TMagicTable
v-if="model[modelName]"
ref="tMagicTable"
style="width: 100%"
show-header

View File

@ -180,7 +180,8 @@ describe('GroupList container', () => {
expect(item.props('model')).toEqual({});
});
test('对比模式隐藏底部操作栏与复制/移动按钮', async () => {
test('对比模式隐藏底部操作栏与复制/移动/删除按钮,且不触发 ElOnlyChild 警告', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const wrapper = mountForm(
compareConfig,
{ list: [{ text: 'a' }, { text: 'b' }] },
@ -192,9 +193,14 @@ describe('GroupList container', () => {
await nextTick();
await nextTick();
expect(wrapper.find('.m-fields-group-list-footer').exists()).toBe(false);
expect(wrapper.find('.delete-button').exists()).toBe(false);
expect(wrapper.text()).not.toContain('复制');
expect(wrapper.text()).not.toContain('上移');
expect(wrapper.text()).not.toContain('下移');
expect(
warn.mock.calls.some((args) => args.some((arg) => String(arg?.message ?? arg).includes('ElOnlyChild'))),
).toBe(false);
warn.mockRestore();
});
});

View File

@ -3,7 +3,7 @@
*
* Copyright (C) 2025 Tencent.
*/
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import ElementPlus from 'element-plus';
@ -41,4 +41,14 @@ describe('Table container —— 对比模式', () => {
await nextTick();
expect(wrapper.text()).not.toContain('清空');
});
test('model 为空时不渲染表格,且不触发 ElOnlyChild 警告', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
mountTable({ model: { list: undefined } });
await nextTick();
expect(
warn.mock.calls.some((args) => args.some((arg) => String(arg?.message ?? arg).includes('ElOnlyChild'))),
).toBe(false);
warn.mockRestore();
});
});