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();
});
+44
View File
@@ -0,0 +1,44 @@
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { X } from "lucide-react";
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
import { Button } from "@/components/ui/button";
/** A right-sliding Base UI Drawer for a master/detail "detail" on narrow viewports.
* Provides the close affordance + an accessible dialog name; the caller supplies the content. */
export function DetailDrawer({
open,
onClose,
ariaLabel,
children,
}: {
open: boolean;
onClose: () => void;
ariaLabel: string;
children: ReactNode;
}) {
const { t } = useTranslation();
return (
<Drawer
open={open}
onOpenChange={(next) => {
if (!next) onClose();
}}
swipeDirection="right"
>
<DrawerContent aria-label={ariaLabel}>
<div className="flex justify-end border-b p-2">
<DrawerClose
aria-label={t("actions.closeDetail")}
render={<Button variant="ghost" size="icon-sm" />}
>
<X className="size-4" aria-hidden="true" />
</DrawerClose>
</div>
<div className="flex-1 overflow-auto">{children}</div>
</DrawerContent>
</Drawer>
);
}