diff --git a/packages/editor/src/layouts/NavMenuColumn.vue b/packages/editor/src/layouts/NavMenuColumn.vue index 390d0aca..07a131f3 100644 --- a/packages/editor/src/layouts/NavMenuColumn.vue +++ b/packages/editor/src/layouts/NavMenuColumn.vue @@ -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>(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; diff --git a/packages/editor/src/theme/nav-menu.scss b/packages/editor/src/theme/nav-menu.scss index e1fa862e..6545ab29 100644 --- a/packages/editor/src/theme/nav-menu.scss +++ b/packages/editor/src/theme/nav-menu.scss @@ -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; diff --git a/packages/editor/tests/unit/layouts/NavMenuColumn.spec.ts b/packages/editor/tests/unit/layouts/NavMenuColumn.spec.ts index 497ef6ce..68681124 100644 --- a/packages/editor/tests/unit/layouts/NavMenuColumn.spec.ts +++ b/packages/editor/tests/unit/layouts/NavMenuColumn.spec.ts @@ -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' }] },