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 and ImageInput components #922

Merged
merged 36 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
90c4ea6
Create draft Avatar and ImageInput components
May 6, 2021
6e6b5b3
Add SVG placeholders for object and identity variants
May 6, 2021
ac2b34d
Fix linting
May 6, 2021
31fc08b
Update Avatar prop names
May 6, 2021
2ec6862
Wrap Avatar in label in ImageInput instead of passing an as prop
May 10, 2021
42a0ca2
Add icons and logic for removing an image
May 11, 2021
4fcd7a0
Clear file input via ref when the clear button is clicked
May 20, 2021
7ad234b
Add @jsxRuntime pragma to enable usage with React 17
May 20, 2021
d074661
Fix focus state UI bug where the outline appeared behind the ActionBu…
May 20, 2021
2a7d67f
Add active state styles
May 20, 2021
1b96826
Update Avatar docs
May 20, 2021
b93b5ac
Switch Avatar default variant to object
May 21, 2021
bceaece
Add onChange and onClear callbacks with loading and error states
May 25, 2021
0b37bad
Add snapshot tests for Avatar
May 25, 2021
dbb4f8c
Fix types in ImageInput spec to pass CI
May 25, 2021
48fa172
Switch from brightness() to pseudo-elements and clean up styles
May 25, 2021
52f3f0f
Do not call onChange with a falsy file param
May 25, 2021
6817573
Update ImageInput docs
May 25, 2021
a9716ca
Add initial tests for ImageInput
May 25, 2021
e162fe9
Add business logic tests for ImageInput
May 26, 2021
4c0a39e
Rename ImageInput into AvatarInput
May 26, 2021
40f10de
Add test for the logic of clearing an uploaded avatar
May 26, 2021
a73ffc4
Properly export the components
May 27, 2021
b729518
Add pointer-events:none to spinner to fix FF loading UI
May 27, 2021
018f6bf
Address code review feedback
May 27, 2021
c2cf4cf
Optimize SVGs
May 27, 2021
7b825bb
Batched improvements (see commit description)
May 28, 2021
a70e608
Rename AvatarInput back to ImageInput
May 28, 2021
1a6fe89
Move invalid border to inset
May 31, 2021
10d54aa
Move add button out of label element
May 31, 2021
7327509
Add snapshot test for rendering with a custom component
May 31, 2021
0ae1158
Write docs
May 31, 2021
5950ee9
Add changeset
May 31, 2021
5727c31
Address PR review comments
Jun 2, 2021
1d4c229
Make the Avatar's alt prop required
Jun 3, 2021
eb3121e
Update Avatar spec with required alt prop
Jun 3, 2021
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
5 changes: 5 additions & 0 deletions .changeset/cuddly-bats-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sumup/circuit-ui': minor
---

Added a new `ImageInput` component to allow users to upload images.
5 changes: 5 additions & 0 deletions .changeset/sharp-mugs-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sumup/circuit-ui': minor
---

Added a new `Avatar` component to display identity or object images.
44 changes: 44 additions & 0 deletions packages/circuit-ui/components/Avatar/Avatar.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Status, Props, Story } from '../../../../.storybook/components';

# Avatar

<Status.Stable />

The Avatar component displays an identity or an object image. It can be passed to the [ImageInput](Forms/ImageInput) to allow users to upload an avatar.

<Story id="components-avatar--base" />

<Props />

## Usage guidelines

- **Do** use the right variant for your use case (see _Variants_ below).
- **Do** use the [ImageInput component](Forms/ImageInput) with the `component={Avatar}` prop to allow users to upload an avatar (note: the ImageInput only supports `variant="object"` for now).
connor-baer marked this conversation as resolved.
Show resolved Hide resolved

## Accessibility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a dedicated Accessibility section! 🙌🏻

Some feedback: I would flip the order of the recommendations: adding alt text should be the default, omitting it the exception. It's better to have duplicate content than missing content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworded it to put more focus on when the alt text was necessary.

Do you think we should make alt a required prop and remove the "" default? In that case developers will need to explicitly pass "" if their images are presentational.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea. It forces developers to make a conscious decision.


The Avatar has a required `alt` prop to ensure that it is accessible to visually impaired users.

Alt text can be fundamental for accessibility, especially in interfaces without textual elements. For example, if the Avatar is used to render a grid of products where the names are not shown, omitting alt text would make it inaccessible.

However, if the image is purely presentational, the alt text can be set to `""`. For example, if the Avatar is used as an illustrative element in a products list next to each product's name, using the product name as alt text would be redundant: assistive technology will already read out the names once. In this case, setting `alt=""` will effectively make the Avatar invisible to assistive technology.

## Variants

There are two variants and two sizes available for the component.

### Object variant

Use the object variant with a square shape for product item purposes (e.g. product catalogue).

<Story id="components-avatar--object" />

### Identity variant

Use the identity variant with a circle shape for identity account purposes (e.g. profile, contact, business).

<Story id="components-avatar--identity" />

### Sizes

<Story id="components-avatar--sizes" />
76 changes: 76 additions & 0 deletions packages/circuit-ui/components/Avatar/Avatar.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright 2021, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';

import { render, axe } from '../../util/test-utils';

import { Avatar, AvatarProps } from './Avatar';

const sizes = ['giga', 'yotta'] as const;
const variants = ['object', 'identity'] as const;
const images = {
object: 'https://source.unsplash.com/EcWFOYOpkpY/200x200',
identity: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png',
};

