mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user