Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add avatar component #689

Open
wants to merge 16 commits into
base: release/1.1.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const parameters = {
],
'Form',
[
'Avatar',
['Documentation', 'Live', 'Without style', 'Class based'],
'Autocomplete',
['Documentation', 'Live', 'Without style', 'Class based'],
'Button',
Expand Down
124 changes: 124 additions & 0 deletions src/avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';

type SharedProps = React.HTMLAttributes<HTMLElement> & {
/**
* The class to pass to the parent div
*/
className?: string;
/**
* The shape variant we want to display (circle, rounded or square)
*/
shape?: 'circle' | 'rounded' | 'square';
/**
* The predefined background colours that can be set. These can be overwritten with the 'style' prop.
*/
backgroundColourOption?: 'default' | 'light' | 'dark';
/**
* The custom background colour
*/
backgroundColour?: string;
/**
* A href property to use on an anchor tag that wraps the avatars child components
*/
wrappingAnchorHref?: string;
cg-julian-taylor marked this conversation as resolved.
Show resolved Hide resolved
/**
* A target property to use on an anchor tag that wraps the avatars child components
*/
wrappingAnchorTarget?: '_blank' | '_self' | '_parent' | '_top';
cg-julian-taylor marked this conversation as resolved.
Show resolved Hide resolved
/**
* The desired width of the avatar component
*/
width?: string;
/**
* The desired height of the avatar component
*/
height?: string;
};

type AvatarProps = SharedProps & {
/**
* Src not used on non image avatars
*/
src?: never;
/**
* Alt not used on non image avatars
*/
alt?: never;
};

type ImageAvatarProps = SharedProps & {
cg-julian-taylor marked this conversation as resolved.
Show resolved Hide resolved
/**
* The src of an image we would like to display.
*/
src: string;
/**
* The alt text for the image element.
*/
alt?: string;
};

const shapeStyles = {
circle: {
borderRadius: '50%',
},
rounded: {
borderRadius: '4px',
},
square: {
borderRadius: '0',
},
};

const backgroundColourOptions = {
default: '#bdbdbd',
dark: '#000',
light: '#fff',
};

export const Avatar = ({
className,
shape = 'circle',
backgroundColourOption = 'default',
children,
src,
alt,
wrappingAnchorHref,
wrappingAnchorTarget,
width = '40px',
height = '40px',
backgroundColour,
...props
}: AvatarProps | ImageAvatarProps) => {
const backgroundColor =
cg-julian-taylor marked this conversation as resolved.
Show resolved Hide resolved
backgroundColour || backgroundColourOptions[backgroundColourOption];

let contents = src ? <img src={src} alt={alt} /> : children;

if (wrappingAnchorHref) {
contents = (
<a href={wrappingAnchorHref} target={wrappingAnchorTarget}>
{contents}
</a>
);
}

return (
<div
className={className}
{...props}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
width,
height,
backgroundColor,
...shapeStyles[shape],
...props.style,
}}
>
{contents}
</div>
);
};
152 changes: 152 additions & 0 deletions src/avatar/__tests__/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Avatar } from '../Avatar';

