fix(editor): 优化导航更多菜单宽度与溢出分割线

不传 popoverWidth 时按内容撑开,并将溢出项中的分割线改为横向且去掉首尾无意义分割线。
This commit is contained in:
roymondchen 2026-10-19 17:08:02 +08:00
parent 7f0e78bf6a
commit 8ac1a38601
3 changed files with 177 additions and 7 deletions

View File

@ -62,13 +62,14 @@ const props = withDefaults(
width?: number;
/** 子元素之间的间距,需与 SCSS gap 保持一致 */
gap?: number;
/** Popover 内容宽度 */
popoverWidth?: number;
/**
* Popover 内容宽度。不传时按内容撑开,避免下拉框超出菜单。
*/
popoverWidth?: number | string;
}>(),
{
items: () => [],
gap: 3,
popoverWidth: 180,
},
);
@ -110,7 +111,18 @@ const containerWidth = ref(0);
const hiddenIndexSet = ref<Set<number>>(new Set());
const hasOverflow = computed(() => hiddenIndexSet.value.size > 0);
const overflowItems = computed(() => props.items.filter((_, index) => hiddenIndexSet.value.has(index)));
// 更多菜单是纵向列表:分割线改成横线;收进去的首尾分割线没有分组意义,直接去掉。
const overflowItems = computed(() => {
const items = props.items
.filter((_, index) => hiddenIndexSet.value.has(index))
.map((item) => (item.type === 'divider' ? { ...item, direction: 'horizontal' as const } : item));
let start = 0;
let end = items.length;
while (start < end && items[start].type === 'divider') start += 1;
while (end > start && items[end - 1].type === 'divider') end -= 1;
return items.slice(start, end);
});
const measureAndCompute = () => {
if (!columnEl.value) return;

View File

@ -120,7 +120,12 @@
}
}
.m-editor-nav-menu-popover {
.tmagic-design-popper.m-editor-nav-menu-popover {
// 不固定宽度,按内容撑开,避免下拉框超出菜单;content-box 让 padding 不挤占内容宽度。
width: max-content;
min-width: 180px;
box-sizing: content-box;
.m-editor-nav-menu-overflow-list {
display: flex;
flex-direction: column;
@ -139,8 +144,30 @@
}
&.divider {
padding: 0;
display: flex;
align-items: center;
align-self: stretch;
width: auto;
height: auto;
padding: 4px 12px;
cursor: default;
box-sizing: border-box;
// 更多菜单是纵向列表,分割线必须盖过组件默认的竖线样式
.tmagic-design-divider,
.el-divider,
.t-divider {
display: block;
flex: 1 1 auto;
width: 100%;
min-width: 0;
height: 0;
margin: 0;
border: 0;
border-top: 1px solid var(--el-border-color, #dcdfe6);
border-left: 0;
position: static;
}
&:hover {
background-color: transparent;

View File

@ -21,7 +21,8 @@ vi.mock('@editor/components/ToolButton.vue', () => ({
'button',
{
ref: rootEl,
class: ['tool-btn', (props.data as any)?.className],
class: ['tool-btn', (props.data as any)?.className, (props.data as any)?.type],
'data-direction': (props.data as any)?.direction || '',
},
(props.data as any)?.text || '',
);
@ -119,6 +120,15 @@ describe('NavMenuColumn', () => {
expect(moreWrapper.classes()).toContain('m-editor-nav-menu-more-wrapper-hidden');
});
test('未指定 popoverWidth 时不给更多菜单固定宽度', () => {
const wrapper = mount(NavMenuColumn as any, {
props: { columnKey: 'left', items: [{ type: 'button', text: 'A' }] },
});
const popover = wrapper.findComponent({ name: 'TMagicPopover' });
expect(popover.exists()).toBe(true);
expect(popover.props('width')).toBeUndefined();
});
test('点击 more 引用元素切换 popover 显示状态', async () => {
const wrapper = mount(NavMenuColumn as any, {
props: { columnKey: 'left', items: [{ type: 'button', text: 'A' }] },
@ -344,6 +354,127 @@ describe('NavMenuColumn', () => {
wrapper.unmount();
});
test('更多菜单中的分割线改为横向,工具栏中的分割线保持竖向', async () => {
const items = [
{ type: 'button', text: 'A' },
{ type: 'button', text: 'B' },
{ type: 'divider', className: 'sep' },
{ type: 'button', text: 'C' },
];
const wrapper = mount(NavMenuColumn as any, {
props: { columnKey: 'right', items, width: 80 },
attachTo: document.body,
});
const columnEl = wrapper.find('.m-editor-nav-menu-column').element as HTMLElement;
Object.defineProperty(columnEl, 'clientWidth', { configurable: true, get: () => 80 });
const moreEl = wrapper.find('.m-editor-nav-menu-more-wrapper').element as HTMLElement;
moreEl.getBoundingClientRect = () => ({
width: 30,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
toJSON: () => ({}),
});
wrapper.findAll('.tool-btn').forEach((b) => {
const el = b.element as HTMLElement;
el.getBoundingClientRect = () => ({
width: 40,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
toJSON: () => ({}),
});
});
roCallbacks.forEach((cb) => cb());
await flushRaf();
await flushRaf();
const overflowDividers = wrapper.findAll('.m-editor-nav-menu-overflow-list .sep');
expect(overflowDividers.length).toBeGreaterThan(0);
overflowDividers.forEach((divider) => {
expect(divider.attributes('data-direction')).toBe('horizontal');
});
wrapper.findAll('.m-editor-nav-menu-column > .sep').forEach((divider) => {
expect(divider.attributes('data-direction')).toBe('');
});
wrapper.unmount();
});
test('更多菜单首尾的分割线会被去掉,中间的分割线保留', async () => {
const items = [
{ type: 'button', text: 'A' },
{ type: 'divider', className: 'lead' },
{ type: 'button', text: 'B' },
{ type: 'divider', className: 'mid' },
{ type: 'button', text: 'C' },
{ type: 'divider', className: 'trail' },
];
const wrapper = mount(NavMenuColumn as any, {
props: { columnKey: 'right', items, width: 80 },
attachTo: document.body,
});
const columnEl = wrapper.find('.m-editor-nav-menu-column').element as HTMLElement;
Object.defineProperty(columnEl, 'clientWidth', { configurable: true, get: () => 80 });
const moreEl = wrapper.find('.m-editor-nav-menu-more-wrapper').element as HTMLElement;
moreEl.getBoundingClientRect = () => ({
width: 30,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
toJSON: () => ({}),
});
wrapper.findAll('.tool-btn').forEach((b) => {
const el = b.element as HTMLElement;
el.getBoundingClientRect = () => ({
width: 40,
height: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
toJSON: () => ({}),
});
});
roCallbacks.forEach((cb) => cb());
await flushRaf();
await flushRaf();
const overflow = wrapper.find('.m-editor-nav-menu-overflow-list');
expect(overflow.find('.lead').exists()).toBe(false);
expect(overflow.find('.trail').exists()).toBe(false);
expect(
overflow.findAll('.tool-btn').map((button) => button.text() || button.classes().find((name) => name === 'mid')),
).toEqual(['B', 'mid', 'C']);
wrapper.unmount();
});
test('卸载时清理 ResizeObserver', () => {
const wrapper = mount(NavMenuColumn as any, {
props: { columnKey: 'left', items: [{ type: 'button', text: 'A' }] },