refactor(web): shared DetailDrawer; objects-page uses it (#58)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user