generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding avatar base component to design system react (#390)
## **Description** This PR adds the initial `AvatarBase` component to design system react. Key features: 1. Five size variants: xs (16px), sm (24px), md (32px), lg (40px), and xl (48px) 2. Support for different content types: text, images, and icons 3. Customizable styling through className prop using Tailwind classes 4. Built-in accessibility features through Radix UI's Slot primitive 5. Comprehensive test coverage and Storybook documentation ## **Related issues** Fixes: #359 ## **Manual testing steps** 1. Run Storybook locally (`yarn storybook`) 2. Navigate to React Components/AvatarBase 3. Check the docs align with component and make sense 4. Verify controls work ## **Screenshots/Recordings** ### **Before** N/A - New component ### **After** https://github.com/user-attachments/assets/51ef4b5c-8f41-4f67-909d-cc6033a8acaf ## **Pre-merge author checklist** - [x] I've followed MetaMask Contributor Docs - [x] I've completed the PR template to the best of my ability - [x] I've included tests (unit tests and Storybook stories) - [x] I've documented my code using JSDoc format - [ ] I've applied the right labels on the PR ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
- Loading branch information
1 parent
eadea1f
commit 4812f88
Showing
7 changed files
with
597 additions
and
0 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
packages/design-system-react/src/components/avatar-base/AvatarBase.constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { AvatarBaseSize } from './AvatarBase.types'; | ||
import { TextVariant } from '../text'; | ||
|
||
export const AVATAR_BASE_SIZE_CLASS_MAP: Record<AvatarBaseSize, string> = { | ||
[AvatarBaseSize.Xs]: 'h-4 w-4', | ||
[AvatarBaseSize.Sm]: 'h-6 w-6', | ||
[AvatarBaseSize.Md]: 'h-8 w-8', | ||
[AvatarBaseSize.Lg]: 'h-10 w-10', | ||
[AvatarBaseSize.Xl]: 'h-12 w-12', | ||
}; | ||
|
||
export const AVATAR_BASE_SIZE_TO_TEXT_VARIANT_MAP: Record< | ||
AvatarBaseSize, | ||
TextVariant | ||
> = { | ||
[AvatarBaseSize.Xs]: TextVariant.BodyXs, | ||
[AvatarBaseSize.Sm]: TextVariant.BodyXs, | ||
[AvatarBaseSize.Md]: TextVariant.BodySm, | ||
[AvatarBaseSize.Lg]: TextVariant.BodyMd, | ||
[AvatarBaseSize.Xl]: TextVariant.BodyMd, | ||
}; |
159 changes: 159 additions & 0 deletions
159
packages/design-system-react/src/components/avatar-base/AvatarBase.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import React from 'react'; | ||
|
||
import { Icon, IconName, IconSize, Text, TextVariant, TextColor } from '..'; | ||
import { AvatarBase } from './AvatarBase'; | ||
import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types'; | ||
import README from './README.mdx'; | ||
|
||
const meta: Meta<typeof AvatarBase> = { | ||
title: 'React Components/AvatarBase', | ||
component: AvatarBase, | ||
parameters: { | ||
docs: { | ||
page: README, | ||
}, | ||
}, | ||
argTypes: { | ||
children: { | ||
control: 'text', | ||
description: | ||
'Optional prop for the content to be rendered within the AvatarBase. Not required if fallbackText is provided', | ||
}, | ||
fallbackText: { | ||
control: 'text', | ||
description: 'Optional text to display when no children are provided', | ||
}, | ||
fallbackTextProps: { | ||
control: 'object', | ||
description: | ||
'Optional props to be passed to the Text component when rendering fallback text', | ||
}, | ||
className: { | ||
control: 'text', | ||
description: | ||
'Optional prop for additional CSS classes to be applied to the AvatarBase component', | ||
}, | ||
size: { | ||
control: 'select', | ||
options: Object.keys(AvatarBaseSize), | ||
mapping: AvatarBaseSize, | ||
description: 'Optional prop to control the size of the AvatarBase', | ||
}, | ||
shape: { | ||
control: 'select', | ||
options: Object.keys(AvatarBaseShape), | ||
mapping: AvatarBaseShape, | ||
description: 'Optional prop to control the shape of the AvatarBase', | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof AvatarBase>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
fallbackText: 'A', | ||
}, | ||
}; | ||
|
||
export const Shape: Story = { | ||
render: () => ( | ||
<div className="flex gap-2 items-center"> | ||
<AvatarBase shape={AvatarBaseShape.Circle} fallbackText="C" /> | ||
<AvatarBase shape={AvatarBaseShape.Square} fallbackText="S" /> | ||
</div> | ||
), | ||
}; | ||
|
||
export const Size: Story = { | ||
render: () => ( | ||
<div className="flex flex-col gap-2"> | ||
<div className="flex gap-2 items-center"> | ||
<AvatarBase size={AvatarBaseSize.Xs} fallbackText="XS" /> | ||
<AvatarBase size={AvatarBaseSize.Sm} fallbackText="SM" /> | ||
<AvatarBase size={AvatarBaseSize.Md} fallbackText="MD" /> | ||
<AvatarBase size={AvatarBaseSize.Lg} fallbackText="LG" /> | ||
<AvatarBase size={AvatarBaseSize.Xl} fallbackText="XL" /> | ||
</div> | ||
<div className="flex gap-2 items-center"> | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
size={AvatarBaseSize.Xs} | ||
fallbackText="XS" | ||
/> | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
size={AvatarBaseSize.Sm} | ||
fallbackText="SM" | ||
/> | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
size={AvatarBaseSize.Md} | ||
fallbackText="MD" | ||
/> | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
size={AvatarBaseSize.Lg} | ||
fallbackText="LG" | ||
/> | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
size={AvatarBaseSize.Xl} | ||
fallbackText="XL" | ||
/> | ||
</div> | ||
</div> | ||
), | ||
}; | ||
|
||
export const FallbackText: Story = { | ||
render: () => ( | ||
<div className="flex gap-2"> | ||
<AvatarBase fallbackText="A" /> | ||
<AvatarBase fallbackText="B" /> | ||
<AvatarBase fallbackText="C" /> | ||
</div> | ||
), | ||
}; | ||
|
||
export const FallbackTextWithProps: Story = { | ||
render: () => ( | ||
<div className="flex gap-2"> | ||
<AvatarBase | ||
fallbackText="A" | ||
fallbackTextProps={{ color: TextColor.PrimaryDefault }} | ||
/> | ||
<AvatarBase | ||
fallbackText="B" | ||
fallbackTextProps={{ color: TextColor.ErrorDefault }} | ||
/> | ||
<AvatarBase | ||
fallbackText="C" | ||
fallbackTextProps={{ color: TextColor.SuccessDefault }} | ||
/> | ||
</div> | ||
), | ||
}; | ||
|
||
export const Children: Story = { | ||
render: () => ( | ||
<div className="flex gap-2 items-center"> | ||
{/* Text */} | ||
<AvatarBase fallbackText="A" /> | ||
{/* Image */} | ||
<AvatarBase> | ||
<img | ||
src="https://cryptologos.cc/logos/avalanche-avax-logo.png?v=040" | ||
alt="Avalanche" | ||
className="w-full h-full object-cover" | ||
/> | ||
</AvatarBase> | ||
{/* Icon */} | ||
<AvatarBase> | ||
<Icon name={IconName.User} size={IconSize.Sm} /> | ||
</AvatarBase> | ||
</div> | ||
), | ||
}; |
184 changes: 184 additions & 0 deletions
184
packages/design-system-react/src/components/avatar-base/AvatarBase.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import React from 'react'; | ||
|
||
import { TextColor } from '../text'; | ||
import { AvatarBase } from './AvatarBase'; | ||
import { AVATAR_BASE_SIZE_CLASS_MAP } from './AvatarBase.constants'; | ||
import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types'; | ||
|
||
describe('AvatarBase', () => { | ||
it('renders with default styles', () => { | ||
render(<AvatarBase fallbackText="A" data-testid="avatar" />); | ||
|
||
const avatar = screen.getByTestId('avatar'); | ||
expect(screen.getByText('A')).toBeInTheDocument(); | ||
}); | ||
|
||
it('applies size classes correctly', () => { | ||
const { rerender } = render( | ||
<AvatarBase | ||
size={AvatarBaseSize.Xs} | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
|
||
Object.entries(AVATAR_BASE_SIZE_CLASS_MAP).forEach(([size, classes]) => { | ||
rerender( | ||
<AvatarBase | ||
size={size as AvatarBaseSize} | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
const avatar = screen.getByTestId('avatar'); | ||
const classArray = classes.split(' '); | ||
classArray.forEach((className) => { | ||
expect(avatar).toHaveClass(className); | ||
}); | ||
}); | ||
}); | ||
|
||
it('renders children correctly', () => { | ||
render( | ||
<AvatarBase> | ||
<img src="test.jpg" alt="test" data-testid="avatar-image" /> | ||
</AvatarBase>, | ||
); | ||
|
||
expect(screen.getByTestId('avatar-image')).toBeInTheDocument(); | ||
}); | ||
|
||
it('merges custom className with default classes', () => { | ||
render( | ||
<AvatarBase | ||
className="custom-class" | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
|
||
const avatar = screen.getByTestId('avatar'); | ||
expect(avatar).toHaveClass('custom-class'); | ||
expect(avatar).toHaveClass('rounded-full'); | ||
}); | ||
|
||
it('renders as child component when asChild is true', () => { | ||
render( | ||
<AvatarBase asChild> | ||
<span>A</span> | ||
</AvatarBase>, | ||
); | ||
|
||
const avatar = screen.getByText('A'); | ||
expect(avatar.tagName).toBe('SPAN'); | ||
}); | ||
|
||
it('applies custom styles when provided', () => { | ||
render( | ||
<AvatarBase | ||
style={{ backgroundColor: 'red' }} | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
|
||
const avatar = screen.getByTestId('avatar'); | ||
expect(avatar).toHaveStyle({ backgroundColor: 'red' }); | ||
}); | ||
|
||
it('applies correct shape classes', () => { | ||
const { rerender } = render( | ||
<AvatarBase | ||
shape={AvatarBaseShape.Circle} | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
|
||
let avatar = screen.getByTestId('avatar'); | ||
expect(avatar).toHaveClass('rounded-full'); | ||
|
||
rerender( | ||
<AvatarBase | ||
shape={AvatarBaseShape.Square} | ||
fallbackText="A" | ||
data-testid="avatar" | ||
/>, | ||
); | ||
avatar = screen.getByTestId('avatar'); | ||
expect(avatar).toHaveClass('rounded-lg'); | ||
}); | ||
|
||
it('uses circle shape by default', () => { | ||
render(<AvatarBase fallbackText="A" data-testid="avatar" />); | ||
|
||
const avatar = screen.getByTestId('avatar'); | ||
expect(avatar).toHaveClass('rounded-full'); | ||
}); | ||
|
||
it('renders fallbackText when no children are provided', () => { | ||
render(<AvatarBase fallbackText="Test" />); | ||
expect(screen.getByText('Test')).toBeInTheDocument(); | ||
}); | ||
|
||
it('prioritizes children over fallbackText', () => { | ||
render( | ||
<AvatarBase fallbackText="Fallback"> | ||
<span>Child</span> | ||
</AvatarBase>, | ||
); | ||
expect(screen.getByText('Child')).toBeInTheDocument(); | ||
expect(screen.queryByText('Fallback')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('applies fallbackTextProps correctly', () => { | ||
render( | ||
<AvatarBase | ||
fallbackText="Test" | ||
fallbackTextProps={{ | ||
color: TextColor.PrimaryDefault, | ||
'data-testid': 'fallback-text', | ||
}} | ||
/>, | ||
); | ||
const fallbackText = screen.getByTestId('fallback-text'); | ||
expect(fallbackText).toHaveClass('text-primary-default'); | ||
}); | ||
|
||
it('uses correct text variant based on size', () => { | ||
const { rerender } = render( | ||
<AvatarBase | ||
size={AvatarBaseSize.Xs} | ||
fallbackText="A" | ||
fallbackTextProps={{ 'data-testid': 'fallback-text' }} | ||
/>, | ||
); | ||
|
||
// Test XS size | ||
let fallbackText = screen.getByTestId('fallback-text'); | ||
expect(fallbackText).toHaveClass('text-s-body-xs'); | ||
|
||
// Test MD size | ||
rerender( | ||
<AvatarBase | ||
size={AvatarBaseSize.Md} | ||
fallbackText="A" | ||
fallbackTextProps={{ 'data-testid': 'fallback-text' }} | ||
/>, | ||
); | ||
fallbackText = screen.getByTestId('fallback-text'); | ||
expect(fallbackText).toHaveClass('text-s-body-sm'); | ||
|
||
// Test XL size | ||
rerender( | ||
<AvatarBase | ||
size={AvatarBaseSize.Xl} | ||
fallbackText="A" | ||
fallbackTextProps={{ 'data-testid': 'fallback-text' }} | ||
/>, | ||
); | ||
fallbackText = screen.getByTestId('fallback-text'); | ||
expect(fallbackText).toHaveClass('text-s-body-md'); | ||
}); | ||
}); |
Oops, something went wrong.