Skip to content

Commit

Permalink
feat: adding avatar base to design system react
Browse files Browse the repository at this point in the history
  • Loading branch information
georgewrmarshall committed Feb 3, 2025
1 parent 2564ee9 commit ac9d8c4
Show file tree
Hide file tree
Showing 7 changed files with 597 additions and 0 deletions.
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,
};
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>
),
};
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');
});
});
Loading

0 comments on commit ac9d8c4

Please sign in to comment.