mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-23 22:48:14 +00:00
fix(editor): 右键菜单动态变化后修复菜单位置防止截断
This commit is contained in:
parent
d9d2f8f04c
commit
6919a0a0b1
@ -128,20 +128,42 @@ const outsideClickHideHandler = (e: MouseEvent) => {
|
||||
hide();
|
||||
};
|
||||
|
||||
const setPosition = (e: { clientY: number; clientX: number }) => {
|
||||
const menuHeight = menuEl.value?.clientHeight || 0;
|
||||
// 根据菜单实际尺寸修正位置,避免超出可视范围
|
||||
const fixPosition = () => {
|
||||
const menu = menuEl.value;
|
||||
if (!menu || !visible.value) return;
|
||||
|
||||
let top = e.clientY;
|
||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||
top = document.body.clientHeight - menuHeight;
|
||||
const menuHeight = menu.clientHeight;
|
||||
const menuWidth = menu.clientWidth;
|
||||
|
||||
let { top, left } = menuPosition.value;
|
||||
|
||||
if (top + menuHeight > document.body.clientHeight) {
|
||||
top = Math.max(0, document.body.clientHeight - menuHeight);
|
||||
}
|
||||
|
||||
if (left + menuWidth > document.body.clientWidth) {
|
||||
left = Math.max(0, document.body.clientWidth - menuWidth);
|
||||
}
|
||||
|
||||
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
|
||||
menuPosition.value = { top, left };
|
||||
}
|
||||
};
|
||||
|
||||
const setPosition = (e: { clientY: number; clientX: number }) => {
|
||||
menuPosition.value = {
|
||||
top,
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
};
|
||||
fixPosition();
|
||||
};
|
||||
|
||||
// 菜单大小动态变化(如菜单项更新)后重新修正位置
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fixPosition();
|
||||
});
|
||||
|
||||
const show = (e?: { clientY: number; clientX: number }) => {
|
||||
visible.value = true;
|
||||
|
||||
@ -186,12 +208,18 @@ const mouseenterHandler = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (menuEl.value) {
|
||||
resizeObserver.observe(menuEl.value);
|
||||
}
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.disconnect();
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);
|
||||
|
||||
@ -4,10 +4,27 @@
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
|
||||
const roCallbacks: Array<() => void> = [];
|
||||
class FakeResizeObserver {
|
||||
public static disconnectSpy = vi.fn();
|
||||
public cb: () => void;
|
||||
constructor(cb: () => void) {
|
||||
this.cb = cb;
|
||||
roCallbacks.push(cb);
|
||||
}
|
||||
public observe() {}
|
||||
public unobserve() {}
|
||||
public disconnect() {
|
||||
FakeResizeObserver.disconnectSpy();
|
||||
}
|
||||
}
|
||||
(globalThis as any).ResizeObserver = FakeResizeObserver;
|
||||
|
||||
const provideServices = () => ({
|
||||
global: {
|
||||
provide: {
|
||||
@ -22,6 +39,8 @@ const provideServices = () => ({
|
||||
describe('ContentMenu.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
roCallbacks.length = 0;
|
||||
FakeResizeObserver.disconnectSpy.mockClear();
|
||||
});
|
||||
|
||||
test('show 后触发 show 事件', async () => {
|
||||
@ -67,6 +86,62 @@ describe('ContentMenu.vue', () => {
|
||||
expect((wrapper.vm as any).menuPosition.top).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
test('菜单大小动态变化后修正位置避免超出可视范围', async () => {
|
||||
// jsdom 中 clientWidth/clientHeight 默认为 0,先 mock 一个足够大的视口,避免 show 时位置被修正
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 1000, configurable: true });
|
||||
Object.defineProperty(document.body, 'clientWidth', { value: 1000, configurable: true });
|
||||
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
|
||||
attachTo: document.body,
|
||||
});
|
||||
(wrapper.vm as any).show({ clientX: 90, clientY: 90 });
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(90);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(90);
|
||||
|
||||
// 视口缩小为 100x100
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
|
||||
Object.defineProperty(document.body, 'clientWidth', { value: 100, configurable: true });
|
||||
|
||||
const menuEl = wrapper.find('.magic-editor-content-menu').element as HTMLElement;
|
||||
// 模拟菜单内容增多后尺寸变为 50x50
|
||||
Object.defineProperty(menuEl, 'clientHeight', { value: 50, configurable: true });
|
||||
Object.defineProperty(menuEl, 'clientWidth', { value: 50, configurable: true });
|
||||
|
||||
roCallbacks.forEach((cb) => cb());
|
||||
await nextTick();
|
||||
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(50);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(50);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('菜单未显示时尺寸变化不修正位置', async () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
|
||||
});
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
|
||||
|
||||
roCallbacks.forEach((cb) => cb());
|
||||
await nextTick();
|
||||
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(0);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(0);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('卸载时断开 ResizeObserver', () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [] as any },
|
||||
});
|
||||
wrapper.unmount();
|
||||
expect(FakeResizeObserver.disconnectSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('contains 判断 DOM 是否在菜单内部', async () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user