From cc25790c81a0e2d3c31f629fef0f61e7790ea55f Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 6 May 2026 07:42:53 -0400 Subject: [PATCH] Add mobile drawer support to notifications popover (#12276) * fix: Open notifications in a bottom drawer on mobile Match the mobile context menu pattern by rendering the notifications panel as a Vaul bottom drawer below the tablet breakpoint, while keeping the existing Radix popover on desktop. * fix: Notification drawer opens at correct height on mobile Skip the height animation while bounds is unmeasured to avoid a feedback loop between framer-motion's animation toward 0 and the ResizeObserver re-targeting it. Eagerly import Notifications so first paint has real content for the initial measurement, and bump its minHeight to 75vh on mobile to match other bottom drawers. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude --- .../Notifications/Notifications.tsx | 6 +- .../Notifications/NotificationsPopover.tsx | 56 +++++++++++++++---- app/components/primitives/Drawer.tsx | 12 ++-- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/app/components/Notifications/Notifications.tsx b/app/components/Notifications/Notifications.tsx index 63a2f4c537..76ac45f556 100644 --- a/app/components/Notifications/Notifications.tsx +++ b/app/components/Notifications/Notifications.tsx @@ -6,6 +6,7 @@ import styled from "styled-components"; import { s, hover } from "@shared/styles"; import Notification, { type NotificationFilter } from "~/models/Notification"; import { markNotificationsAsRead } from "~/actions/definitions/notifications"; +import useMobile from "~/hooks/useMobile"; import useStores from "~/hooks/useStores"; import NotificationMenu from "~/menus/NotificationMenu"; import Empty from "../Empty"; @@ -83,6 +84,7 @@ function Notifications( ) { const { notifications } = useStores(); const { t } = useTranslation(); + const isMobile = useMobile(); const [filter, setFilter] = React.useState("all"); const filterOptions = React.useMemo( @@ -110,9 +112,9 @@ function Notifications( diff --git a/app/components/Notifications/NotificationsPopover.tsx b/app/components/Notifications/NotificationsPopover.tsx index d2f8b5b499..da0ea18a9f 100644 --- a/app/components/Notifications/NotificationsPopover.tsx +++ b/app/components/Notifications/NotificationsPopover.tsx @@ -1,15 +1,20 @@ import { observer } from "mobx-react"; -import { Suspense, useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { + Drawer, + DrawerContent, + DrawerTitle, + DrawerTrigger, +} from "~/components/primitives/Drawer"; import { Popover, PopoverTrigger, PopoverContent, } from "~/components/primitives/Popover"; +import useMobile from "~/hooks/useMobile"; import useStores from "~/hooks/useStores"; -import lazyWithRetry from "~/utils/lazyWithRetry"; - -const Notifications = lazyWithRetry(() => import("./Notifications")); +import Notifications from "./Notifications"; type Props = { children?: React.ReactNode; @@ -19,7 +24,9 @@ const NotificationsPopover: React.FC = ({ children }: Props) => { const { t } = useTranslation(); const { notifications } = useStores(); const [open, setOpen] = useState(false); + const isMobile = useMobile(); const scrollableRef = useRef(null); + const drawerContentRef = useRef>(null); useEffect(() => { void notifications.fetchPage({ archived: false }); @@ -40,6 +47,40 @@ const NotificationsPopover: React.FC = ({ children }: Props) => { } }, []); + const enablePointerEvents = useCallback(() => { + if (drawerContentRef.current) { + drawerContentRef.current.style.pointerEvents = "auto"; + } + }, []); + + const disablePointerEvents = useCallback(() => { + if (drawerContentRef.current) { + drawerContentRef.current.style.pointerEvents = "none"; + } + }, []); + + const notificationsList = ( + + ); + + if (isMobile) { + return ( + + {children} + + + {notificationsList} + + + ); + } + return ( {children} @@ -51,12 +92,7 @@ const NotificationsPopover: React.FC = ({ children }: Props) => { scrollable={false} shrink > - - - + {notificationsList} ); diff --git a/app/components/primitives/Drawer.tsx b/app/components/primitives/Drawer.tsx index b721fec49b..9fdd6af6a4 100644 --- a/app/components/primitives/Drawer.tsx +++ b/app/components/primitives/Drawer.tsx @@ -35,10 +35,14 @@ const DrawerContent = React.forwardRef< {children}