Skip to content

Commit

Permalink
chore: adding initial avatar network component
Browse files Browse the repository at this point in the history
  • Loading branch information
georgewrmarshall committed Jan 31, 2025
1 parent fe89222 commit 58f1ffc
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Remove this file if it's not needed
export const AVATARNETWORK_CLASSMAP = {};

import { AvatarBaseSize } from '../avatar-base';
import { TextVariant } from '../text';

export const AVATAR_NETWORK_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,107 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { TextColor } from '..';
import { AvatarNetwork } from './AvatarNetwork';
import { AvatarNetworkSize } from '.';
import README from './README.mdx';

const meta: Meta<typeof AvatarNetwork> = {
title: 'React Components/AvatarNetwork',
component: AvatarNetwork,
parameters: {
docs: {
page: README,
},
},
argTypes: {
src: {
control: 'text',
description:
'Optional URL for the network image. When provided, displays the image instead of fallback text',
},
size: {
control: 'select',
options: Object.keys(AvatarNetworkSize),
mapping: AvatarNetworkSize,
description:
'Optional prop to control the size of the avatar. Defaults to AvatarNetworkSize.Md',
},
fallbackText: {
control: 'text',
description:
'Required text to display when no image is provided. Also used as alt text for the image when src is provided',
},
fallbackTextProps: {
control: 'object',
description:
'Optional props to be passed to the Text component when rendering fallback text. Only used when src is not provided',
},
className: {
control: 'text',
description:
'Optional additional CSS classes to be applied to the component',
},
},
};

export default meta;
type Story = StoryObj<typeof AvatarNetwork>;

export const Default: Story = {
args: {
src: 'https://cryptologos.cc/logos/ethereum-eth-logo.png',
fallbackText: 'Eth',
},
};

export const Src: Story = {
render: () => (
<div className="flex gap-2">
<AvatarNetwork
fallbackText="Eth"
src="https://cryptologos.cc/logos/ethereum-eth-logo.png"
/>
<AvatarNetwork
fallbackText="Ava"
src="https://cryptologos.cc/logos/avalanche-avax-logo.png"
/>
<AvatarNetwork
fallbackText="Pol"
src="https://cryptologos.cc/logos/polygon-matic-logo.png"
/>
</div>
),
};

export const FallbackText: Story = {
render: () => (
<div className="flex gap-2">
<AvatarNetwork fallbackText="Eth" />
<AvatarNetwork fallbackText="Ava" />
<AvatarNetwork fallbackText="Pol" />
</div>
),
};

export const FallbackTextProps: Story = {
args: {
fallbackText: 'Eth',
fallbackTextProps: {
color: TextColor.ErrorDefault,
'data-testid': 'fallback-text',
},
},
};

export const Size: Story = {
render: () => (
<div className="flex gap-2 items-center">
<AvatarNetwork fallbackText="E" size={AvatarNetworkSize.Xs} />
<AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Sm} />
<AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Md} />
<AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Lg} />
<AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Xl} />
</div>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { render, screen } from '@testing-library/react';
import React from 'react';

import { TextColor } from '..';
import { AvatarNetwork } from './AvatarNetwork';
import { AvatarNetworkSize } from '.';

