|
|
"use client"; |
|
|
import { |
|
|
useState, |
|
|
useRef, |
|
|
useEffect, |
|
|
type RefObject, |
|
|
type ReactNode, |
|
|
} from "react"; |
|
|
|
|
|
type FloatingMenuProps = { |
|
|
targetRef: RefObject<HTMLDivElement | null>; |
|
|
children: ReactNode; |
|
|
|
|
|
fixedTopOffset?: number; |
|
|
fixedRightOffset?: number; |
|
|
}; |
|
|
|
|
|
function FloatingMenu({ |
|
|
targetRef, |
|
|
children, |
|
|
fixedTopOffset = 10, |
|
|
fixedRightOffset = 0, |
|
|
}: FloatingMenuProps) { |
|
|
const [isFixed, setIsFixed] = useState(false); |
|
|
const [fixedTop, setFixedTop] = useState<number>(0); |
|
|
const [fixedRight, setFixedRight] = useState<number>(0); |
|
|
const menuRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
const handleScroll = () => { |
|
|
if (!targetRef.current || !menuRef.current) return; |
|
|
|
|
|
const targetRect = targetRef.current.getBoundingClientRect(); |
|
|
|
|
|
|
|
|
if (targetRect.bottom < 0) { |
|
|
|
|
|
setIsFixed(false); |
|
|
setFixedTop(0); |
|
|
setFixedRight(0); |
|
|
} |
|
|
|
|
|
else if (targetRect.top <= 0) { |
|
|
|
|
|
setIsFixed(true); |
|
|
|
|
|
setFixedTop(fixedTopOffset); |
|
|
|
|
|
setFixedRight(targetRect.right - targetRect.width); |
|
|
} else { |
|
|
|
|
|
setIsFixed(false); |
|
|
setFixedTop(0); |
|
|
setFixedRight(0); |
|
|
} |
|
|
}; |
|
|
|
|
|
window.addEventListener("scroll", handleScroll); |
|
|
|
|
|
handleScroll(); |
|
|
|
|
|
return () => { |
|
|
window.removeEventListener("scroll", handleScroll); |
|
|
}; |
|
|
}, [targetRef, fixedTopOffset]); |
|
|
|
|
|
return ( |
|
|
<div |
|
|
ref={menuRef} |
|
|
style={{ |
|
|
position: isFixed ? "fixed" : "absolute", |
|
|
top: isFixed ? fixedTop : 0, |
|
|
right: fixedRightOffset + (isFixed ? fixedRight : 0), |
|
|
// Make sure the menu is above the content |
|
|
zIndex: 30, |
|
|
}} |
|
|
> |
|
|
{children} |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export default FloatingMenu; |
|
|
|