From 31afa2ef03f180a204372a46aa320bc135eda23f Mon Sep 17 00:00:00 2001 From: Brendan from DeFi Date: Fri, 15 Nov 2024 10:07:35 -0800 Subject: [PATCH] Feat: toast animations (#1620) --- .../nextjs-app-router/onchainkit/package.json | 2 +- src/internal/components/Toast.test.tsx | 138 ++++++++++++++++-- src/internal/components/Toast.tsx | 48 +++--- tailwind.config.js | 24 ++- 4 files changed, 182 insertions(+), 30 deletions(-) diff --git a/playground/nextjs-app-router/onchainkit/package.json b/playground/nextjs-app-router/onchainkit/package.json index 97aa5d49b3..be64781475 100644 --- a/playground/nextjs-app-router/onchainkit/package.json +++ b/playground/nextjs-app-router/onchainkit/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/onchainkit", - "version": "0.35.4", + "version": "0.35.5", "type": "module", "repository": "https://github.com/coinbase/onchainkit.git", "license": "MIT", diff --git a/src/internal/components/Toast.test.tsx b/src/internal/components/Toast.test.tsx index 6de177cbaa..1151635d84 100644 --- a/src/internal/components/Toast.test.tsx +++ b/src/internal/components/Toast.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import { Toast } from './Toast'; describe('Toast component', () => { - it('should render bottom-right correctly', () => { + it('should render bottom-right with default animation correctly', () => { const handleClose = vi.fn(); const { getByTestId } = render( @@ -12,15 +12,44 @@ describe('Toast component', () => { , ); - const toastContainer = getByTestId('ockToast'); + const toastContainer = getByTestId('ockToastContainer'); expect(toastContainer).toBeInTheDocument(); expect(toastContainer).toHaveClass('bottom-5 left-3/4'); + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterRight'); + + const closeButton = getByTestId('ockCloseButton'); + expect(closeButton).toBeInTheDocument(); + }); + + it('should render bottom-right with custom animation correctly', () => { + const handleClose = vi.fn(); + const { getByTestId } = render( + +
Test
+
, + ); + + const toastContainer = getByTestId('ockToastContainer'); + expect(toastContainer).toBeInTheDocument(); + expect(toastContainer).toHaveClass('bottom-5 left-3/4'); + + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterUp'); + const closeButton = getByTestId('ockCloseButton'); expect(closeButton).toBeInTheDocument(); }); - it('should render top-right correctly', () => { + it('should render top-right with default animation correctly', () => { const handleClose = vi.fn(); const { getByTestId } = render( @@ -28,15 +57,44 @@ describe('Toast component', () => { , ); - const toastContainer = getByTestId('ockToast'); + const toastContainer = getByTestId('ockToastContainer'); expect(toastContainer).toBeInTheDocument(); expect(toastContainer).toHaveClass('top-[100px] left-3/4'); + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterRight'); + + const closeButton = getByTestId('ockCloseButton'); + expect(closeButton).toBeInTheDocument(); + }); + + it('should render top-right with custom animation correctly', () => { + const handleClose = vi.fn(); + const { getByTestId } = render( + +
Test
+
, + ); + + const toastContainer = getByTestId('ockToastContainer'); + expect(toastContainer).toBeInTheDocument(); + expect(toastContainer).toHaveClass('top-[100px] left-3/4'); + + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterUp'); + const closeButton = getByTestId('ockCloseButton'); expect(closeButton).toBeInTheDocument(); }); - it('should render top-center correctly', () => { + it('should render top-center with default animation correctly', () => { const handleClose = vi.fn(); const { getByTestId } = render( @@ -44,15 +102,44 @@ describe('Toast component', () => { , ); - const toastContainer = getByTestId('ockToast'); + const toastContainer = getByTestId('ockToastContainer'); expect(toastContainer).toBeInTheDocument(); expect(toastContainer).toHaveClass('top-[100px] left-2/4'); + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterDown'); + + const closeButton = getByTestId('ockCloseButton'); + expect(closeButton).toBeInTheDocument(); + }); + + it('should render top-center with custom animation correctly', () => { + const handleClose = vi.fn(); + const { getByTestId } = render( + +
Test
+
, + ); + + const toastContainer = getByTestId('ockToastContainer'); + expect(toastContainer).toBeInTheDocument(); + expect(toastContainer).toHaveClass('top-[100px] left-2/4'); + + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterRight'); + const closeButton = getByTestId('ockCloseButton'); expect(closeButton).toBeInTheDocument(); }); - it('should render bottom-center correctly', () => { + it('should render bottom-center with default animation correctly', () => { const handleClose = vi.fn(); const { getByTestId } = render( @@ -60,10 +147,39 @@ describe('Toast component', () => { , ); - const toastContainer = getByTestId('ockToast'); + const toastContainer = getByTestId('ockToastContainer'); expect(toastContainer).toBeInTheDocument(); expect(toastContainer).toHaveClass('bottom-5 left-2/4'); + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterUp'); + + const closeButton = getByTestId('ockCloseButton'); + expect(closeButton).toBeInTheDocument(); + }); + + it('should render bottom-center with custom animation correctly', () => { + const handleClose = vi.fn(); + const { getByTestId } = render( + +
Test
+
, + ); + + const toastContainer = getByTestId('ockToastContainer'); + expect(toastContainer).toBeInTheDocument(); + expect(toastContainer).toHaveClass('bottom-5 left-2/4'); + + const toast = getByTestId('ockToast'); + expect(toast).toBeInTheDocument(); + expect(toast).toHaveClass('animate-enterRight'); + const closeButton = getByTestId('ockCloseButton'); expect(closeButton).toBeInTheDocument(); }); @@ -81,8 +197,8 @@ describe('Toast component', () => { , ); - const toastContainer = getByTestId('ockToast'); - expect(toastContainer).toHaveClass('custom-class'); + const toast = getByTestId('ockToast'); + expect(toast).toHaveClass('custom-class'); }); it('should not be visible when isVisible is false', () => { @@ -92,7 +208,7 @@ describe('Toast component', () => {
Test
, ); - const toastContainer = queryByTestId('ockToast'); + const toastContainer = queryByTestId('ockToastContainer'); expect(toastContainer).not.toBeInTheDocument(); }); diff --git a/src/internal/components/Toast.tsx b/src/internal/components/Toast.tsx index 801fba8cb2..315b3f3200 100644 --- a/src/internal/components/Toast.tsx +++ b/src/internal/components/Toast.tsx @@ -7,20 +7,30 @@ type ToastProps = { className?: string; durationMs?: number; position: 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right'; + animation?: 'animate-enterRight' | 'animate-enterUp' | 'animate-enterDown'; isVisible: boolean; onClose: () => void; children: React.ReactNode; }; +const defaultAnimationByPosition = { + 'top-center': 'animate-enterDown', + 'top-right': 'animate-enterRight', + 'bottom-center': 'animate-enterUp', + 'bottom-right': 'animate-enterRight', +}; + export function Toast({ className, durationMs = 3000, position = 'bottom-center', + animation, isVisible, onClose, children, }: ToastProps) { const positionClass = getToastPosition(position); + const animationClass = animation ?? defaultAnimationByPosition[position]; useEffect(() => { const timer = setTimeout(() => { @@ -42,25 +52,29 @@ export function Toast({ return (
-
{children}
- +
{children}
+ +
); } diff --git a/tailwind.config.js b/tailwind.config.js index 6f7fd72469..db2fad346b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -22,9 +22,31 @@ export default { transform: 'translate(0)', }, }, + fadeInUp: { + '0%': { + opacity: '0', + transform: 'translateY(2rem)', + }, + '100%': { + opacity: '1', + transform: 'translateY(0)', + }, + }, + fadeInDown: { + '0%': { + opacity: '0', + transform: 'translateY(-2rem)', + }, + '100%': { + opacity: '1', + transform: 'translateY(0)', + }, + }, }, animation: { - enter: 'fadeInRight 500ms ease-out', + enterRight: 'fadeInRight 500ms ease-out', + enterUp: 'fadeInUp 500ms ease-out', + enterDown: 'fadeInDown 500ms ease-out', }, }, },