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 to design system react
- Loading branch information
1 parent
2564ee9
commit ac9d8c4
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.