Skip to main content
Glama
avatar.stories.tsx13.9 kB
import type { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from '@storybook/test'; import { getAvatarImageUrl } from '../../utils/image'; import { Avatar } from './index'; /** * Avatar component displays user profile information in a circular format. * It supports images, initials, loading states, and interactive features. * * ## Features * - **Responsive sizing**: sm, md, lg, xl variants * - **Multiple display modes**: Image, initials, default icon, loading spinner * - **Accessibility**: ARIA labels, keyboard navigation, focus management * - **Interactive**: Optional click handlers with visual feedback * - **Customizable**: Flexible styling and theming options */ const meta: Meta<typeof Avatar> = { title: 'Components/Avatar', component: Avatar, parameters: { docs: { description: { component: ` The Avatar component is a versatile user profile display that adapts to different content types and states. It follows accessibility best practices and supports multiple interaction patterns. ### Usage Guidelines - Use images when available for better user recognition - Provide meaningful alt text for screen readers - Consider loading states for async image loading - Use initials as fallback when images aren't available `, }, }, layout: 'centered', backgrounds: { values: [ { name: 'light', value: '#ffffff' }, { name: 'dark', value: '#333333' }, { name: 'neutral', value: '#f5f5f5' }, ], }, }, tags: ['autodocs'], argTypes: { src: { control: { type: 'text' }, description: 'Image source URL for the avatar', table: { type: { summary: 'string' }, defaultValue: { summary: 'undefined' }, }, }, fullname: { control: { type: 'text' }, description: 'Full name used to generate initials and alt text', table: { type: { summary: 'string' }, defaultValue: { summary: 'undefined' }, }, }, size: { control: { type: 'select' }, options: ['sm', 'md', 'lg', 'xl'], description: 'Size variant of the avatar', table: { type: { summary: "'sm' | 'md' | 'lg' | 'xl'" }, defaultValue: { summary: "'md'" }, }, }, isLoading: { control: { type: 'boolean' }, description: 'Displays a loading spinner when true', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, }, }, isLoggedIn: { control: { type: 'boolean' }, description: 'Whether the user is authenticated', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' }, }, }, alt: { control: { type: 'text' }, description: 'Alternative text for accessibility', table: { type: { summary: 'string' }, defaultValue: { summary: 'undefined' }, }, }, focusable: { control: { type: 'boolean' }, description: 'Whether the avatar should be focusable when not clickable', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, }, }, onClick: { action: 'clicked', description: 'Click handler - when provided, makes the avatar clickable', table: { type: { summary: '(event: MouseEvent<HTMLButtonElement>) => void' }, }, }, className: { control: { type: 'text' }, description: 'Additional CSS classes for styling', table: { type: { summary: 'string' }, }, }, }, args: { isLoggedIn: true, isLoading: false, size: 'md', focusable: false, }, }; export default meta; type Story = StoryObj<typeof Avatar>; /** * Default avatar with an image source */ export const Default: Story = { args: { src: '', fullname: 'John Doe', alt: 'John Doe profile picture', }, parameters: { docs: { description: { story: 'Basic avatar with a profile image. This is the most common use case when user images are available.', }, }, }, }; /** * Avatar displaying initials when no image is provided */ export const WithInitials: Story = { args: { fullname: 'Sarah Wilson', }, parameters: { docs: { description: { story: 'Avatar showing user initials when no image source is provided. Great fallback option for user profiles.', }, }, }, }; /** * Avatar displaying default user icon */ export const WithIcon: Story = { args: { isLoggedIn: true, fullname: '', }, parameters: { docs: { description: { story: 'Default user icon displayed when no image or name is available.', }, }, }, }; /** * Avatar in loading state */ export const Loading: Story = { args: { isLoading: true, fullname: 'Loading User', }, parameters: { docs: { description: { story: 'Loading state with spinner animation. Use this while fetching user data or profile images.', }, }, }, }; /** * Avatar for logged out user */ export const LoggedOut: Story = { args: { isLoggedIn: false, fullname: 'Anonymous User', }, parameters: { docs: { description: { story: 'Avatar state for unauthenticated users. Shows empty state or placeholder.', }, }, }, }; /** * Clickable avatar with interaction */ export const Clickable: Story = { args: { src: getAvatarImageUrl(), fullname: 'Emma Chen', onClick: () => console.log('Avatar clicked!'), }, parameters: { docs: { description: { story: 'Interactive avatar that responds to clicks. Includes hover effects and accessibility features.', }, }, }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const avatar = canvas.getByRole('button'); // Test accessibility expect(avatar).toBeInTheDocument(); expect(avatar).toHaveAttribute('aria-label'); // Test interaction await userEvent.click(avatar); expect(args.onClick).toHaveBeenCalled(); }, }; /** * Size variations showcase */ export const SizeVariants: Story = { render: () => ( <div className="flex items-center gap-4"> <div className="flex flex-col items-center gap-2"> <Avatar size="sm" src={getAvatarImageUrl()} fullname="Small Avatar" /> <span className="text-gray-600 text-xs">Small</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar size="md" src={getAvatarImageUrl()} fullname="Medium Avatar" /> <span className="text-gray-600 text-xs">Medium</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar size="lg" src={getAvatarImageUrl()} fullname="Large Avatar" /> <span className="text-gray-600 text-xs">Large</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar size="xl" src={getAvatarImageUrl()} fullname="Extra Large Avatar" /> <span className="text-gray-600 text-xs">Extra Large</span> </div> </div> ), parameters: { docs: { description: { story: 'All available size variants from small to extra large. Choose the appropriate size based on your layout needs.', }, }, }, }; /** * Accessibility demonstration */ export const AccessibilityExample: Story = { args: { src: getAvatarImageUrl(), fullname: 'Alex Johnson', alt: 'Alex Johnson, Software Engineer at TechCorp', focusable: true, }, parameters: { docs: { description: { story: 'Avatar with comprehensive accessibility features including custom alt text, focus management, and ARIA labels.', }, }, a11y: { config: { rules: [ { id: 'color-contrast', enabled: true, }, { id: 'focus-visible', enabled: true, }, ], }, }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const avatar = canvas.getByRole('img'); // Test accessibility attributes expect(avatar).toHaveAttribute('aria-label'); expect(avatar).toHaveAttribute('tabindex', '0'); // Test focus behavior await userEvent.tab(); expect(avatar).toHaveFocus(); }, }; /** * Multiple content states */ export const ContentStates: Story = { render: () => ( <div className="grid grid-cols-2 gap-6 md:grid-cols-4"> <div className="flex flex-col items-center gap-2"> <Avatar src={getAvatarImageUrl()} fullname="Image User" /> <span className="text-center text-sm">With Image</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar fullname="Initials User" /> <span className="text-center text-sm">Initials Only</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar isLoading fullname="Loading User" /> <span className="text-center text-sm">Loading State</span> </div> <div className="flex flex-col items-center gap-2"> <Avatar /> <span className="text-center text-sm">Default Icon</span> </div> </div> ), parameters: { docs: { description: { story: 'Comparison of different content states: image, initials, loading, and default icon.', }, }, }, }; /** * Interactive gallery with various personas */ export const UserGallery: Story = { render: () => ( <div className="grid grid-cols-3 gap-4 md:grid-cols-6"> {[ { name: 'John Doe', img: getAvatarImageUrl() }, { name: 'Jane Smith', img: getAvatarImageUrl() }, { name: 'Bob Wilson', img: getAvatarImageUrl() }, { name: 'Alice Brown', img: getAvatarImageUrl() }, { name: 'Charlie Davis', img: getAvatarImageUrl() }, { name: 'Eva Garcia', img: getAvatarImageUrl() }, { name: 'Frank Miller', img: getAvatarImageUrl() }, { name: 'Grace Lee', img: getAvatarImageUrl() }, { name: 'Henry Taylor', img: getAvatarImageUrl() }, { name: 'Ivy Chen', img: getAvatarImageUrl() }, { name: 'Jack Robinson', img: getAvatarImageUrl() }, { name: 'Kate Johnson', img: getAvatarImageUrl() }, ].map((user, index) => ( <Avatar key={index} src={user.img} fullname={user.name} onClick={() => console.log(`Clicked on ${user.name}`)} className="transition-transform duration-200 hover:scale-110" /> ))} </div> ), parameters: { docs: { description: { story: 'Gallery of user avatars demonstrating various combinations of images and initials with interactive hover effects.', }, }, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const avatars = canvas.getAllByRole('button'); // Test that all avatars are rendered expect(avatars).toHaveLength(12); // Test interaction with first avatar const firstAvatar = avatars[0]; await userEvent.hover(firstAvatar); await userEvent.click(firstAvatar); }, }; /** * Different background themes */ export const ThemeVariations: Story = { render: () => ( <div className="grid grid-cols-1 gap-8"> <div className="rounded-lg bg-white p-6"> <h3 className="mb-4 font-semibold text-lg">Light Theme</h3> <div className="flex gap-4"> <Avatar src={getAvatarImageUrl()} fullname="Light Theme User" /> <Avatar fullname="Light Initials" /> <Avatar isLoading /> </div> </div> <div className="rounded-lg bg-gray-900 p-6"> <h3 className="mb-4 font-semibold text-lg text-white">Dark Theme</h3> <div className="flex gap-4"> <Avatar src={getAvatarImageUrl()} fullname="Dark Theme User" /> <Avatar fullname="Dark Initials" /> <Avatar isLoading /> </div> </div> <div className="rounded-lg bg-blue-50 p-6"> <h3 className="mb-4 font-semibold text-blue-900 text-lg"> Colored Background </h3> <div className="flex gap-4"> <Avatar src={getAvatarImageUrl()} fullname="Colored Theme User" /> <Avatar fullname="Colored Initials" /> <Avatar isLoading /> </div> </div> </div> ), parameters: { docs: { description: { story: 'Avatar appearance across different background themes and color schemes.', }, }, }, }; /** * Comprehensive test scenario */ export const TestScenario: Story = { args: { src: getAvatarImageUrl(), fullname: 'Test User', onClick: () => console.log('Test avatar clicked'), }, parameters: { docs: { description: { story: 'Comprehensive test scenario with automated interactions and accessibility validation.', }, }, }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const avatar = canvas.getByRole('button'); // Test initial state expect(avatar).toBeInTheDocument(); expect(avatar).toHaveAttribute('type', 'button'); // Test accessibility expect(avatar).toHaveAttribute('aria-label'); expect(avatar).toHaveClass('cursor-pointer'); // Test keyboard navigation await userEvent.tab(); expect(avatar).toHaveFocus(); // Test click interaction await userEvent.click(avatar); expect(args.onClick).toHaveBeenCalled(); // Test hover state await userEvent.hover(avatar); expect(avatar).toHaveClass('hover:opacity-80'); }, };

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