修复
This commit is contained in:
110
src/components/AnchoredMenu.tsx
Normal file
110
src/components/AnchoredMenu.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useLayoutEffect, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const MENU_GAP = 4;
|
||||
const VIEWPORT_PAD = 8;
|
||||
|
||||
function useAnchoredMenuPosition(
|
||||
open: boolean,
|
||||
anchorRef: React.RefObject<HTMLElement | null>,
|
||||
menuRef: React.RefObject<HTMLElement | null>
|
||||
) {
|
||||
const [coords, setCoords] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
setCoords(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const anchor = anchorRef.current;
|
||||
const menu = menuRef.current;
|
||||
if (!anchor || !menu) return;
|
||||
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const menuHeight = menu.offsetHeight;
|
||||
|
||||
let top = anchorRect.bottom + MENU_GAP;
|
||||
if (top + menuHeight > window.innerHeight - VIEWPORT_PAD) {
|
||||
top = anchorRect.top - menuHeight - MENU_GAP;
|
||||
}
|
||||
top = Math.max(
|
||||
VIEWPORT_PAD,
|
||||
Math.min(top, window.innerHeight - menuHeight - VIEWPORT_PAD)
|
||||
);
|
||||
|
||||
let left = anchorRect.right - menuWidth;
|
||||
left = Math.max(
|
||||
VIEWPORT_PAD,
|
||||
Math.min(left, window.innerWidth - menuWidth - VIEWPORT_PAD)
|
||||
);
|
||||
|
||||
setCoords({ top, left });
|
||||
};
|
||||
|
||||
update();
|
||||
const raf = requestAnimationFrame(update);
|
||||
window.addEventListener('resize', update);
|
||||
window.addEventListener('scroll', update, true);
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('scroll', update, true);
|
||||
};
|
||||
}, [open, anchorRef, menuRef]);
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
interface AnchoredMenuProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
anchorRef: React.RefObject<HTMLElement | null>;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnchoredMenu: React.FC<AnchoredMenuProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
anchorRef,
|
||||
children,
|
||||
className = '',
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const coords = useAnchoredMenuPosition(open, anchorRef, menuRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (menuRef.current?.contains(target) || anchorRef.current?.contains(target)) return;
|
||||
onClose();
|
||||
};
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
||||
}, [open, onClose, anchorRef]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
role="menu"
|
||||
className={className}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: coords?.top ?? 0,
|
||||
left: coords?.left ?? 0,
|
||||
visibility: coords ? 'visible' : 'hidden',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user