import { useRef, useEffect } from "react"; /** * 提供一个 DOM,在 DOM 的外部点击之后,会触发回调函数 * @param domRefs 给定一个或多个 DOM 的 ref,在此 DOM 之外点击认为是外部点击 */ export function useOutsideClick( domRefs: React.RefObject | React.RefObject[], enabled = true ) { const timerRef = useRef(null); const eventRef = useRef(null); const reactEvtRef = useRef(null); const listenerRef = useRef(null); /** * 清理外部点击事件处理器 */ const remove = () => { listenerRef.current = null; }; /** * 注册外部点击事件处理器 * @param handle DOM 外部点击时的回调函数 */ const listen = (handle: (evt: MouseEvent) => void) => { listenerRef.current = (evt: MouseEvent) => { const refs = !Array.isArray(domRefs) ? [domRefs] : domRefs; for (const domRef of refs) { if (domRef.current && domRef.current.contains(evt.target as Node)) { return; } } // Portal 上模拟的冒泡事件比 DOM 事件晚 timerRef.current = setTimeout(() => { if (reactEvtRef.current !== evt) { handle(evt); } }, 0); }; }; // 在 unmount 之后,无论如何要清理 useEffect(() => { if (!enabled) { return () => null; } eventRef.current = evt => { if (listenerRef.current) { clearTimeout(timerRef.current); listenerRef.current(evt); } }; document.addEventListener("mousedown", eventRef.current); return () => { remove(); clearTimeout(timerRef.current); document.removeEventListener("mousedown", eventRef.current); }; }, [enabled]); return { listen, remove, // 需要忽略的组件要传入的 Props ignoreProps: { onMouseDown: (evt: React.MouseEvent) => { reactEvtRef.current = evt.nativeEvent; return evt; }, }, }; }