新闻详情

新闻详情

首页 / 资讯中心 / 详情

React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理

发布时间:2026/9/1 11:55:34来源:尧图网络
React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理
React createPortal 实战:模态框逃出 overflow:hidden、事件冒泡与焦点管理写模态框(Modal)时你大概率遇到过这个诡异现象:弹窗明明该盖住全屏,结果只在某个卡片内部显示一小块,还被父容器的滚动条截断了。加z-index: 9999也没用。问题不在层级,在于 DOM 结构。这篇用createPortal把这个坑填平,顺带讲清楚 Portal 里事件冒泡和焦点管理这两个真正容易翻车的地方。先复现:被父容器裁掉的弹窗假设有个卡片列表,每张卡片里放一个「查看详情」的弹窗:function Card() { const [open, setOpen] useState(false); return ( div classNamecard style{{ overflow: hidden, position: relative }} button onClick{() setOpen(true)}查看详情/button {open ( div classNamemodal-mask div classNamemodal这里是详情内容/div /div )} /div ); }你期望.modal-mask铺满屏幕,但实际它是.card的子节点。父级的overflow: hidden会把溢出部分裁掉,position: relative又让absolute/fixed定位的基准变成了这张卡片而不是视口。于是弹窗被困在卡片里。治标的办法是去掉父级的overflow和transform,但这些样式往往是布局必需的,牵一发动全身。真正的解法是让弹窗的 DOM 节点根本不待在卡片里。createPortal:渲染到别处,状态还在这里createPortal(children, container)做的事很简单:把children渲染到你指定的 DOM 节点里,而不是当前组件的位置。但这个组件在 React 树里的逻辑位置不变——props、state、context 全都照常从父组件流下来。import { createPortal } from react-dom; function Modal({ children, onClose }) { // document.body 不受任何卡片的 overflow/transform 影响 return createPortal( div classNamemodal-mask onClick{onClose} div classNamemodal onClick{(e) e.stopPropagation()} {children} /div /div, document.body ); }改造后的卡片:function Card() { const [open, setOpen] useState(false); return ( div classNamecard style{{ overflow: hidden }} button onClick{() setOpen(true)}查看详情/button {open Modal onClose{() setOpen(false)}这里是详情内容/Modal} /div ); }现在.modal-mask是document.body的直接子节点,position: fixed相对视口定位,不再被任何卡片裁剪。而open这个 state 依然住在Card里,onClose也是Card传下去的——逻辑归属和 DOM 归属彻底解耦。配套的 CSS:.modal-mask{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;}.modal{background:#fff;padding:24px;border-radius:8px;min-width:320px;}坑一:事件冒泡走的是 React 树,不是 DOM 树这是 Portal 最反直觉的地方。弹窗的 DOM 节点在body下,但 React 的合成事件冒泡,走的是组件树而不是真实 DOM 树。也就是说,如果Card外面还包了个onClick,点击 Portal 里的按钮,事件依然会冒泡到那个外层onClick:function Panel() { return ( // 点弹窗里的按钮,这个 onClick 也会触发 div onClick{() console.log(panel clicked)} Card / /div ); }这经常导致「点弹窗内部,结果触发了背后面板的点击逻辑」。记住:Portal 里的 React 事件按组件层级冒泡。要拦就在合适的层级e.stopPropagation(),别指望 DOM 结构帮你隔离。上面Modal里给.modal加onClick{(e) e.stopPropagation()},就是防止点击弹窗内容时冒泡到.modal-mask触发onClose——这一步利用的正是「按组件树冒泡」的特性。坑二:焦点管理,别让键盘用户困在弹窗外弹窗打开后,键盘 Tab 焦点应该被「关」在弹窗内,关闭后焦点要还给触发它的按钮。很多手写模态框忽略这点,导致视障用户用起来完全乱套。至少要做三件事:打开时聚焦到弹窗、Esc 关闭、关闭后焦点归还。function Modal({ children, onClose }) { const ref useRef(null); useEffect(() { // 记住打开弹窗前的焦点元素,关闭时好还回去 const prev document.activeElement; ref.current?.focus(); const onKey (e) { if (e.key Escape) onClose(); }; document.addEventListener(keydown, onKey); return () { document.removeEventListener(keydown, onKey); // 归还焦点,否则焦点会掉到 body 上,键盘用户彻底迷路 prev instanceof HTMLElement prev.focus(); }; }, [onClose]); return createPortal( div classNamemodal-mask onClick{onClose} div classNamemodal ref{ref} tabIndex{-1} // 让这个 div 能接收 focus roledialog aria-modaltrue onClick{(e) e.stopPropagation()} {children} /div /div, document.body ); }tabIndex{-1}让.modal可以被.focus()聚焦但不进入 Tab 序列;roledialog和aria-modaltrue让屏幕阅读器知道这是个模态对话框。坑三:body 滚动穿透弹窗打开时,如果背景页面还能滚,移动端体验会很糟(滑弹窗结果背景在动)。打开时锁住 body 滚动:useEffect(() { const original document.body.style.overflow; document.body.style.overflow hidden; return () { document.body.style.overflow original; // 恢复原值,别写死成 }; }, []);注意恢复时用保存的original而不是硬写。如果同时开了多个需要锁滚动的组件,写死会导致先关的那个把滚动提前放开。生产项目里更稳的做法是用计数器管理,但单弹窗场景保存原值就够了。进阶:挂到专用容器而不是 body直接挂document.body在多数场景够用,但如果项目里有多个弹窗层级(弹窗里再弹确认框),或者要控制它们的 DOM 顺序,建议挂到一个专用的根节点:function usePortalContainer() { const [el] useState(() { const div document.createElement(div); div.id portal-root; return div; }); useEffect(() { document.body.appendChild(el); return () el.remove(); // 组件卸载时清掉,别留一堆空 div }, [el]); return el; }用useState的惰性初始化保证div只创建一次(直接useRef(document.createElement(...))也行,但每次渲染都会执行createElement,虽然结果被丢弃仍是浪费)。返回的el传给createPortal第二个参数即可。小结Portal 的价值:把弹窗 DOM 渲染到body,逃出父级overflow: hidden/position/transform的束缚,状态和 props 仍从原组件流转。事件冒泡按组件树走,不按 DOM 树——想隔离必须显式stopPropagation,别靠 DOM 结构。无障碍三件套:打开聚焦弹窗、Esc 关闭、关闭归还焦点,配roledialogaria-modal。锁 body 滚动记得保存并恢复原值,别硬写空字符串。一句话记忆:createPortal只搬 DOM 不搬逻辑——DOM 挂到 body,事件和状态还在原地。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

JWT的签名算法有哪些? 2026/9/1 11:55:25

JWT的签名算法有哪些?

JWT(JSON Web Token)的签名算法主要有几种,其中最常见的是HMAC(Hash-based Message Authentication Code)算法族和RSA(Rivest–Shamir–Adleman)或ECDSA(Elliptic Curve Digital Sig…

阅读更多 →
如何验证JWT的签名?底层原理是什么? 2026/9/1 11:55:25

如何验证JWT的签名?底层原理是什么?

JWT(JSON Web Token)的签名验证是确保JWT的真实性和未被篡改的重要步骤。签名是JWT的一部分,通常位于JWT的最后一段。签名的目的是确保JWT的Payload(载荷)数据在传输过程中没有被修改过。底层原理:JWT通常由…

阅读更多 →
JWT的“jti”声明有什么作用? 2026/9/1 11:55:25

JWT的“jti”声明有什么作用?

JWT的“jti”声明代表“JWT ID”,它是一个唯一标识符,用于标识每一个JWT。这个声明不是强制性的,但如果使用了,它可以提供额外的安全性和可追踪性。作用:防止重放攻击:通过记录已经使用过的“jti”&#xf…

阅读更多 →
ReasonCode客户端实战:基于ReasonixGUI搭建DeepSeek Harness工作流 2026/9/1 11:55:25

ReasonCode客户端实战:基于ReasonixGUI搭建DeepSeek Harness工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
晶圆级架构如何破解大模型推理的“内存墙”瓶颈 2026/9/1 11:55:25

晶圆级架构如何破解大模型推理的“内存墙”瓶颈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Agentic AI验证框架:从规则引擎到可观测性的Python实现 2026/9/1 11:52:25

Agentic AI验证框架:从规则引擎到可观测性的Python实现

当把带工具调用的 Agent 从 Demo 推向生产环境时,会遇到一类非常现实的问题:模型明明只有两个内部工具可用,某些 Prompt 下却会拼接出不存在的工具名;工具已经返回了结构化结果,模型却没有基于返回值作答,而…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