-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Avatar and ImageInput components (#922)
* Create draft Avatar and ImageInput components * Add SVG placeholders for object and identity variants * Fix linting * Update Avatar prop names * Wrap Avatar in label in ImageInput instead of passing an as prop * Add icons and logic for removing an image * Clear file input via ref when the clear button is clicked A file input is always uncontrolled so a ref is necessary here: https://reactjs.org/docs/uncontrolled-components.html\#the-file-input-tag * Add @jsxRuntime pragma to enable usage with React 17 * Fix focus state UI bug where the outline appeared behind the ActionButton * Add active state styles * Update Avatar docs This also renames the variant prop options to object/identity to match the design specs. Person/business was inaccurate because a business should actually use the identity variant with a business placeholder (in development). * Switch Avatar default variant to object This matches the ImageInput defaults and makes the two components more interchangeable * Add onChange and onClear callbacks with loading and error states * Add snapshot tests for Avatar The Avatar is a purely visual component, so relying on snapshot testing makes sense here. It particularly ensures that the placeholder SVGs are set as expected wheen no image is provided. * Fix types in ImageInput spec to pass CI * Switch from brightness() to pseudo-elements and clean up styles * Do not call onChange with a falsy file param * Update ImageInput docs * Add initial tests for ImageInput Snapshots for main states (default, invalid, existing image) and functional test of an image upload via userEvent.upload * Add business logic tests for ImageInput * Rename ImageInput into AvatarInput * Add test for the logic of clearing an uploaded avatar * Properly export the components * Add pointer-events:none to spinner to fix FF loading UI * Address code review feedback * Optimize SVGs * Batched improvements (see commit description) - the border-radius of the zetta size was increased to 12px, the value will be moves to design tokens and used in the next Circuit major version - a bug on Chrome where uploading the same image twice would not fire onChange the second time was fixed by clearing the DOM input element on click - the AvatarInput was adapted to accept any component as a visual element, the Avatar or something else. A new story "Custom Component" was added to document it. I will add tests for it and rename AvatarInput back to ImageInput in follow-up commits * Rename AvatarInput back to ImageInput Since the last commit, it accepts any visual element, although it is still optimized for the Avatar component. This will enable teams to use the ImageInput for use cases beyond an avatar upload. * Move invalid border to inset This is to ensure that the button still stands out well even with invalid styles (from a design feedback) * Move add button out of label element - This makes the component's implementation cleaner - It makes the tests cleaner because there's only one matching label that we can query with getByLabelText - This commit also adds missing prop descriptions * Add snapshot test for rendering with a custom component * Write docs * Add changeset * Address PR review comments * Make the Avatar's alt prop required The ImageInput also exposes it as part of the custom component's signature, but it defaults to in that case. Accessibility docs were also updated and an extra usage example with a custom component was added to the ImageInput stories. * Update Avatar spec with required alt prop
- Loading branch information
Robin Métral
authored
Jun 3, 2021
1 parent
1f86a58
commit feb6b32
Showing
15 changed files
with
2,428 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). | ||
|
||
## Accessibility | ||
|
||
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" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
); | ||
}; |
Oops, something went wrong.
feb6b32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: