refactor(web): shared DetailDrawer; objects-page uses it (#58)

This commit is contained in:
2026-06-09 15:09:37 +02:00
parent b3f061ced7
commit b5756e16b5
3 changed files with 35 additions and 21 deletions
+20
View File
@@ -0,0 +1,20 @@
import { expect, test, vi } from "vitest";
import { within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderApp } from "../test/render";
import { DetailDrawer } from "./detail-drawer";
test("renders children in a named drawer and closes via the close button", async () => {
const onClose = vi.fn();
renderApp(
<DetailDrawer open onClose={onClose} ariaLabel="Object detail">
<p>detail body</p>
</DetailDrawer>,
);
const body = within(document.body);
expect(await body.findByText("detail body")).toBeInTheDocument();
await userEvent.click(body.getByRole("button", { name: /close detail/i }));
expect(onClose).toHaveBeenCalled();
});
@@ -1,21 +1,22 @@
import { Outlet } from "react-router-dom"; import type { ReactNode } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
/** /** A right-sliding Base UI Drawer for a master/detail "detail" on narrow viewports.
* Narrow-viewport object detail: the nested <Outlet/> inside a Base UI Drawer that * Provides the close affordance + an accessible dialog name; the caller supplies the content. */
* slides from the right. Lazy-loaded so Base UI's drawer code (swipe/snap machinery) export function DetailDrawer({
* splits out of the main entry chunk the wide pane path never pays for it.
*/
export function ObjectDetailDrawer({
open, open,
onClose, onClose,
ariaLabel,
children,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
ariaLabel: string;
children: ReactNode;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -27,7 +28,7 @@ export function ObjectDetailDrawer({
}} }}
swipeDirection="right" swipeDirection="right"
> >
<DrawerContent aria-label={t("objects.detailTitle")}> <DrawerContent aria-label={ariaLabel}>
<div className="flex justify-end border-b p-2"> <div className="flex justify-end border-b p-2">
<DrawerClose <DrawerClose
aria-label={t("actions.closeDetail")} aria-label={t("actions.closeDetail")}
@@ -36,9 +37,7 @@ export function ObjectDetailDrawer({
<X className="size-4" aria-hidden="true" /> <X className="size-4" aria-hidden="true" />
</DrawerClose> </DrawerClose>
</div> </div>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">{children}</div>
<Outlet />
</div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );
+5 -10
View File
@@ -1,19 +1,15 @@
import { lazy, Suspense } from "react";
import { Outlet, useMatch, useNavigate, useSearchParams } from "react-router-dom"; import { Outlet, useMatch, useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { ObjectsTable } from "./objects-table"; import { ObjectsTable } from "./objects-table";
import { DetailDrawer } from "../components/detail-drawer";
import { useMediaQuery } from "../lib/use-media-query"; import { useMediaQuery } from "../lib/use-media-query";
import { useDocumentTitle } from "../lib/use-document-title"; import { useDocumentTitle } from "../lib/use-document-title";
import { useBreadcrumb } from "../shell/use-breadcrumb"; import { useBreadcrumb } from "../shell/use-breadcrumb";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PageTitle } from "@/components/ui/page-title"; import { PageTitle } from "@/components/ui/page-title";
const ObjectDetailDrawer = lazy(() =>
import("./object-detail-drawer").then((m) => ({ default: m.ObjectDetailDrawer })),
);
export function ObjectsPage() { export function ObjectsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -66,15 +62,14 @@ export function ObjectsPage() {
); );
} }
// Narrow: the detail lives in a Drawer, lazy-loaded so Base UI's drawer code stays // Narrow: the detail lives in a Drawer sliding from the right.
// out of the main entry chunk.
return ( return (
<div className="h-full"> <div className="h-full">
{table} {table}
{open && ( {open && (
<Suspense fallback={null}> <DetailDrawer open={open} onClose={closeDetail} ariaLabel={t("objects.detailTitle")}>
<ObjectDetailDrawer open={open} onClose={closeDetail} /> <Outlet />
</Suspense> </DetailDrawer>
)} )}
</div> </div>
); );