React Modal中使用Portal渲染Tooltip时点击外部关闭逻辑异常问题
问题原因
Tooltip通过Portal挂载到body元素下,不在Modal的DOM子树范围内,原有useClickAway的ref.current.contains(e.target)判断逻辑会将点击Tooltip的行为识别为Modal外部点击,触发关闭。
解决方案
方案1:扩展useClickAway支持豁免规则(通用场景优先)
修改useClickAway的入参,新增可选的豁免判断回调,适配Portal类组件的场景:
type UseClickAwayOptions = { condition: boolean; handler: (e: Event) => void; // 返回true时,当前点击不会触发关闭逻辑 shouldIgnore?: (e: Event) => boolean; } const useClickAway = ( ref: React.Ref<Node>, options: UseClickAwayOptions ): void => { const { condition, handler, shouldIgnore } = options; useEffect(() => { const listener = (e: Event) => { const target = e.target as Node; // 原有逻辑:点击在当前ref所属DOM内部,不触发关闭 if (!ref.current || ref.current.contains(target)) { return; } // 新增逻辑:命中豁免规则,不触发关闭 if (shouldIgnore?.(e)) { return; } handler(e); }; if (condition) { document.addEventListener('mouseup', listener); document.addEventListener('touchend', listener); } return () => { document.removeEventListener('mouseup', listener); document.removeEventListener('touchend', listener); }; }, [ref, condition, handler, shouldIgnore]); };
使用时给所有Modal内部渲染的Portal组件(比如Tooltip)的根节点添加自定义属性data-inner-portal="true",然后在Modal中调用useClickAway时添加豁免判断即可:
// Modal组件内部 const modalRef = useRef<HTMLDivElement>(null); const [isOpen, setIsOpen] = useState(false); useClickAway(modalRef, { condition: isOpen, handler: () => setIsOpen(false), shouldIgnore: (e) => { const target = e.target as HTMLElement; // 点击的元素属于内部Portal,豁免关闭 return !!target.closest('[data-inner-portal="true"]'); } })
该方案耦合度极低,不需要维护额外的Portal状态,后续新增的内部Portal类组件只要加上对应自定义属性即可自动适配。
方案2:支持多Ref校验(适合临时适配少量场景)
如果不想修改现有Portal组件的代码,可以改造useClickAway支持传入多个Ref,把打开的Tooltip的Ref也加入校验范围:
const useClickAway = ( refs: React.Ref<Node>[], condition: boolean, handler: (e: Event) => void ): void => { useEffect(() => { const listener = (e: Event) => { const target = e.target as Node; // 点击在任意一个Ref所属DOM内部,就不触发关闭 const isInside = refs.some(ref => ref.current?.contains(target)); if (isInside) return; handler(e); }; if (condition) { document.addEventListener('mouseup', listener); document.addEventListener('touchend', listener); } return () => { document.removeEventListener('mouseup', listener); document.removeEventListener('touchend', listener); }; }, [refs, condition, handler]); };
使用时把Modal的Ref和当前打开的Tooltip的Ref一起传入即可。
内容的提问来源于stack exchange,提问作者Dito
相关产品推荐
相关产品推荐