describe('Avatar', () => {
it('should render an image', () => {
const { container } = render(<Avatar src="test.jpg" />);
const img = screen.getByRole('img');
expect(img).toBeInTheDocument();
const div = container.querySelector('div');
expect(div).toBeInTheDocument();
});

it('should render an avatar with default shape', () => {
const { container } = render(<Avatar />);
const div = container.querySelector('div');
expect(div?.style.borderRadius).toBe('50%');
});

it('should render an avatar with circle shape styling', () => {
const { container } = render(<Avatar shape="circle" />);
const div = container.querySelector('div');
expect(div?.style.borderRadius).toBe('50%');
});

it('should render an avatar with rounded shape styling', () => {
const { container } = render(<Avatar shape="rounded" />);
const div = container.querySelector('div');
expect(div?.style.borderRadius).toBe('4px');
});

it('should render an avatar with custom styling', () => {
const { container } = render(
<Avatar shape="circle" style={{ fontSize: '28px' }} />
);
const div = container.querySelector('div');
expect(div?.style.borderRadius).toBe('50%');
expect(div?.style.fontSize).toBe('28px');
});

it('should render an circle variant avatar with custom styling', () => {
const { container } = render(
<Avatar shape="circle" style={{ fontSize: '28px' }} />
);
const div = container.querySelector('div');
expect(div?.style.fontSize).toBe('28px');
expect(div?.style.borderRadius).toBe('50%');
});

it('should render an avatar circle variant with custom styling merged into styles', () => {
const { container } = render(
<Avatar shape="circle" style={{ fontSize: '28px' }} />
);
const div = container.querySelector('div');
expect(div?.style.borderRadius).toBe('50%');
expect(div?.style.fontSize).toBe('28px');
});

it('should render an avatar with letters', () => {
const { container } = render(<Avatar>test</Avatar>);
const div = container.querySelector('div');
expect(div?.innerHTML).toContain('test');
});

it('should render with a classname', () => {
const { container } = render(<Avatar className="test">text</Avatar>);
const div = container.querySelector('div');
expect(div?.className).toBe('test');
});

it('should render with a default background colour', () => {
const { container } = render(<Avatar className="test">text</Avatar>);
const div = container.querySelector('div');
expect(div?.style.backgroundColor).toBe('rgb(189, 189, 189)');
});

it('should render with the selected preset background colour', () => {
let divPointer;
let containerPointer;

//dark theme
containerPointer = render(
<Avatar backgroundColourOption="dark">text</Avatar>
).container;
divPointer = containerPointer.querySelector('div');
expect(divPointer?.style.backgroundColor).toBe('rgb(0, 0, 0)');

// light theme
containerPointer = render(
<Avatar backgroundColourOption="light">text</Avatar>
).container;
divPointer = containerPointer.querySelector('div');
expect(divPointer?.style.backgroundColor).toBe('rgb(255, 255, 255)');
});

it('should render with a custom background colour', () => {
const { container } = render(
<Avatar className="test" backgroundColour="green">
text
</Avatar>
);
const div = container.querySelector('div');
expect(div?.style.backgroundColor).toBe('green');
});

it('should render an anchor tag if wrappingAnchorHref and wrappingAnchorTarget prop is passed', () => {
const testUrl = 'http://test.url/';

const { container } = render(
<Avatar
className="test"
wrappingAnchorHref={testUrl}
wrappingAnchorTarget="_blank"
>
text
</Avatar>
);
const anchor = container.querySelector('a');
expect(anchor?.href).toBe(testUrl);
expect(anchor?.target).toBe('_blank');
});

it('should not render wrappingAnchorTarget if wrappingAnchorHref prop is not passed', () => {
const { container } = render(
<Avatar className="test" wrappingAnchorTarget="_blank">
text
</Avatar>
);
const anchor = container.querySelector('a');
expect(anchor?.href).toBeUndefined();
expect(anchor?.target).toBeUndefined();
});

it('should have a default width and height', () => {
const { container } = render(<Avatar className="test">text</Avatar>);
const div = container.querySelector('div');
expect(div?.style.width).toBe('40px');
expect(div?.style.height).toBe('40px');
});

it('should accept a width and height prop', () => {
const { container } = render(
<Avatar className="test" width="4em" height="4em">
text
</Avatar>
);
const div = container.querySelector('div');
expect(div?.style.width).toBe('4em');
expect(div?.style.height).toBe('4em');
});
});
1 change: 1 addition & 0 deletions src/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Avatar } from './Avatar';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ export * from './highlight';
export * from './buttonGroup';
export * from './card';
export * from './skeleton';
export * from './avatar';
36 changes: 36 additions & 0 deletions stories/Avatar/ClassBased.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Avatar } from '../../src/avatar/Avatar';
import { useArgs } from '@storybook/preview-api';

export default {
title: 'DCXLibrary/Form/Avatar/Class based',
component: Avatar,
parameters: {
options: {
showPanel: true,
},
},
tags: ['autodocs'],
};

export const Basic = {
name: 'Basic',
args: {
children: 'JB'
},
};

export const WithImage = {
name: 'With Image',
args: {
src: 'https://www.capgemini.com/gb-en/wp-content/themes/capgemini-komposite/assets/images/logo.svg',
},
};

/**
* Avatar can be passed in different child properties such as links to external websites or custom components
*/
export const CustomContent = {
args: {
children: <a href="#" target="_blank">EL</a>,
},
};
Loading