contextmenu事件需第一行同步调用e.preventdefault(),中性元素须加tabindex="0",菜单dom挂document.body并用clientx/clienty+滚动偏移定位,样式用position:fixed+z-index:9999,关闭逻辑覆盖esc、外部点击及iframe/shadow dom边界,且每次showmenu前必须remove旧菜单。

contextmenu事件必须在第一行同步调用e.preventDefault()
不写这句,或者写晚了(比如包在if里、塞进setTimeout或Promise.then),原生菜单就会闪一下甚至叠加显示。浏览器事件流结束前没调用,就彻底失效。
常见错误写法:if (e.target.matches('.file-item')) { e.preventDefault(); }——其他区域右键照样弹系统菜单。
正确做法:不管是否要显示菜单,先无条件执行:
element.addEventListener('contextmenu', (e) => {
e.preventDefault(); // 必须是第一行
if (!e.target.matches('[data-menu-target]')) return;
showMenu(e.clientX, e.clientY);
});
中性元素(div、span)必须加tabindex="0"才可靠触发
Safari(尤其 iOS)和旧版 Edge 默认不给div、span派发contextmenu事件,这不是 bug,是规范行为。不加tabindex="0",右键就静默。
- 加了
tabindex="0"后,元素可聚焦、可捕获上下文事件,且不影响视觉 - 别只绑在
document上指望全局生效——目标元素本身得“能触发” -
input、button等可交互元素自带触发能力,无需额外处理
菜单 DOM 必须挂到document.body,定位用clientX/clientY + window.scrollX/window.scrollY
写死在 HTML 里的<div class="menu">或插在目标元素内部,会受<code>overflow: hidden、transform、框架重渲染影响,位置错乱或直接消失。
定位关键点:
- 用
e.clientX和e.clientY(不是pageX/pageY),它们是视口坐标,更稳定 - 加上
window.scrollX和window.scrollY补偿滚动偏移 - 菜单样式设
position: fixed比absolute更稳妥,避免父级transform创建新定位上下文 - 必须设
z-index: 9999,否则可能被弹窗、广告遮挡
关闭逻辑不能只靠点击菜单项,必须覆盖ESC、外部点击、iframe/Shadow DOM边界
用户点完“复制”菜单还挂着,再右键就叠两层——这是最常见的残留问题。
完整关闭链路要包括:
- 每个菜单项
click回调末尾调closeMenu() - 监听
document的keydown,判断e.key === 'Escape' - 监听
document的mousedown,用e.target.closest('.custom-menu')排除菜单自身和触发源 -
iframe内右键需单独对其contentDocument绑定相同逻辑 - Shadow DOM 内部(如 Web Component)要在
shadowRoot上监听,而非宿主元素
真正容易被忽略的是:每次showMenu()前必须先remove()旧菜单节点,否则 DOM 泄漏+事件重复绑定,开三次菜单就绑三套 ESC 监听器。











