mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-10 21:44:16 +00:00
feat(ui): add Tabs content transition
- Animate tab panels when switching active content - Preserve reduced-motion behavior - Cover shared Tabs motion with focused unit tests
This commit is contained in:
2 files changed
+55
-6
No files matched your search
@@ -0,0 +1,35 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
||||
|
||||
describe("Tabs", () => {
|
||||
it("animates tab content when switching between tabs", async () => {
|
||||
// Given - A tabs group with two content panels
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="events">Events</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">Overview content</TabsContent>
|
||||
<TabsContent value="events">Events content</TabsContent>
|
||||
</Tabs>,
|
||||
);
|
||||
|
||||
// When - The user switches to another tab
|
||||
await user.click(screen.getByRole("tab", { name: /events/i }));
|
||||
|
||||
const eventsPanel = screen.getByText("Events content");
|
||||
|
||||
// Then - The newly active content keeps the motion-ready content element
|
||||
expect(eventsPanel).toHaveAttribute("data-slot", "tabs-content");
|
||||
expect(eventsPanel).toHaveClass(
|
||||
"will-change-transform",
|
||||
"motion-reduce:transform-none",
|
||||
);
|
||||
expect(eventsPanel).toHaveAttribute("data-state", "active");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ const TRIGGER_STYLES = {
|
||||
* Content component styles
|
||||
*/
|
||||
const CONTENT_STYLES =
|
||||
"mt-2 focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const;
|
||||
"mt-2 will-change-transform motion-reduce:transform-none focus-visible:rounded-md focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:border-ring focus-visible:outline-ring focus-visible:ring-ring/50" as const;
|
||||
|
||||
/**
|
||||
* Build trigger className by combining style parts
|
||||
@@ -122,14 +123,27 @@ function TabsTrigger({
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn(CONTENT_STYLES, className)}
|
||||
{...props}
|
||||
/>
|
||||
<TabsPrimitive.Content asChild {...props}>
|
||||
<motion.div
|
||||
data-slot="tabs-content"
|
||||
initial={shouldReduceMotion ? false : { opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={
|
||||
shouldReduceMotion
|
||||
? { duration: 0 }
|
||||
: { duration: 0.2, ease: "easeOut" }
|
||||
}
|
||||
className={cn(CONTENT_STYLES, className)}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</TabsPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user