From 87bd2e78a128db830a15d62168f916b9f690c9ee Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Fri, 5 Jun 2026 14:54:45 +0200 Subject: [PATCH] feat(ui): add Drawer content transition - Animate drawer overlay and directional content states - Preserve reduced-motion behavior - Cover Drawer motion with focused unit tests --- ui/components/shadcn/drawer.test.tsx | 88 ++++++++++++++++++++++++++++ ui/components/shadcn/drawer.tsx | 12 ++-- 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 ui/components/shadcn/drawer.test.tsx diff --git a/ui/components/shadcn/drawer.test.tsx b/ui/components/shadcn/drawer.test.tsx new file mode 100644 index 0000000000..50ef7d8704 --- /dev/null +++ b/ui/components/shadcn/drawer.test.tsx @@ -0,0 +1,88 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerTitle, + DrawerTrigger, +} from "./drawer"; + +function renderOpenDrawer() { + return render( + + Open drawer + + Resource details + Review resource metadata + + , + ); +} + +describe("Drawer", () => { + it("renders controlled content through the Vaul Drawer API", () => { + // Given + renderOpenDrawer(); + + // When + const drawer = screen.getByRole("dialog", { name: "Resource details" }); + + // Then + expect(drawer).toBeVisible(); + expect(drawer).toHaveAttribute("data-slot", "drawer-content"); + expect(screen.getByText("Review resource metadata")).toBeVisible(); + }); + + it("uses an intentional overlay motion contract", () => { + // Given + renderOpenDrawer(); + + // When + const overlay = document.querySelector("[data-slot='drawer-overlay']"); + + // Then + expect(overlay).toHaveClass( + "duration-200", + "ease-out", + "data-[state=open]:animate-in", + "data-[state=open]:fade-in-0", + "data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0", + "data-[state=closed]:duration-100", + "data-[state=closed]:ease-in", + "motion-reduce:animate-none", + "motion-reduce:transition-none", + ); + }); + + it("uses direction-aware drawer content motion", () => { + // Given + renderOpenDrawer(); + + // When + const drawer = screen.getByRole("dialog", { name: "Resource details" }); + + // Then + expect(drawer).toHaveClass( + "duration-200", + "ease-out", + "data-[state=open]:animate-in", + "data-[state=closed]:animate-out", + "data-[state=closed]:duration-100", + "data-[state=closed]:ease-in", + "data-[vaul-drawer-direction=bottom]:slide-in-from-bottom-full", + "data-[vaul-drawer-direction=bottom]:data-[state=closed]:slide-out-to-bottom-full", + "data-[vaul-drawer-direction=top]:slide-in-from-top-full", + "data-[vaul-drawer-direction=top]:data-[state=closed]:slide-out-to-top-full", + "data-[vaul-drawer-direction=right]:slide-in-from-right-full", + "data-[vaul-drawer-direction=right]:data-[state=closed]:slide-out-to-right-full", + "data-[vaul-drawer-direction=left]:slide-in-from-left-full", + "data-[vaul-drawer-direction=left]:data-[state=closed]:slide-out-to-left-full", + "motion-reduce:animate-none", + "motion-reduce:transform-none", + "motion-reduce:transition-none", + ); + }); +}); diff --git a/ui/components/shadcn/drawer.tsx b/ui/components/shadcn/drawer.tsx index 5468f0114d..c674c34341 100644 --- a/ui/components/shadcn/drawer.tsx +++ b/ui/components/shadcn/drawer.tsx @@ -35,7 +35,7 @@ function DrawerOverlay({