gongxiangp2/components/ui/useAnchoredPopover.ts

35 lines
1.4 KiB
TypeScript

'use client'
import { useLayoutEffect, useState, type CSSProperties, type RefObject } from 'react'
/** Keep portalled controls inside the viewport, including within scrolling dialogs. */
export function useAnchoredPopover(open: boolean, anchor: RefObject<HTMLElement>, width?: number, height = 320) {
const [position, setPosition] = useState<CSSProperties>({ visibility: 'hidden' })
useLayoutEffect(() => {
if (!open) return
const update = () => {
if (!anchor.current) return
const rect = anchor.current.getBoundingClientRect()
const menuWidth = Math.min(width ?? rect.width, window.innerWidth - 24)
const below = window.innerHeight - rect.bottom - 20
const above = rect.top - 20
const flip = below < Math.min(height, 240) && above > below
setPosition({
position: 'fixed',
width: menuWidth,
left: Math.max(12, Math.min(rect.left, window.innerWidth - menuWidth - 12)),
...(flip ? { bottom: window.innerHeight - rect.top + 8 } : { top: rect.bottom + 8 }),
maxHeight: Math.max(120, Math.min(height, flip ? above : below)),
})
}
update()
window.addEventListener('resize', update)
window.addEventListener('scroll', update, true)
return () => {
window.removeEventListener('resize', update)
window.removeEventListener('scroll', update, true)
}
}, [open, anchor, width, height])
return position
}