diff --git a/.storybook/preview.js b/.storybook/preview.js index 60c4a2d6..6ab8fbf9 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -24,6 +24,8 @@ export const parameters = { ], 'Form', [ + 'Avatar', + ['Documentation', 'Live', 'Without style', 'Class based'], 'Autocomplete', ['Documentation', 'Live', 'Without style', 'Class based'], 'Button', diff --git a/src/avatar/Avatar.tsx b/src/avatar/Avatar.tsx new file mode 100644 index 00000000..191a4b2b --- /dev/null +++ b/src/avatar/Avatar.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +type AvatarProps = React.HTMLAttributes & { + /** + * 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'; + /** + * A target url that the avatar should link to + */ + avatarLink?: string; + /** + * The target behaviour that should be used with the `avatarLink` prop + */ + avatarLinkTarget?: '_blank' | '_self' | '_parent' | '_top'; + /** + * The desired width of the avatar component + */ + width?: string; + /** + * The desired height of the avatar component + */ + height?: string; + /** + * 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', + }, +}; + +export const Avatar = ({ + className, + shape = 'circle', + children, + src, + alt, + avatarLink, + avatarLinkTarget, + width = '40px', + height = '40px', + ...props +}: AvatarProps) => { + let contents = src ? {alt} : children; + + if (avatarLink) { + contents = ( + + {contents} + + ); + } + + return ( +
+ {contents} +
+ ); +}; diff --git a/src/avatar/__tests__/Avatar.test.tsx b/src/avatar/__tests__/Avatar.test.tsx new file mode 100644 index 00000000..c8450679 --- /dev/null +++ b/src/avatar/__tests__/Avatar.test.tsx @@ -0,0 +1,117 @@ +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(); + 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(); + const div = container.querySelector('div'); + expect(div?.style.borderRadius).toBe('50%'); + }); + + it('should render an avatar with circle shape styling', () => { + const { container } = render(); + const div = container.querySelector('div'); + expect(div?.style.borderRadius).toBe('50%'); + }); + + it('should render an avatar with rounded shape styling', () => { + const { container } = render(); + const div = container.querySelector('div'); + expect(div?.style.borderRadius).toBe('4px'); + }); + + it('should render an avatar with custom styling', () => { + const { container } = render( + + ); + 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( + + ); + 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( + + ); + 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(test); + const div = container.querySelector('div'); + expect(div?.innerHTML).toContain('test'); + }); + + it('should render with a classname', () => { + const { container } = render(text); + const div = container.querySelector('div'); + expect(div?.className).toBe('test'); + }); + + it('should render an anchor tag if avatarLink and avatarLinkTarget prop is passed', () => { + const testUrl = 'http://test.url/'; + + const { container } = render( + + text + + ); + const anchor = container.querySelector('a'); + expect(anchor?.href).toBe(testUrl); + expect(anchor?.target).toBe('_blank'); + }); + + it('should not render avatarLinkTarget if avatarLink prop is not passed', () => { + const { container } = render( + + text + + ); + const anchor = container.querySelector('a'); + expect(anchor?.href).toBeUndefined(); + expect(anchor?.target).toBeUndefined(); + }); + + it('should have a default width and height', () => { + const { container } = render(text); + 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( + + text + + ); + const div = container.querySelector('div'); + expect(div?.style.width).toBe('4em'); + expect(div?.style.height).toBe('4em'); + }); +}); diff --git a/src/avatar/index.ts b/src/avatar/index.ts new file mode 100644 index 00000000..d3fb6dfa --- /dev/null +++ b/src/avatar/index.ts @@ -0,0 +1 @@ +export { Avatar } from './Avatar'; diff --git a/src/index.ts b/src/index.ts index 9e3f5770..a2bbae57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,3 +38,4 @@ export * from './highlight'; export * from './buttonGroup'; export * from './card'; export * from './skeleton'; +export * from './avatar'; \ No newline at end of file diff --git a/stories/Avatar/ClassBased.stories.js b/stories/Avatar/ClassBased.stories.js new file mode 100644 index 00000000..930591ac --- /dev/null +++ b/stories/Avatar/ClassBased.stories.js @@ -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: EL, + }, +}; diff --git a/stories/Avatar/Documentation.mdx b/stories/Avatar/Documentation.mdx new file mode 100644 index 00000000..f2d0a0fe --- /dev/null +++ b/stories/Avatar/Documentation.mdx @@ -0,0 +1,27 @@ +import { Meta, Canvas, ArgTypes } from '@storybook/blocks'; +import * as AvatarStories from './UnStyled.stories'; + + + +# Avatar + +An Avatar component to display either an image or child component such as initials or an svg. + +When you import the avatar component without providing any className or style associated it will look like this: + + + +Styles can be passed as a prop to customise the look and feel. + +```js +EL +``` + + \ No newline at end of file diff --git a/stories/Avatar/Live.stories.js b/stories/Avatar/Live.stories.js new file mode 100644 index 00000000..da2d5d8f --- /dev/null +++ b/stories/Avatar/Live.stories.js @@ -0,0 +1,23 @@ +import AvatarLive from '../liveEdit/AvatarLive'; +import { Avatar } from '../../src/avatar/Avatar'; + +export default { + title: 'DCXLibrary/Form/Avatar/Live', + component: Avatar, + + parameters: { + options: { + showPanel: false, + }, + viewMode: 'docs', + previewTabs: { + canvas: { + hidden: true, + }, + }, + }, +}; + +export const Live = { + render: () => , +}; diff --git a/stories/Avatar/UnStyled.stories.js b/stories/Avatar/UnStyled.stories.js new file mode 100644 index 00000000..6776acd2 --- /dev/null +++ b/stories/Avatar/UnStyled.stories.js @@ -0,0 +1,25 @@ +import { Avatar } from '../../src/avatar/Avatar'; + +export default { + title: 'DCXLibrary/Form/Avatar/Without style', + component: Avatar, + parameters: { + options: { + showPanel: true, + }, + }, + argTypes: { + children: { + description: 'Allows you to add an element as children', + }, + }, +}; + +export const Unstyled = { + args: { + avatarLink: 'http://localhost/', + children: [ + 'EL', + ], + }, +}; diff --git a/stories/liveEdit/AvatarLive.tsx b/stories/liveEdit/AvatarLive.tsx new file mode 100644 index 00000000..43d18871 --- /dev/null +++ b/stories/liveEdit/AvatarLive.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live'; +import { Avatar } from '../../src/avatar/Avatar'; + +const AvatarDemo = ` +function AvatarDemo() { + + const style = { + border: '1px solid black' + } + + return ( + JB + ) +} +`.trim(); + +const AvatarLive = () => { + const scope = { Avatar }; + return ( + +
+ + +
+ +
+ ); +}; + +export default AvatarLive;