describe('AvatarNetwork', () => {
it('renders image when src is provided', () => {
render(<AvatarNetwork src="test-image.jpg" fallbackText="Eth" />);

const img = screen.getByRole('img');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', 'test-image.jpg');
expect(img).toHaveAttribute('alt', 'Eth');
});

it('renders fallbackText when src is not provided', () => {
render(<AvatarNetwork fallbackText="Eth" />);
expect(screen.getByText('Eth')).toBeInTheDocument();
});

it('applies fallbackTextProps to Text component', () => {
render(
<AvatarNetwork
fallbackText="Eth"
fallbackTextProps={{
color: TextColor.TextAlternative,
className: 'test-class',
'data-testid': 'fallback-text',
}}
/>,
);

const text = screen.getByTestId('fallback-text');
expect(text).toHaveClass('text-alternative', 'test-class');
});

it('applies custom className to root element', () => {
render(
<AvatarNetwork
fallbackText="Eth"
className="custom-class"
data-testid="avatar"
/>,
);

const avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('custom-class');
});

it('passes through additional image props when src is provided', () => {
render(
<AvatarNetwork
src="test-image.jpg"
fallbackText="Eth"
imageProps={{
loading: 'lazy',
}}
/>,
);

screen.debug();

const img = screen.getByRole('img');
expect(img).toHaveAttribute('loading', 'lazy');
});

it('applies size classes correctly', () => {
const { rerender } = render(
<AvatarNetwork
fallbackText="Eth"
size={AvatarNetworkSize.Xs}
data-testid="avatar"
/>,
);

let avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-4 w-4');

rerender(
<AvatarNetwork
fallbackText="Eth"
size={AvatarNetworkSize.Sm}
data-testid="avatar"
/>,
);
avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-6 w-6');

rerender(
<AvatarNetwork
fallbackText="Eth"
size={AvatarNetworkSize.Md}
data-testid="avatar"
/>,
);
avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-8 w-8');

rerender(
<AvatarNetwork
fallbackText="Eth"
size={AvatarNetworkSize.Lg}
data-testid="avatar"
/>,
);
avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-10 w-10');

rerender(
<AvatarNetwork
fallbackText="Eth"
size={AvatarNetworkSize.Xl}
data-testid="avatar"
/>,
);
avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-12 w-12');
});

it('uses medium size by default', () => {
render(<AvatarNetwork fallbackText="Eth" data-testid="avatar" />);
const avatar = screen.getByTestId('avatar');
expect(avatar).toHaveClass('h-8 w-8');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';

import { AvatarBase, AvatarBaseShape, AvatarBaseSize } from '../avatar-base';
import { Text } from '../text';
import { AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP } from './AvatarNetwork.constants';
import type { AvatarNetworkProps } from './AvatarNetwork.types';

export const AvatarNetwork = React.forwardRef<
HTMLDivElement,
AvatarNetworkProps
>(
(
{
src,
fallbackText,
fallbackTextProps,
className,
size = AvatarBaseSize.Md,
'data-testid': dataTestId,
imageProps,
...props
},
ref,
) => (
<AvatarBase
ref={ref}
shape={AvatarBaseShape.Square}
size={size}
className={className}
data-testid={dataTestId}
{...props}
>
{src ? (
<img
src={src}
alt={fallbackText}
className="w-full h-full object-cover"
{...imageProps}
/>
) : (
<Text
variant={AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP[size]}
asChild
{...fallbackTextProps}
>
<span>{fallbackText}</span>
</Text>
)}
</AvatarBase>
),
);

AvatarNetwork.displayName = 'AvatarNetwork';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ComponentProps } from 'react';

import type { TextProps } from '../text';
import { AvatarNetworkSize } from '.';

export type AvatarNetworkProps = Omit<
ComponentProps<'img'>,
'children' | 'size'
> & {
/**
* Optional URL for the network image
* When provided, displays the image instead of fallback text
*/
src?: string;
/**
* Optional prop to pass to the underlying img element
*/
imageProps?: ComponentProps<'img'>;
/**
* Optional prop to control the size of the avatar
* @default AvatarNetworkSize.Md
*/
size?: AvatarNetworkSize;
/**
* Required text to display when no image is provided
* Also used as alt text for the image when src is provided
*/
fallbackText: string;
/**
* Optional props to be passed to the Text component when rendering fallback text
* Only used when src is not provided
*/
fallbackTextProps?: Partial<
React.HTMLAttributes<HTMLSpanElement> & TextProps
>;
/**
* Optional additional CSS classes to be applied to the component
*/
className?: string;
/**
* Optional prop for testing purposes
* Passed to the root element
*/
'data-testid'?: string;
};
Loading

0 comments on commit 58f1ffc

Please sign in to comment.