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 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-05-06 07:42:53 -04:00
committed by GitHub
parent 41031aa7e6
commit cc25790c81
3 changed files with 58 additions and 16 deletions
@@ -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<NotificationFilter>("all");
const filterOptions = React.useMemo<Option[]>(
@@ -110,9 +112,9 @@ function Notifications(
<Flex
style={{
width: "100%",
minHeight: "300px",
minHeight: isMobile ? "75vh" : "300px",
maxHeight:
"min(75vh, calc(var(--radix-popover-content-available-height) - 44px))",
"min(75vh, calc(var(--radix-popover-content-available-height, 75vh) - 44px))",
}}
column
>
@@ -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<HTMLDivElement>(null);
const drawerContentRef = useRef<React.ElementRef<typeof DrawerContent>>(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 = (
<Notifications onRequestClose={handleRequestClose} ref={scrollableRef} />
);
if (isMobile) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{children}</DrawerTrigger>
<DrawerContent
ref={drawerContentRef}
aria-label={t("Notifications")}
aria-describedby={undefined}
onAnimationStart={disablePointerEvents}
onAnimationEnd={enablePointerEvents}
>
<DrawerTitle hidden>{t("Notifications")}</DrawerTitle>
{notificationsList}
</DrawerContent>
</Drawer>
);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>{children}</PopoverTrigger>
@@ -51,12 +92,7 @@ const NotificationsPopover: React.FC = ({ children }: Props) => {
scrollable={false}
shrink
>
<Suspense fallback={null}>
<Notifications
onRequestClose={handleRequestClose}
ref={scrollableRef}
/>
</Suspense>
{notificationsList}
</PopoverContent>
</Popover>
);
+8 -4
View File
@@ -35,10 +35,14 @@ const DrawerContent = React.forwardRef<
</DrawerPrimitive.Overlay>
<DrawerPrimitive.Content ref={ref} asChild>
<StyledContent
animate={{
height: bounds.height,
transition: { bounce: 0, duration: 0.2 },
}}
animate={
bounds.height
? {
height: bounds.height,
transition: { bounce: 0, duration: 0.2 },
}
: undefined
}
>
<StyledInnerContent column ref={measureRef} {...rest}>
{children}