fix: use bottom positioning for notification dropdown

Instead of measuring panel height with unreliable RAF timing, use
CSS bottom positioning when the button is in the lower half of the
viewport. The dropdown grows upward, no measurement needed.
This commit is contained in:
Usman Baig
2026-03-19 00:54:50 +01:00
parent a31f183b7b
commit dd76aed157

View File

@@ -56,17 +56,17 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null)
const panelRef = useRef<HTMLDivElement>(null) const panelRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) const [fixedPos, setFixedPos] = useState<{ left: number; top?: number; bottom?: number } | null>(null)
const updatePosition = useCallback(() => { const updatePosition = useCallback(() => {
if (anchor === 'right' && buttonRef.current) { if (anchor === 'right' && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect() const rect = buttonRef.current.getBoundingClientRect()
let top = rect.top const left = rect.right + 8
if (panelRef.current) { if (rect.top > window.innerHeight / 2) {
const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 setFixedPos({ left, bottom: window.innerHeight - rect.bottom })
top = Math.min(top, Math.max(8, maxTop)) } else {
setFixedPos({ left, top: rect.top })
} }
setFixedPos({ left: rect.right + 8, top })
} }
}, [anchor]) }, [anchor])
@@ -100,17 +100,9 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
if (open) { if (open) {
fetchNotifications() fetchNotifications()
updatePosition() updatePosition()
requestAnimationFrame(() => updatePosition())
} }
}, [open, updatePosition]) }, [open, updatePosition])
// Recalculate position after content changes (notifications load, loading state)
useEffect(() => {
if (open && anchor === 'right') {
requestAnimationFrame(() => updatePosition())
}
}, [notifications, loading, open, anchor, updatePosition])
// * Poll unread count in background (when authenticated) // * Poll unread count in background (when authenticated)
useEffect(() => { useEffect(() => {
fetchUnreadCount() fetchUnreadCount()
@@ -214,10 +206,10 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
aria-label="Notifications" aria-label="Notifications"
className={`bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100] ${ className={`bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100] ${
anchor === 'right' anchor === 'right'
? 'fixed w-96 origin-top-left' ? `fixed w-96 ${fixedPos?.bottom !== undefined ? 'origin-bottom-left' : 'origin-top-left'}`
: 'fixed left-4 right-4 top-16 sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96' : 'fixed left-4 right-4 top-16 sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96'
}`} }`}
style={anchor === 'right' && fixedPos ? { left: fixedPos.left, top: fixedPos.top } : undefined} style={anchor === 'right' && fixedPos ? { left: fixedPos.left, top: fixedPos.top, bottom: fixedPos.bottom } : undefined}
> >
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700"> <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3> <h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3>