contextmenu 事件必须同步调用 event.preventdefault(),且需在监听器第一行无条件执行;菜单 dom 应挂载到 document.body,定位用 clientx/clienty 与 scroll 偏移计算;样式设为 position: absolute 并配高 z-index;需手动管理关闭逻辑、焦点及 esc/点击空白处响应;iframe 和 shadow dom 需单独监听;移动端应长按兜底。

contextmenu 事件必须同步调用 event.preventDefault()
不加这句,或延迟调用(比如包在 setTimeout、Promise.then 或条件分支里),原生右键菜单就会闪现甚至叠加显示。浏览器事件流一旦结束,再调用就无效了。
- 必须写在监听器函数第一行,无条件执行
- 多个监听器共存时,每个都要写,不能只在一个分支里写
- 别用
oncontextmenu="return false"这类内联写法——无法动态控制,也拦不住冒泡传播
菜单 DOM 必须挂到 document.body,不能插进目标元素内部
把菜单 <div class="menu"> 直接插入被右键点击的 <code><tr> 或 <code><div> 里,看似省事,但 React/Vue 重渲染、<code>innerHTML = 覆盖、父容器 display: none 切换,都会让菜单瞬间消失或错位。
- 统一用
document.body.appendChild(menu)挂载 - 定位用
event.clientX/event.clientY+window.scrollX/window.scrollY计算,不是pageX/pageY - 样式必须设
position: absolute(或fixed),并配足够高的z-index
菜单显示后必须手动管理关闭逻辑和焦点
浏览器不会帮你聚焦菜单、拦截 Tab 键,也不会自动关掉它。菜单弹出后,若不主动处理,用户点完一项菜单还留着,ESC 不响应,点空白处也不消失。
- 监听
document.addEventListener('click', hideMenu),并在菜单项click中调用event.stopPropagation() - 监听
document.addEventListener('keydown', e => e.key === 'Escape' && hideMenu()) - 菜单容器需有
tabindex="0",显示后立即调用menu.focus() - 每次打开前先
remove()上一个实例,避免内存泄漏和重复渲染
iframe 和 Shadow DOM 需单独监听,不能指望事件穿透
右键发生在 iframe 内部?主页面绑的 contextmenu 完全收不到。Shadow DOM(尤其是 mode: "closed")同样隔离事件,父级监听器形同虚设。
- 对 iframe,要在其
contentDocument上重新绑定监听器 - 对自定义元素,监听要注册到
shadowRoot,而不是宿主元素 - 移动端(尤其 iOS Safari)基本不触发
contextmenu,长按模拟是更靠谱的兜底方案











