Skip to main content
Glama
toaster.stories.tsx13.2 kB
import type { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; import { RefreshCw, Trash2, Upload, X } from 'lucide-react'; import { useState } from 'react'; import { Button } from '../Button'; import { ToastAction } from './Toast'; import { Toaster } from './Toaster'; import { useToast } from './useToast'; const meta: Meta<typeof Toaster> = { title: 'Components/Toaster', component: Toaster, parameters: { layout: 'fullscreen', docs: { description: { component: ` The Toaster component provides a comprehensive notification system with multiple variants, actions, and automatic state management. It's built on Radix UI primitives with full accessibility support and smooth animations. ## Key Features - **Multiple Variants**: Default, success, and error notifications - **Interactive Actions**: Buttons for user actions (retry, undo, view details) - **Auto-dismiss**: Configurable timeout with manual dismiss options - **Accessibility**: Full ARIA support and keyboard navigation - **Queue Management**: Handles multiple toasts with proper stacking - **Animations**: Smooth slide-in/out with hardware acceleration `, }, }, }, decorators: [ (Story) => ( <div style={{ minHeight: '100vh', padding: '2rem' }}> <Story /> <Toaster /> </div> ), ], }; export default meta; type Story = StoryObj<typeof Toaster>; /** * Interactive demo showing all toast variants with different content configurations. * Test each variant to see the different styling and icon treatments. */ export const AllVariants: Story = { render: () => { const { toast } = useToast(); return ( <div className="mx-auto w-full max-w-2xl rounded-lg border bg-white p-6"> <div className="mb-6"> <h2 className="mb-2 font-semibold text-xl">Toast Variants</h2> </div> <div className="space-y-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-3"> <Button label="Show default notification" onClick={() => toast({ title: 'Information', description: 'This is a default notification with useful information.', }) } variant="outline" > Default Toast </Button> <Button label="Show success notification" onClick={() => toast({ title: 'Success!', description: 'Your action was completed successfully.', variant: 'success', }) } variant="outline" className="border-green-300 text-green-700" > Success Toast </Button> <Button label="Show error notification" onClick={() => toast({ title: 'Error Occurred', description: 'Something went wrong. Please try again.', variant: 'error', }) } variant="outline" className="border-red-300 text-red-700" > Error Toast </Button> </div> <div className="mt-4 text-gray-600 text-sm"> Click any button to see the corresponding toast variant. Each variant has distinct styling to communicate the appropriate message tone. </div> </div> </div> ); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Test default toast const defaultButton = canvas.getByText('Default Toast'); await userEvent.click(defaultButton); // Verify toast appears (with a small delay for animation) await new Promise((resolve) => setTimeout(resolve, 100)); // Test success toast const successButton = canvas.getByText('Success Toast'); await userEvent.click(successButton, { delay: 500, }); // Test error toast const errorButton = canvas.getByText('Error Toast'); await userEvent.click(errorButton, { delay: 500, }); }, }; /** * Demonstrates toasts with interactive actions like retry, undo, and view details. * Actions provide users with immediate ways to respond to notifications. */ export const WithActions: Story = { render: () => { const { toast } = useToast(); const [uploadStatus, setUploadStatus] = useState< 'idle' | 'uploading' | 'success' | 'error' >('idle'); const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleted'>( 'idle' ); const simulateUpload = () => { setUploadStatus('uploading'); toast({ title: 'Uploading...', description: 'Your file is being uploaded.', }); // Simulate upload failure setTimeout(() => { setUploadStatus('error'); toast({ title: 'Upload Failed', description: 'Could not upload file. Connection timeout.', variant: 'error', action: ( <ToastAction altText="Retry upload" onClick={() => simulateUpload()} color="white" > <RefreshCw className="mr-2 h-4 w-4" /> Retry </ToastAction> ), }); }, 2000); }; const simulateDelete = () => { setDeleteStatus('deleted'); const undoTimer = setTimeout(() => { // Permanent delete after 5 seconds toast({ title: 'File Deleted', description: 'The file has been permanently deleted.', variant: 'success', }); }, 5000); toast({ title: 'File Moved to Trash', description: 'Your file will be permanently deleted in 5 seconds.', action: ( <ToastAction altText="Undo delete" onClick={() => { clearTimeout(undoTimer); setDeleteStatus('idle'); toast({ title: 'Deletion Cancelled', description: 'Your file has been restored.', variant: 'success', }); }} > Undo </ToastAction> ), }); }; return ( <div className="mx-auto w-full max-w-2xl rounded-lg border bg-white p-6"> <div className="mb-6"> <h2 className="mb-2 font-semibold text-xl">Interactive Actions</h2> </div> <div className="space-y-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <Button label="Upload a file and see retry action on failure" onClick={simulateUpload} disabled={uploadStatus === 'uploading'} variant="outline" Icon={Upload} > {uploadStatus === 'uploading' ? 'Uploading...' : 'Upload File'} </Button> <Button label="Delete a file and see undo action" onClick={simulateDelete} disabled={deleteStatus === 'deleted'} variant="outline" className="border-red-300 text-red-700" Icon={Trash2} > Delete File </Button> </div> <div className="mt-4 text-gray-600 text-sm"> <strong>Actions Demo:</strong> <ul className="mt-2 list-inside list-disc space-y-1"> <li> <strong>Upload:</strong> Shows retry action on failure </li> <li> <strong>Delete:</strong> Shows undo action with countdown </li> </ul> </div> </div> </div> ); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Test upload with retry action const uploadButton = canvas.getByText('Upload File'); await userEvent.click(uploadButton); // Wait for upload to "fail" and retry button to appear await new Promise((resolve) => setTimeout(resolve, 2500)); // Test delete with undo action const deleteButton = canvas.getByText('Delete File'); await userEvent.click(deleteButton); // Wait a moment to see the undo toast appear await new Promise((resolve) => setTimeout(resolve, 500)); }, }; /** * Real-world scenarios demonstrating common use cases like form validation, * API responses, and user workflows with appropriate toast implementations. */ export const RealWorldScenarios: Story = { render: () => { const { toast } = useToast(); const [formData, setFormData] = useState({ name: '', email: '' }); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>( 'idle' ); const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!formData.name || !formData.email) { toast({ title: 'Validation Error', description: 'Please fill in all required fields.', variant: 'error', }); return; } setSaveStatus('saving'); toast({ title: 'Saving...', description: 'Your profile is being updated.', }); // Simulate API call setTimeout(() => { setSaveStatus('saved'); toast({ title: 'Profile Saved', description: 'Your changes have been saved successfully.', variant: 'success', action: ( <ToastAction altText="View profile">View Profile</ToastAction> ), }); }, 2000); }; const simulateNetworkError = () => { toast({ title: 'Connection Error', description: 'Unable to connect to server. Check your internet connection.', variant: 'error', action: ( <ToastAction altText="Retry connection" onClick={() => simulateNetworkError()} > <RefreshCw className="mr-2 h-4 w-4" /> Retry </ToastAction> ), }); }; return ( <div className="mx-auto w-full max-w-2xl rounded-lg border bg-white p-6"> <div className="mb-6"> <h2 className="mb-2 font-semibold text-xl">Real-World Scenarios</h2> </div> <div className="space-y-6"> {/* Form Example */} <div className="space-y-4"> <h3 className="font-medium text-lg">Form Validation</h3> <form onSubmit={handleFormSubmit} className="space-y-3"> <div> <label className="mb-1 block font-medium text-sm">Name *</label> <input type="text" value={formData.name} onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value })) } className="w-full rounded-md border border-gray-300 px-3 py-2" placeholder="Enter your name" /> </div> <div> <label className="mb-1 block font-medium text-sm"> Email * </label> <input type="email" value={formData.email} onChange={(e) => setFormData((prev) => ({ ...prev, email: e.target.value })) } className="w-full rounded-md border border-gray-300 px-3 py-2" placeholder="Enter your email" /> </div> <Button label="Save user profile with validation" type="submit" disabled={saveStatus === 'saving'} className="w-full" > {saveStatus === 'saving' ? 'Saving...' : 'Save Profile'} </Button> </form> </div> {/* Error Scenarios */} <div className="space-y-4"> <h3 className="font-medium text-lg">Error Handling</h3> <Button label="Simulate network connection error" onClick={simulateNetworkError} variant="outline" className="border-red-300 text-red-700" Icon={X} > Network Error </Button> </div> </div> </div> ); }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); // Test form validation const saveButton = canvas.getByText('Save Profile'); await userEvent.click(saveButton); // Wait for validation error toast await new Promise((resolve) => setTimeout(resolve, 500)); // Fill form and submit const nameInput = canvas.getByPlaceholderText('Enter your name'); const emailInput = canvas.getByPlaceholderText('Enter your email'); await userEvent.type(nameInput, 'John Doe'); await userEvent.type(emailInput, 'john@example.com'); await userEvent.click(saveButton); }, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server