35 lines
1.4 KiB
TypeScript
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
|
|
}
|