describe('Avatar', () => {
function renderAvatar(props: AvatarProps = { alt: '' }, options = {}) {
return render(<Avatar {...props} />, options);
}

describe('styles', () => {
it('should render with default styles', () => {
const { container } = renderAvatar();
expect(container).toMatchSnapshot();
});

it.each(sizes)('should render the %s size', (size) => {
const { container } = renderAvatar({
size,
alt: '',
});
expect(container).toMatchSnapshot();
});

it.each(variants)(
'should render the %s variant with an image',
(variant) => {
const { container } = renderAvatar({
src: images[variant],
variant,
alt: '',
});
expect(container).toMatchSnapshot();
},
);

it.each(variants)('should render the %s variant placeholder', (variant) => {
const { container } = renderAvatar({
variant,
alt: '',
});
expect(container).toMatchSnapshot();
});
});

describe('accessibility', () => {
it('should meet accessibility guidelines', async () => {
const { container } = renderAvatar();
const actual = await axe(container);
expect(actual).toHaveNoViolations();
});
});
});
85 changes: 85 additions & 0 deletions packages/circuit-ui/components/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright 2021, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';

import { Stack } from '../../../../.storybook/components';

import { Avatar, AvatarProps } from './Avatar';
import docs from './Avatar.docs.mdx';

export default {
title: 'Components/Avatar',
component: Avatar,
parameters: {
docs: { page: docs },
},
};

export const Base = (args: AvatarProps): JSX.Element => <Avatar {...args} />;
Base.args = {
src: 'https://source.unsplash.com/EcWFOYOpkpY/200x200',
variant: 'object',
size: 'yotta',
};

export const ObjectVariant = (): JSX.Element => (
<Stack>
<Avatar
src="https://source.unsplash.com/EcWFOYOpkpY/200x200"
variant="object"
/>
<Avatar variant="object" />
</Stack>
);

export const IdentityVariant = (): JSX.Element => (
<Stack>
<Avatar
src="https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png"
variant="identity"
/>
<Avatar variant="identity" />
</Stack>
);

export const Sizes = (): JSX.Element => (
<Stack>
<Stack>
<Avatar
src="https://source.unsplash.com/EcWFOYOpkpY/200x200"
variant="object"
size="yotta"
/>
<Avatar
src="https://source.unsplash.com/EcWFOYOpkpY/200x200"
variant="object"
size="giga"
/>
</Stack>
<Stack>
<Avatar
src="https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png"
variant="identity"
size="yotta"
/>
<Avatar
src="https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png"
variant="identity"
size="giga"
/>
</Stack>
</Stack>
);
98 changes: 98 additions & 0 deletions packages/circuit-ui/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright 2021, SumUp Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React, { HTMLAttributes } from 'react';
import { css } from '@emotion/core';
import isPropValid from '@emotion/is-prop-valid';

import styled, { StyleProps } from '../../styles/styled';

export interface AvatarProps extends HTMLAttributes<HTMLImageElement> {
/**
* The source URL of the Avatar image.
* Defaults to a placeholder illustration.
*/
src?: string;
/**
* Alt text for the Avatar image. Set it to "" if the image is presentational.
*/
alt: string;
/**
* The variant of the Avatar, either identity or object. Refer to the docs for usage guidelines.
* The variant also changes which placeholder is rendered when the `src` prop is not provided.
*/
variant?: 'object' | 'identity';
/**
* One of two available sizes for the Avatar, either giga or yotta.
*/
size?: 'giga' | 'yotta';
}

const avatarSizes = {
yotta: '96px',
giga: '48px',
};

const placeholders = {
object: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 96 96"><path fill="white" d="M30 25c0-4.9706 4.0294-9 9-9s9 4.0294 9 9-4.0294 9-9 9-9-4.0294-9-9zM41.1571 60.5691L30.6742 48.3905c-1.6438-1.9097-4.6225-1.8422-6.1782.1399L8 69.5483v12.4515c0 3.3137 2.6863 6 6 6h5.9592l21.1979-27.4307zM70.4856 32.878c1.5553-2.002 4.5569-2.0705 6.202-.1417l11.312 13.2623v36c0 3.3137-2.6863 6-6 6H27.6611L70.4856 32.878z"/></svg>`,
identity: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 96 96"><path fill="white" d="M48 18c-7.732 0-14 6.268-14 14s6.268 14 14 14 14-6.268 14-14-6.268-14-14-14zM47.9998 88C61.53 88 73.4913 81.2822 80.73 71c-7.2387-10.2822-19.2-17-32.7303-17-13.5302 0-25.4914 6.7178-32.7302 17 7.2388 10.2822 19.2 17 32.7303 17z"/></svg>`,
};

const baseStyles = ({
theme,
variant,
size = 'yotta',
}: AvatarProps & StyleProps) => css`
display: block;
width: ${avatarSizes[size]};
height: ${avatarSizes[size]};
box-shadow: 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1);
background-color: ${theme.colors.n300};
border-radius: ${variant === 'identity'
? theme.borderRadius.circle
: /**
* @FIXME add this value to design tokens and upgrade in the next major
* to use it here and in the ImageInput
*/
'12px'};
object-fit: cover;
object-position: center;
`;

const StyledImage = styled('img', {
shouldForwardProp: (prop) => isPropValid(prop),
})<AvatarProps>(baseStyles);

/**
* The Avatar component displays an identity or an object image.
*/
export const Avatar = ({
src,
alt = '',
variant = 'object',
size,
...props
}: AvatarProps): JSX.Element => {
const placeholder = `data:image/svg+xml;utf8,${placeholders[variant]}`;
return (
<StyledImage
src={src || placeholder}
alt={alt}
variant={variant}
size={size}
{...props}
/>
);
};
Loading