From ef4d05a782c82ee118e29b56b11c7acdf04e6f08 Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Fri, 5 Jun 2026 12:16:27 +0200 Subject: [PATCH] feat(ui): add Popover open close microinteraction - Add explicit timing for Popover state transitions - Add reduced-motion fallback utilities - Cover controlled Popover motion behavior --- ui/components/shadcn/popover.test.tsx | 87 +++++++++++++++++++++++++++ ui/components/shadcn/popover.tsx | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 ui/components/shadcn/popover.test.tsx diff --git a/ui/components/shadcn/popover.test.tsx b/ui/components/shadcn/popover.test.tsx new file mode 100644 index 0000000000..c7ae61350a --- /dev/null +++ b/ui/components/shadcn/popover.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { Popover, PopoverContent, PopoverTrigger } from "./popover"; + +describe("Popover", () => { + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders controlled content through the Radix Popover API", () => { + // Given + const portalContainer = document.createElement("div"); + document.body.appendChild(portalContainer); + + // When + render( + + Open filters + + Filter content + + , + ); + + // Then + expect(screen.getByText("Filter content")).toBeVisible(); + expect(screen.getByText("Filter content")).toHaveAttribute( + "data-slot", + "popover-content", + ); + }); + + it("uses an intentional open and close motion contract", () => { + // Given + const portalContainer = document.createElement("div"); + document.body.appendChild(portalContainer); + + // When + render( + + Open filters + + Filter content + + , + ); + + // Then + expect(screen.getByText("Filter content")).toHaveClass( + "origin-(--radix-popover-content-transform-origin)", + "duration-200", + "ease-out", + "data-[state=open]:animate-in", + "data-[state=open]:fade-in-0", + "data-[state=open]:zoom-in-95", + "data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0", + "data-[state=closed]:zoom-out-95", + "data-[state=closed]:duration-100", + "data-[state=closed]:ease-in", + ); + }); + + it("removes transform-heavy motion for reduced-motion users", () => { + // Given + const portalContainer = document.createElement("div"); + document.body.appendChild(portalContainer); + + // When + render( + + Open filters + + Filter content + + , + ); + + // Then + expect(screen.getByText("Filter content")).toHaveClass( + "motion-reduce:animate-none", + "motion-reduce:transform-none", + "motion-reduce:transition-none", + ); + }); +}); diff --git a/ui/components/shadcn/popover.tsx b/ui/components/shadcn/popover.tsx index e64bae46ad..9ddfbc90b1 100644 --- a/ui/components/shadcn/popover.tsx +++ b/ui/components/shadcn/popover.tsx @@ -32,7 +32,7 @@ function PopoverContent({ align={align} sideOffset={sideOffset} className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 pointer-events-auto z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 pointer-events-auto z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden duration-200 ease-out data-[state=closed]:duration-100 data-[state=closed]:ease-in motion-reduce:transform-none motion-reduce:animate-none motion-reduce:transition-none", className, )} {...props}