From 90c4ea64cf562b8b7a88742601300e44a64aadb5 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 6 May 2021 11:05:42 +0200 Subject: [PATCH 01/36] Create draft Avatar and ImageInput components --- .../components/Avatar/Avatar.docs.mdx | 36 ++++++++ .../components/Avatar/Avatar.spec.tsx | 45 ++++++++++ .../components/Avatar/Avatar.stories.tsx | 76 ++++++++++++++++ .../circuit-ui/components/Avatar/Avatar.tsx | 81 +++++++++++++++++ .../circuit-ui/components/Avatar/index.ts | 20 +++++ .../components/ImageInput/ImageInput.docs.mdx | 20 +++++ .../components/ImageInput/ImageInput.spec.tsx | 45 ++++++++++ .../ImageInput/ImageInput.stories.tsx | 49 ++++++++++ .../components/ImageInput/ImageInput.tsx | 89 +++++++++++++++++++ .../circuit-ui/components/ImageInput/index.ts | 20 +++++ 10 files changed, 481 insertions(+) create mode 100644 packages/circuit-ui/components/Avatar/Avatar.docs.mdx create mode 100644 packages/circuit-ui/components/Avatar/Avatar.spec.tsx create mode 100644 packages/circuit-ui/components/Avatar/Avatar.stories.tsx create mode 100644 packages/circuit-ui/components/Avatar/Avatar.tsx create mode 100644 packages/circuit-ui/components/Avatar/index.ts create mode 100644 packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx create mode 100644 packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx create mode 100644 packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx create mode 100644 packages/circuit-ui/components/ImageInput/ImageInput.tsx create mode 100644 packages/circuit-ui/components/ImageInput/index.ts diff --git a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx new file mode 100644 index 0000000000..314fc8dbaf --- /dev/null +++ b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx @@ -0,0 +1,36 @@ +import { Status, Props, Story } from '../../../../.storybook/components'; + +# Avatar + + + +The Avatar component is used to display an avatar or an object image. Use the ImageInput component to allow users to upload an image. + + + + + +## Usage guidelines + +- **Do** … +- **Do not** … + +## Variants + +There are two main variants for the component, as well as two sizes. + +### Object variant + +For example for product images. + + + +### Identity variant + +For a merchant or business profile. + + + +### Sizes + + diff --git a/packages/circuit-ui/components/Avatar/Avatar.spec.tsx b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx new file mode 100644 index 0000000000..d9d81c5837 --- /dev/null +++ b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx @@ -0,0 +1,45 @@ +/** + * 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'; + +describe('Avatar', () => { + function renderAvatar(props: AvatarProps = {}, options = {}) { + return render(, options); + } + + describe('styles', () => { + it('should render with default styles', () => { + const { container } = renderAvatar(); + expect(container).toMatchSnapshot(); + }); + }); + + describe('business logic', () => { + it.todo('should have tests'); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const { container } = renderAvatar(); + const actual = await axe(container); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000000..2f86f41bfa --- /dev/null +++ b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx @@ -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 { Stack } from '../../../../.storybook/components'; + +import { Avatar, AvatarProps } from './Avatar'; +import docs from './Avatar.docs.mdx'; + +export default { + title: `Avatar`, + component: Avatar, + parameters: { + docs: { page: docs }, + }, + argTypes: { + imageUrl: { control: 'text' }, + variant: { control: { type: 'radio', options: ['square', 'round'] } }, + size: { control: { type: 'radio', options: ['large', 'small'] } }, + }, +}; + +export const base = (args: AvatarProps): JSX.Element => ; +base.args = { + imageUrl: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png', + variant: 'round', + size: 'large', +}; + +export const sizes = (): JSX.Element => ( + + + + +); + +export const identity = (): JSX.Element => ( + + + + +); + +export const object = (): JSX.Element => ( + + + + +); diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx new file mode 100644 index 0000000000..9fa15babe2 --- /dev/null +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -0,0 +1,81 @@ +/** + * 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 { 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 Omit, 'size'> { + /** + * Image to render + */ + imageUrl?: string; + /** + * Shape of the Avatar + */ + variant?: 'square' | 'round'; + /** + * Size of the Avatar + */ + size?: 'small' | 'large'; + /** + * Alternative DOM element to render + */ + as?: 'label'; + /** + * htmlFor when the element is a label + * TODO the element should extend either a div or a label + */ + htmlFor?: string; +} + +const avatarSizes = { + large: '96px', + small: '48px', +}; + +const baseStyles = ({ + theme, + imageUrl, + variant = 'square', + size = 'large', +}: AvatarProps & StyleProps) => css` + display: block; + width: ${avatarSizes[size]}; + height: ${avatarSizes[size]}; + box-shadow: inset 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); + border-radius: ${variant === 'round' + ? theme.borderRadius.circle + : theme.borderRadius.tera}; + border: none; + background-color: ${theme.colors.n200}; + + ${imageUrl && + css` + background-image: url(${imageUrl}); + background-size: cover; + background-position: center; + `}; +`; + +/** + * The Avatar component. + */ +export const Avatar = styled('div', { + shouldForwardProp: (prop) => isPropValid(prop), +})(baseStyles); diff --git a/packages/circuit-ui/components/Avatar/index.ts b/packages/circuit-ui/components/Avatar/index.ts new file mode 100644 index 0000000000..689a0f7cca --- /dev/null +++ b/packages/circuit-ui/components/Avatar/index.ts @@ -0,0 +1,20 @@ +/** + * 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 { Avatar } from './Avatar'; + +export type { AvatarProps } from './Avatar'; + +export default Avatar; diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx new file mode 100644 index 0000000000..04db83d7c2 --- /dev/null +++ b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx @@ -0,0 +1,20 @@ +import { Status, Props, Story } from '../../../../.storybook/components'; + +# ImageInput + + + +An image input to handle image uploads in forms. + + + + + +## Usage guidelines + +- **Do** … +- **Do not** … + +## Variants + +(other stories) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx new file mode 100644 index 0000000000..189d4f2854 --- /dev/null +++ b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx @@ -0,0 +1,45 @@ +/** + * 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 { ImageInput, ImageInputProps } from './ImageInput'; + +describe('ImageInput', () => { + function renderImageInput(props: ImageInputProps = {}, options = {}) { + return render(, options); + } + + describe('styles', () => { + it('should render with default styles', () => { + const { container } = renderImageInput(); + expect(container).toMatchSnapshot(); + }); + }); + + describe('business logic', () => { + it.todo('should have tests'); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const { container } = renderImageInput(); + const actual = await axe(container); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx new file mode 100644 index 0000000000..ccd1e51a3d --- /dev/null +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -0,0 +1,49 @@ +/** + * 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. + */ + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; + +import docs from './ImageInput.docs.mdx'; +import { ImageInputProps } from './ImageInput'; + +import ImageInput from '.'; + +export default { + title: `ImageInput`, + component: ImageInput, + parameters: { + docs: { page: docs }, + }, + argTypes: { + label: { control: 'text' }, + variant: { control: { type: 'radio', options: ['square', 'round'] } }, + }, +}; + +export const base = (args: ImageInputProps): JSX.Element => ( + +); + +base.args = { + label: 'Upload an image', +}; + +export const withImage = (): JSX.Element => ( + +); diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx new file mode 100644 index 0000000000..71b83444a5 --- /dev/null +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -0,0 +1,89 @@ +/** + * 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. + */ + +/** @jsx jsx */ +import { ChangeEvent, Fragment, InputHTMLAttributes, useState } from 'react'; +import { css, jsx } from '@emotion/core'; + +import Avatar from '../Avatar'; +import styled from '../../styles/styled'; +import { uniqueId } from '../../util/id'; +import { focusOutline, hideVisually } from '../../styles/style-mixins'; + +export interface ImageInputProps + extends Omit, 'size'> { + /** + * label + */ + label: string; + /** + * imageUrl + */ + imageUrl?: string; +} + +const Input = styled.input( + ({ theme }) => css` + ${hideVisually()}; + + &:focus + label { + ${focusOutline({ theme })}; + border-color: ${theme.colors.p500}; + } + `, +); + +const Image = styled(Avatar)` + :hover { + filter: brightness(90%); + cursor: pointer; + } +`; + +/** + * ImageInput component. + */ +export const ImageInput = ({ + label, + imageUrl: initialImageUrl, + id: customId, + ...props +}: ImageInputProps): JSX.Element => { + const id = customId || uniqueId('imageinput_'); + const [imageUrl, setImageUrl] = useState(initialImageUrl); + + const handleChange = (event: ChangeEvent) => { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + const image = URL.createObjectURL( + event.target.files && event.target.files[0], + ); + setImageUrl(image); + }; + + return ( + + + + {label} + + + ); +}; diff --git a/packages/circuit-ui/components/ImageInput/index.ts b/packages/circuit-ui/components/ImageInput/index.ts new file mode 100644 index 0000000000..0cea3c18a1 --- /dev/null +++ b/packages/circuit-ui/components/ImageInput/index.ts @@ -0,0 +1,20 @@ +/** + * 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 { ImageInput } from './ImageInput'; + +export type { ImageInputProps } from './ImageInput'; + +export default ImageInput; From 6e6b5b3610db3fdea0735b884f31e8c705e70894 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 6 May 2021 11:45:57 +0200 Subject: [PATCH 02/36] Add SVG placeholders for object and identity variants --- .../circuit-ui/components/Avatar/Avatar.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index 9fa15babe2..0d1dad17e0 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -49,6 +49,13 @@ const avatarSizes = { small: '48px', }; +const placeholders = { + round: + "", + square: + "", +}; + const baseStyles = ({ theme, imageUrl, @@ -59,18 +66,21 @@ const baseStyles = ({ width: ${avatarSizes[size]}; height: ${avatarSizes[size]}; box-shadow: inset 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); - border-radius: ${variant === 'round' - ? theme.borderRadius.circle - : theme.borderRadius.tera}; + border-radius: ${ + variant === 'round' ? theme.borderRadius.circle : theme.borderRadius.tera + }; border: none; - background-color: ${theme.colors.n200}; + background-color: ${theme.colors.n300}; + background-size: cover; + background-position: center; + background-image: url("data:image/svg+xml;utf8,${placeholders[variant]}"); - ${imageUrl && - css` - background-image: url(${imageUrl}); - background-size: cover; - background-position: center; - `}; + ${ + imageUrl && + css` + background-image: url(${imageUrl}); + ` + }; `; /** From ac2b34d198ab8203973a8827f8938376426093a6 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 6 May 2021 14:27:19 +0200 Subject: [PATCH 03/36] Fix linting --- .../components/Avatar/Avatar.spec.tsx | 2 +- .../components/Avatar/Avatar.stories.tsx | 2 +- .../circuit-ui/components/Avatar/Avatar.tsx | 18 ++++++++---------- .../components/ImageInput/ImageInput.spec.tsx | 7 +++++-- .../ImageInput/ImageInput.stories.tsx | 2 +- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.spec.tsx b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx index d9d81c5837..961eb0c841 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.spec.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx @@ -19,7 +19,7 @@ import { render, axe } from '../../util/test-utils'; import { Avatar, AvatarProps } from './Avatar'; -describe('Avatar', () => { +describe.skip('Avatar', () => { function renderAvatar(props: AvatarProps = {}, options = {}) { return render(, options); } diff --git a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx index 2f86f41bfa..17a684a019 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx @@ -21,7 +21,7 @@ import { Avatar, AvatarProps } from './Avatar'; import docs from './Avatar.docs.mdx'; export default { - title: `Avatar`, + title: 'Components/Avatar', component: Avatar, parameters: { docs: { page: docs }, diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index 0d1dad17e0..0efc8c908d 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -66,21 +66,19 @@ const baseStyles = ({ width: ${avatarSizes[size]}; height: ${avatarSizes[size]}; box-shadow: inset 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); - border-radius: ${ - variant === 'round' ? theme.borderRadius.circle : theme.borderRadius.tera - }; + border-radius: ${variant === 'round' + ? theme.borderRadius.circle + : theme.borderRadius.tera}; border: none; background-color: ${theme.colors.n300}; background-size: cover; background-position: center; - background-image: url("data:image/svg+xml;utf8,${placeholders[variant]}"); + background-image: url('data:image/svg+xml;utf8,${placeholders[variant]}'); - ${ - imageUrl && - css` - background-image: url(${imageUrl}); - ` - }; + ${imageUrl && + css` + background-image: url(${imageUrl}); + `}; `; /** diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx index 189d4f2854..603db9b2ad 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx @@ -19,8 +19,11 @@ import { render, axe } from '../../util/test-utils'; import { ImageInput, ImageInputProps } from './ImageInput'; -describe('ImageInput', () => { - function renderImageInput(props: ImageInputProps = {}, options = {}) { +describe.skip('ImageInput', () => { + function renderImageInput( + props: ImageInputProps = { label: 'Upload an image' }, + options = {}, + ) { return render(, options); } diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx index ccd1e51a3d..d3c986fd9e 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -22,7 +22,7 @@ import { ImageInputProps } from './ImageInput'; import ImageInput from '.'; export default { - title: `ImageInput`, + title: 'Components/ImageInput', component: ImageInput, parameters: { docs: { page: docs }, From 31fc08bf329469854cac6ea9dc85cf0785a7f303 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 6 May 2021 17:33:41 +0200 Subject: [PATCH 04/36] Update Avatar prop names --- .../components/Avatar/Avatar.stories.tsx | 24 ++++++++--------- .../circuit-ui/components/Avatar/Avatar.tsx | 26 +++++++++---------- .../ImageInput/ImageInput.stories.tsx | 4 --- .../components/ImageInput/ImageInput.tsx | 2 +- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx index 17a684a019..bdb6025488 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx @@ -28,28 +28,28 @@ export default { }, argTypes: { imageUrl: { control: 'text' }, - variant: { control: { type: 'radio', options: ['square', 'round'] } }, - size: { control: { type: 'radio', options: ['large', 'small'] } }, + variant: { control: { type: 'radio', options: ['business', 'person'] } }, + size: { control: { type: 'radio', options: ['yotta', 'giga'] } }, }, }; export const base = (args: AvatarProps): JSX.Element => ; base.args = { imageUrl: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png', - variant: 'round', - size: 'large', + variant: 'person', + size: 'yotta', }; export const sizes = (): JSX.Element => ( @@ -58,19 +58,19 @@ export const sizes = (): JSX.Element => ( export const identity = (): JSX.Element => ( - + ); export const object = (): JSX.Element => ( - + ); diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index 0efc8c908d..5569d3b3fb 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -22,17 +22,17 @@ import styled, { StyleProps } from '../../styles/styled'; export interface AvatarProps extends Omit, 'size'> { /** - * Image to render + * The URL of the Avatar image */ imageUrl?: string; /** - * Shape of the Avatar + * The variant of the Avatar, either representing a person or a business */ - variant?: 'square' | 'round'; + variant?: 'person' | 'business'; /** - * Size of the Avatar + * The size of the Avatar */ - size?: 'small' | 'large'; + size?: 'giga' | 'yotta'; /** * Alternative DOM element to render */ @@ -45,28 +45,26 @@ export interface AvatarProps } const avatarSizes = { - large: '96px', - small: '48px', + yotta: '96px', + giga: '48px', }; const placeholders = { - round: - "", - square: - "", + person: ``, + business: ``, }; const baseStyles = ({ theme, imageUrl, - variant = 'square', - size = 'large', + variant = 'person', + size = 'yotta', }: AvatarProps & StyleProps) => css` display: block; width: ${avatarSizes[size]}; height: ${avatarSizes[size]}; box-shadow: inset 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); - border-radius: ${variant === 'round' + border-radius: ${variant === 'person' ? theme.borderRadius.circle : theme.borderRadius.tera}; border: none; diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx index d3c986fd9e..739e4554c8 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -27,10 +27,6 @@ export default { parameters: { docs: { page: docs }, }, - argTypes: { - label: { control: 'text' }, - variant: { control: { type: 'radio', options: ['square', 'round'] } }, - }, }; export const base = (args: ImageInputProps): JSX.Element => ( diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 71b83444a5..7802e2932f 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -78,7 +78,7 @@ export const ImageInput = ({ From 2ec6862632301ef8a60901f8f4208f8c9ee6aa46 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Mon, 10 May 2021 15:29:45 +0200 Subject: [PATCH 05/36] Wrap Avatar in label in ImageInput instead of passing an as prop --- .../components/Avatar/Avatar.stories.tsx | 22 +++--- .../circuit-ui/components/Avatar/Avatar.tsx | 68 ++++++++++--------- .../components/ImageInput/ImageInput.spec.tsx | 2 +- .../ImageInput/ImageInput.stories.tsx | 1 + .../components/ImageInput/ImageInput.tsx | 27 +++++--- 5 files changed, 69 insertions(+), 51 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx index bdb6025488..201e5b7e1b 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx @@ -27,7 +27,8 @@ export default { docs: { page: docs }, }, argTypes: { - imageUrl: { control: 'text' }, + src: { control: 'text' }, + alt: { control: 'text' }, variant: { control: { type: 'radio', options: ['business', 'person'] } }, size: { control: { type: 'radio', options: ['yotta', 'giga'] } }, }, @@ -35,7 +36,8 @@ export default { export const base = (args: AvatarProps): JSX.Element => ; base.args = { - imageUrl: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png', + src: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png', + alt: '', variant: 'person', size: 'yotta', }; @@ -45,12 +47,14 @@ export const sizes = (): JSX.Element => ( ); @@ -59,9 +63,10 @@ export const identity = (): JSX.Element => ( - + ); @@ -69,8 +74,9 @@ export const object = (): JSX.Element => ( - + ); diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index 5569d3b3fb..bfbd597da4 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -13,18 +13,21 @@ * limitations under the License. */ -import { HTMLAttributes } from 'react'; +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 Omit, 'size'> { +export interface AvatarProps extends HTMLAttributes { /** - * The URL of the Avatar image + * The Avatar image source */ - imageUrl?: string; + src?: string; + /** + * Alt text for the Avatar + */ + alt: string; /** * The variant of the Avatar, either representing a person or a business */ @@ -33,15 +36,6 @@ export interface AvatarProps * The size of the Avatar */ size?: 'giga' | 'yotta'; - /** - * Alternative DOM element to render - */ - as?: 'label'; - /** - * htmlFor when the element is a label - * TODO the element should extend either a div or a label - */ - htmlFor?: string; } const avatarSizes = { @@ -54,34 +48,44 @@ const placeholders = { business: ``, }; +type StyledImageProps = Omit & { + variant: 'person' | 'business'; + size: 'giga' | 'yotta'; +}; + const baseStyles = ({ theme, - imageUrl, - variant = 'person', - size = 'yotta', -}: AvatarProps & StyleProps) => css` + variant, + size, +}: StyledImageProps & StyleProps) => css` display: block; width: ${avatarSizes[size]}; height: ${avatarSizes[size]}; - box-shadow: inset 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); + box-shadow: 0 0 0 ${theme.borderWidth.kilo} rgba(0, 0, 0, 0.1); + background-color: ${theme.colors.n300}; border-radius: ${variant === 'person' ? theme.borderRadius.circle : theme.borderRadius.tera}; - border: none; - background-color: ${theme.colors.n300}; - background-size: cover; - background-position: center; - background-image: url('data:image/svg+xml;utf8,${placeholders[variant]}'); - - ${imageUrl && - css` - background-image: url(${imageUrl}); - `}; + object-fit: cover; + object-position: center; `; +const StyledImage = styled('img', { + shouldForwardProp: (prop) => isPropValid(prop), +})(baseStyles); + /** * The Avatar component. */ -export const Avatar = styled('div', { - shouldForwardProp: (prop) => isPropValid(prop), -})(baseStyles); +export const Avatar = ({ + src: initialSrc, + alt = '', + variant = 'person', + size = 'yotta', + ...props +}: AvatarProps): JSX.Element => { + const src = initialSrc || `data:image/svg+xml;utf8,${placeholders[variant]}`; + return ( + + ); +}; diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx index 603db9b2ad..08b34df32f 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx @@ -21,7 +21,7 @@ import { ImageInput, ImageInputProps } from './ImageInput'; describe.skip('ImageInput', () => { function renderImageInput( - props: ImageInputProps = { label: 'Upload an image' }, + props: ImageInputProps = { label: 'Upload an image', alt: '' }, options = {}, ) { return render(, options); diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx index 739e4554c8..8156d50422 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -39,6 +39,7 @@ base.args = { export const withImage = (): JSX.Element => ( diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 7802e2932f..34e6ec5600 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -18,6 +18,7 @@ import { ChangeEvent, Fragment, InputHTMLAttributes, useState } from 'react'; import { css, jsx } from '@emotion/core'; import Avatar from '../Avatar'; +import Label from '../Label'; import styled from '../../styles/styled'; import { uniqueId } from '../../util/id'; import { focusOutline, hideVisually } from '../../styles/style-mixins'; @@ -28,6 +29,10 @@ export interface ImageInputProps * label */ label: string; + /** + * alt + */ + alt: string; /** * imageUrl */ @@ -40,12 +45,12 @@ const Input = styled.input( &:focus + label { ${focusOutline({ theme })}; - border-color: ${theme.colors.p500}; + border-radius: ${theme.borderRadius.tera}; } `, ); -const Image = styled(Avatar)` +const StyledAvatar = styled(Avatar)` :hover { filter: brightness(90%); cursor: pointer; @@ -58,6 +63,7 @@ const Image = styled(Avatar)` export const ImageInput = ({ label, imageUrl: initialImageUrl, + alt, id: customId, ...props }: ImageInputProps): JSX.Element => { @@ -74,16 +80,17 @@ export const ImageInput = ({ return ( - - + /> + ); }; From 42a0ca273f4a93b89b047f0a2f8a7c5ac267760d Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Tue, 11 May 2021 17:03:33 +0200 Subject: [PATCH 06/36] Add icons and logic for removing an image --- .../components/ImageInput/ImageInput.tsx | 98 ++++++++++++++++--- 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 34e6ec5600..bfb8080c9f 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -14,11 +14,13 @@ */ /** @jsx jsx */ -import { ChangeEvent, Fragment, InputHTMLAttributes, useState } from 'react'; +import { ChangeEvent, InputHTMLAttributes, useState } from 'react'; import { css, jsx } from '@emotion/core'; +import { Bin } from '@sumup/icons'; import Avatar from '../Avatar'; import Label from '../Label'; +import IconButton from '../IconButton'; import styled from '../../styles/styled'; import { uniqueId } from '../../util/id'; import { focusOutline, hideVisually } from '../../styles/style-mixins'; @@ -26,20 +28,24 @@ import { focusOutline, hideVisually } from '../../styles/style-mixins'; export interface ImageInputProps extends Omit, 'size'> { /** - * label + * A clear and concise description of the image input's purpose. */ label: string; /** - * alt + * A unique identifier for the input element. If not defined, a generated id is used. */ - alt: string; + id?: string; /** - * imageUrl + * An existing image URL to be displayed in the image input. */ imageUrl?: string; + /** + * An accessible label for the "remove" icon button. + */ + removeButtonLabel: string; } -const Input = styled.input( +const HiddenInput = styled.input( ({ theme }) => css` ${hideVisually()}; @@ -50,11 +56,29 @@ const Input = styled.input( `, ); -const StyledAvatar = styled(Avatar)` - :hover { - filter: brightness(90%); - cursor: pointer; - } +const StyledAvatar = styled(Avatar)( + ({ theme }) => css` + &:hover { + filter: brightness(90%); + cursor: pointer; + } + &:hover + button { + background-color: ${theme.colors.p900}; + border-color: ${theme.colors.p900}; + } + `, +); + +const ActionButton = styled(IconButton)( + ({ theme }) => css` + position: absolute; + right: -${theme.spacings.bit}; + bottom: -${theme.spacings.bit}; + `, +); + +const AddButton = styled(ActionButton)` + pointer-events: none; `; /** @@ -63,8 +87,8 @@ const StyledAvatar = styled(Avatar)` export const ImageInput = ({ label, imageUrl: initialImageUrl, - alt, id: customId, + removeButtonLabel, ...props }: ImageInputProps): JSX.Element => { const id = customId || uniqueId('imageinput_'); @@ -79,8 +103,12 @@ export const ImageInput = ({ }; return ( - - + - + {imageUrl && ( + setImageUrl(undefined)} + > + + + )} + ); }; From 4fcd7a0b1572695646eb56d9429acf0e36d18a22 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 20 May 2021 11:40:15 +0200 Subject: [PATCH 07/36] 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 --- .../components/ImageInput/ImageInput.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index bfb8080c9f..6ee9e79338 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -14,7 +14,7 @@ */ /** @jsx jsx */ -import { ChangeEvent, InputHTMLAttributes, useState } from 'react'; +import { useState, useRef, InputHTMLAttributes, ChangeEvent } from 'react'; import { css, jsx } from '@emotion/core'; import { Bin } from '@sumup/icons'; @@ -40,9 +40,9 @@ export interface ImageInputProps */ imageUrl?: string; /** - * An accessible label for the "remove" icon button. + * An accessible label for the "clear" icon button. */ - removeButtonLabel: string; + clearButtonLabel: string; } const HiddenInput = styled.input( @@ -88,9 +88,10 @@ export const ImageInput = ({ label, imageUrl: initialImageUrl, id: customId, - removeButtonLabel, + clearButtonLabel, ...props }: ImageInputProps): JSX.Element => { + const inputRef = useRef(null); const id = customId || uniqueId('imageinput_'); const [imageUrl, setImageUrl] = useState(initialImageUrl); @@ -102,6 +103,13 @@ export const ImageInput = ({ setImageUrl(image); }; + const handleClear = () => { + if (inputRef.current) { + setImageUrl(undefined); + inputRef.current.value = ''; + } + }; + return (
setImageUrl(undefined)} + label={clearButtonLabel} + onClick={handleClear} > From 7ad234b06fea8427882502cc0baef96df5a5e2ae Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 20 May 2021 11:45:06 +0200 Subject: [PATCH 08/36] Add @jsxRuntime pragma to enable usage with React 17 --- packages/circuit-ui/components/ImageInput/ImageInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 6ee9e79338..8c99d2cbd9 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -13,6 +13,7 @@ * limitations under the License. */ +/** @jsxRuntime classic */ /** @jsx jsx */ import { useState, useRef, InputHTMLAttributes, ChangeEvent } from 'react'; import { css, jsx } from '@emotion/core'; From d07466175cdfed3a5ccd0b5895170c7bfbfdeb41 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 20 May 2021 14:32:52 +0200 Subject: [PATCH 09/36] Fix focus state UI bug where the outline appeared behind the ActionButton --- packages/circuit-ui/components/ImageInput/ImageInput.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 8c99d2cbd9..8651b09b59 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -53,6 +53,8 @@ const HiddenInput = styled.input( &:focus + label { ${focusOutline({ theme })}; border-radius: ${theme.borderRadius.tera}; + // ensures the focus outline doesn't appear behind the ActionButton + border-bottom-right-radius: 12px; } `, ); From 2a7d67fc9b7dd46b686658d63f29385759bacfea Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 20 May 2021 14:34:05 +0200 Subject: [PATCH 10/36] Add active state styles --- packages/circuit-ui/components/ImageInput/ImageInput.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 8651b09b59..84d5309609 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -66,6 +66,13 @@ const StyledAvatar = styled(Avatar)( cursor: pointer; } &:hover + button { + background-color: ${theme.colors.p700}; + border-color: ${theme.colors.p700}; + } + &:active { + filter: brightness(80%); + } + &:active + button { background-color: ${theme.colors.p900}; border-color: ${theme.colors.p900}; } From 1b968269facfdb665a09ed4a13f6a1c306de56b1 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 20 May 2021 15:58:22 +0200 Subject: [PATCH 11/36] 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). --- .../components/Avatar/Avatar.docs.mdx | 21 +++---- .../components/Avatar/Avatar.stories.tsx | 60 ++++++++++--------- .../circuit-ui/components/Avatar/Avatar.tsx | 25 ++++---- .../components/ImageInput/ImageInput.docs.mdx | 6 +- .../ImageInput/ImageInput.stories.tsx | 3 +- .../components/ImageInput/ImageInput.tsx | 2 +- 6 files changed, 63 insertions(+), 54 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx index 314fc8dbaf..2a0574141f 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx +++ b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx @@ -4,7 +4,7 @@ import { Status, Props, Story } from '../../../../.storybook/components'; -The Avatar component is used to display an avatar or an object image. Use the ImageInput component to allow users to upload an image. +The Avatar component is used to display an avatar or an object image. It is used internally by the ImageInput component, to allow users to upload images. @@ -12,24 +12,25 @@ The Avatar component is used to display an avatar or an object image. Use the Im ## Usage guidelines -- **Do** … -- **Do not** … +- **Do** use the right variant for your use case (see _Variants_ below). +- **Do** use the ImageInput component to allow users to upload images. +- **Do not** add alt text if the Avatar is purely presentational (for example to show a thumbnail next to a user or product name). ## Variants -There are two main variants for the component, as well as two sizes. +There are two variants and two sizes available for the component. -### Object variant +### Identity variant -For example for product images. +Use the identity variant with a circle shape for identity account purposes (e.g. profile, contact, business). - + -### Identity variant +### Object variant -For a merchant or business profile. +Use the object variant with a rectangle shape for product item purposes (e.g. product catalogue). - + ### Sizes diff --git a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx index 201e5b7e1b..54bebbc05b 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.stories.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.stories.tsx @@ -29,7 +29,7 @@ export default { argTypes: { src: { control: 'text' }, alt: { control: 'text' }, - variant: { control: { type: 'radio', options: ['business', 'person'] } }, + variant: { control: { type: 'radio', options: ['object', 'identity'] } }, size: { control: { type: 'radio', options: ['yotta', 'giga'] } }, }, }; @@ -37,46 +37,52 @@ export default { export const base = (args: AvatarProps): JSX.Element => ; base.args = { src: 'https://upload.wikimedia.org/wikipedia/en/8/86/Avatar_Aang.png', - alt: '', - variant: 'person', + variant: 'identity', size: 'yotta', }; -export const sizes = (): JSX.Element => ( +export const identity = (): JSX.Element => ( - + ); -export const identity = (): JSX.Element => ( +export const object = (): JSX.Element => ( - - + + ); -export const object = (): JSX.Element => ( +export const sizes = (): JSX.Element => ( - - + + + + + + + + ); diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index bfbd597da4..a747528869 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -21,19 +21,22 @@ import styled, { StyleProps } from '../../styles/styled'; export interface AvatarProps extends HTMLAttributes { /** - * The Avatar image source + * The source of Avatar image. + * Defaults to a placeholder. */ src?: string; /** - * Alt text for the Avatar + * Alt text for the Avatar image. + * Defaults to "" for presentational elements (e.g. a small product image next to its name in a list). */ - alt: string; + alt?: string; /** - * The variant of the Avatar, either representing a person or a business + * 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 an src prop is not provided. */ - variant?: 'person' | 'business'; + variant?: 'identity' | 'object'; /** - * The size of the Avatar + * One of two available sizes for the Avatar. */ size?: 'giga' | 'yotta'; } @@ -44,12 +47,12 @@ const avatarSizes = { }; const placeholders = { - person: ``, - business: ``, + identity: ``, + object: ``, }; type StyledImageProps = Omit & { - variant: 'person' | 'business'; + variant: 'identity' | 'object'; size: 'giga' | 'yotta'; }; @@ -63,7 +66,7 @@ const baseStyles = ({ 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 === 'person' + border-radius: ${variant === 'identity' ? theme.borderRadius.circle : theme.borderRadius.tera}; object-fit: cover; @@ -80,7 +83,7 @@ const StyledImage = styled('img', { export const Avatar = ({ src: initialSrc, alt = '', - variant = 'person', + variant = 'identity', size = 'yotta', ...props }: AvatarProps): JSX.Element => { diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx index 04db83d7c2..d889c3b597 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx @@ -4,7 +4,7 @@ import { Status, Props, Story } from '../../../../.storybook/components'; -An image input to handle image uploads in forms. +The ImageInput component allows users to upload images, through drag and drop (desktop only), and local file browsing. It can be used on its own or as part of a form. @@ -15,6 +15,4 @@ An image input to handle image uploads in forms. - **Do** … - **Do not** … -## Variants - -(other stories) +## (TBD) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx index 8156d50422..330be858d3 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -35,12 +35,13 @@ export const base = (args: ImageInputProps): JSX.Element => ( base.args = { label: 'Upload an image', + clearButtonLabel: 'Clear', }; export const withImage = (): JSX.Element => ( ); diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 84d5309609..07149f5e4c 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -136,7 +136,7 @@ export const ImageInput = ({ />
`; @@ -79,7 +79,7 @@ exports[`Avatar styles should render the object variant placeholder 1`] = ` " + src="data:image/svg+xml;utf8," /> `; @@ -121,7 +121,7 @@ exports[`Avatar styles should render the yotta size 1`] = ` " + src="data:image/svg+xml;utf8," /> `; @@ -142,7 +142,7 @@ exports[`Avatar styles should render with default styles 1`] = ` " + src="data:image/svg+xml;utf8," /> `; diff --git a/packages/circuit-ui/components/AvatarInput/AvatarInput.tsx b/packages/circuit-ui/components/AvatarInput/AvatarInput.tsx index 92e9ffb9af..f41325916d 100644 --- a/packages/circuit-ui/components/AvatarInput/AvatarInput.tsx +++ b/packages/circuit-ui/components/AvatarInput/AvatarInput.tsx @@ -278,18 +278,10 @@ export const AvatarInput = ({ disabled={isLoading} > {/* FIXME add to @sumup/icons and upgrade the dependency in the next major */} - + diff --git a/packages/circuit-ui/components/AvatarInput/__snapshots__/AvatarInput.spec.tsx.snap b/packages/circuit-ui/components/AvatarInput/__snapshots__/AvatarInput.spec.tsx.snap index 64c80c47c3..9004366073 100644 --- a/packages/circuit-ui/components/AvatarInput/__snapshots__/AvatarInput.spec.tsx.snap +++ b/packages/circuit-ui/components/AvatarInput/__snapshots__/AvatarInput.spec.tsx.snap @@ -513,7 +513,7 @@ exports[`AvatarInput styles should render with default styles 1`] = ` " + src="data:image/svg+xml;utf8," /> - + + + Uploading @@ -618,7 +618,7 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` object-position: center; } -.circuit-4 { +.circuit-5 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -657,33 +657,33 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` pointer-events: none; } -.circuit-4:focus { +.circuit-5:focus { outline: 0; box-shadow: 0 0 0 4px #AFD0FE; } -.circuit-4:focus::-moz-focus-inner { +.circuit-5:focus::-moz-focus-inner { border: 0; } -.circuit-4:disabled, -.circuit-4[disabled] { +.circuit-5:disabled, +.circuit-5[disabled] { opacity: 0.5; pointer-events: none; box-shadow: none; } -.circuit-4:hover { +.circuit-5:hover { background-color: #234BC3; border-color: #234BC3; } -.circuit-4:active { +.circuit-5:active { background-color: #1A368E; border-color: #1A368E; } -.circuit-3 { +.circuit-4 { border: 0; -webkit-clip: rect(0 0 0 0); clip: rect(0 0 0 0); @@ -721,7 +721,7 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` pointer-events: none; } -.circuit-5 { +.circuit-3 { font-size: 13px; line-height: 20px; display: block; @@ -729,21 +729,21 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` overflow: hidden; } -.circuit-5:hover { +.circuit-3:hover { cursor: pointer; } -.circuit-5:hover > button { +.circuit-3:hover > button { background-color: #234BC3; border-color: #234BC3; } -.circuit-5:active > button { +.circuit-3:active > button { background-color: #1A368E; border-color: #1A368E; } -.circuit-5::after { +.circuit-3::after { content: ''; position: absolute; top: 0; @@ -754,11 +754,11 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` box-shadow: inset 0 0 0 2px #D23F47; } -.circuit-5:hover::after { +.circuit-3:hover::after { box-shadow: inset 0 0 0 2px #B22426; } -.circuit-5::before { +.circuit-3::before { content: ''; position: absolute; top: 0; @@ -771,11 +771,11 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` pointer-events: none; } -.circuit-5:hover::before { +.circuit-3:hover::before { opacity: 0.1; } -.circuit-5:active::before { +.circuit-3:active::before { opacity: 0.2; } @@ -820,7 +820,7 @@ exports[`ImageInput styles should render with invalid styles 1`] = ` type="file" /> + + + Uploading From 73275099676fe207e1b77c2849dca3ee489719db Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Mon, 31 May 2021 10:42:04 +0200 Subject: [PATCH 31/36] Add snapshot test for rendering with a custom component --- .../components/ImageInput/ImageInput.spec.tsx | 19 ++ .../__snapshots__/ImageInput.spec.tsx.snap | 266 ++++++++++++++++++ 2 files changed, 285 insertions(+) diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx index db62fc01c5..b26a53f49b 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.spec.tsx @@ -63,6 +63,25 @@ describe('ImageInput', () => { }); expect(container).toMatchSnapshot(); }); + + it('should render a custom component', () => { + const { container } = renderImageInput({ + ...defaultProps, + src: 'https://source.unsplash.com/EcWFOYOpkpY/800x200', + // eslint-disable-next-line react/display-name + component: ({ src }) => ( + + ), + }); + expect(container).toMatchSnapshot(); + }); }); const mockUploadFn = jest diff --git a/packages/circuit-ui/components/ImageInput/__snapshots__/ImageInput.spec.tsx.snap b/packages/circuit-ui/components/ImageInput/__snapshots__/ImageInput.spec.tsx.snap index f846640507..7ee0bd699d 100644 --- a/packages/circuit-ui/components/ImageInput/__snapshots__/ImageInput.spec.tsx.snap +++ b/packages/circuit-ui/components/ImageInput/__snapshots__/ImageInput.spec.tsx.snap @@ -1,5 +1,271 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`ImageInput styles should render a custom component 1`] = ` +@keyframes animation-0 { + 0% { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +.circuit-7 { + display: inline-block; + position: relative; + text-align: center; +} + +.circuit-0 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.circuit-0:focus + label { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; +} + +.circuit-0:focus + label::-moz-focus-inner { + border: 0; +} + +.circuit-2 { + font-size: 13px; + line-height: 20px; + display: block; + border-radius: 12px; + overflow: hidden; +} + +.circuit-2:hover { + cursor: pointer; +} + +.circuit-2:hover > button { + background-color: #234BC3; + border-color: #234BC3; +} + +.circuit-2:active > button { + background-color: #1A368E; + border-color: #1A368E; +} + +.circuit-2::before { + content: ''; + position: absolute; + top: 0; + left: 0%; + width: 100%; + height: 100%; + border-radius: 12px; + background-color: #000; + opacity: 0; + pointer-events: none; +} + +.circuit-2:hover::before { + opacity: 0.1; +} + +.circuit-2:active::before { + opacity: 0.2; +} + +.circuit-1 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.circuit-3 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.circuit-6 { + display: block; + width: 24px; + height: 24px; + border-radius: 100%; + border: 2px solid currentColor; + border-top-color: transparent; + -webkit-animation: animation-0 1s infinite linear; + animation: animation-0 1s infinite linear; + -webkit-transform-origin: 50% 50%; + -ms-transform-origin: 50% 50%; + transform-origin: 50% 50%; + position: absolute; + width: 32px; + height: 32px; + top: calc(50% - 16px); + left: calc(50% - 16px); + opacity: 0; + visibility: hidden; + -webkit-transition: opacity 120ms ease-in-out, visibility 120ms ease-in-out; + transition: opacity 120ms ease-in-out, visibility 120ms ease-in-out; + color: #FFF; + pointer-events: none; +} + +.circuit-4 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + height: auto; + margin: 0; + cursor: pointer; + font-size: 16px; + line-height: 24px; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-weight: 700; + border-width: 1px; + border-style: solid; + border-radius: 999999px; + -webkit-transition: opacity 120ms ease-in-out, color 120ms ease-in-out, background-color 120ms ease-in-out, border-color 120ms ease-in-out; + transition: opacity 120ms ease-in-out, color 120ms ease-in-out, background-color 120ms ease-in-out, border-color 120ms ease-in-out; + background-color: #D23F47; + border-color: #D23F47; + color: #FFF; + padding: 8px calc(24px - 1px); + padding: 8px; + position: absolute; + right: -4px; + bottom: -4px; +} + +.circuit-4:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; +} + +.circuit-4:focus::-moz-focus-inner { + border: 0; +} + +.circuit-4:disabled, +.circuit-4[disabled] { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +.circuit-4:hover { + background-color: #B22426; + border-color: #B22426; +} + +.circuit-4:active { + background-color: #941618; + border-color: #941618; +} + +
+
+ + + + + + Uploading + + +
+
+`; + exports[`ImageInput styles should render with an existing image 1`] = ` @keyframes animation-0 { 0% { From 0ae1158ce9998f9b3ed9eaebbf9cef95ee7e4fa8 Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Mon, 31 May 2021 12:21:27 +0200 Subject: [PATCH 32/36] Write docs --- .../components/Avatar/Avatar.docs.mdx | 15 ++++++-- .../components/ImageInput/ImageInput.docs.mdx | 37 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx index 05595c3761..8ab1e1acf1 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx +++ b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx @@ -4,7 +4,7 @@ import { Status, Props, Story } from '../../../../.storybook/components'; -The Avatar component is used to display an avatar or an object image. It can be replaced by an [ImageInput](Forms/ImageInput) to allow users to upload an avatar. +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. @@ -13,8 +13,17 @@ The Avatar component is used to display an avatar or an object image. It can be ## 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. -- **Do not** add alt text if the Avatar is purely presentational (for example to show a thumbnail next to a user or product name). +- **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 can receive alt text like any other image element. However, in many cases, alt text in this context will not make sense. + +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 will be redundant: assistive technology will already read out the names once. + +Therefore, the alt prop is not required and defaults to `""`, effectively making the Avatar invisible to assistive technology. + +Howver, bear in mind that the alt text is fundamental for accessibility if the Avatar is used without textual elements, for example as part of a products grid where only the images are shown. ## Variants diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx index ad4530b79c..5bc3565c3b 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx @@ -4,15 +4,42 @@ import { Status, Props, Story } from '../../../../.storybook/components'; -The ImageInput component allows users to upload avatar images. It can be used on its own or as part of a form. +The ImageInput component allows users to upload images. It can be used on its own or as part of a form. - + ## Usage guidelines -- **Do** use a short and concise alert message. Consider the maximum amount of characters in one line (50-60, including spaces) for an optimal readability. -- **Do not** use the ImageInput component if the avatar is read-only for the user, use the [Avatar](Components/Avatar) component instead. +Refer to the `Stateful` component story for a simplified code usage example. -## (TBD) +- **Do** use the onChange callback to immediately trigger an image upload when the user has selected one, even when the component is being used as part of a form (it will make form submission faster). +- **Do not** use the ImageInput component if the image is read-only for the user. Instead, use the visual component from the ImageInput's `component` prop by itself, for example the [Avatar](Components/Avatar). + +## Input states + +### Existing image + +When an `src` prop is provided, the ImageInput renders a "Clear" icon instead of the "Add" icon. It also forwards the prop to the visual component passed as a prop. Clicking the "Clear" icon clears the input. + + + +### Invalid + +When an image upload fails, set the `invalid` prop to true to render invalid styles, and show the user a clear and concise error message using the `validationHint` prop. + + + +## Custom component + +The ImageInput has drop-in support for the [Avatar](Components/Avatar) component as a visual element. + +However, where the end result image needs to be in a landscape view, a custom component with different dimensions can be used to reflect the final image's aspect ratio. + +There are some caveats with this approach: + +- A custom placeholder will also need to be provided to match the custom aspect ratio. Refer to the `CustomComponent` story for a code example. +- Currently, the `ImageInput` only supports rectangular components with a fixed border-radius, in order to correctly apply state-specific styles. + + From 5950ee9a2d5257eb467c388bdfccacbd25a33c8e Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Mon, 31 May 2021 12:25:41 +0200 Subject: [PATCH 33/36] Add changeset --- .changeset/sharp-mugs-marry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sharp-mugs-marry.md diff --git a/.changeset/sharp-mugs-marry.md b/.changeset/sharp-mugs-marry.md new file mode 100644 index 0000000000..bab1811469 --- /dev/null +++ b/.changeset/sharp-mugs-marry.md @@ -0,0 +1,5 @@ +--- +'@sumup/circuit-ui': minor +--- + +Added two new components, `Avatar` and `ImageInput`, to display and upload avatar images. From 5727c310f5b5b38e0a7b905b90f756845031d8bd Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Wed, 2 Jun 2021 17:13:37 +0200 Subject: [PATCH 34/36] Address PR review comments --- .changeset/cuddly-bats-buy.md | 5 +++++ .changeset/sharp-mugs-marry.md | 2 +- packages/circuit-ui/components/Avatar/Avatar.docs.mdx | 6 +++--- packages/circuit-ui/components/Avatar/Avatar.tsx | 2 +- .../circuit-ui/components/ImageInput/ImageInput.docs.mdx | 6 +++--- packages/circuit-ui/components/ImageInput/ImageInput.tsx | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 .changeset/cuddly-bats-buy.md diff --git a/.changeset/cuddly-bats-buy.md b/.changeset/cuddly-bats-buy.md new file mode 100644 index 0000000000..647a7ad99d --- /dev/null +++ b/.changeset/cuddly-bats-buy.md @@ -0,0 +1,5 @@ +--- +'@sumup/circuit-ui': minor +--- + +Added a new `ImageInput` component to allow users to upload images. diff --git a/.changeset/sharp-mugs-marry.md b/.changeset/sharp-mugs-marry.md index bab1811469..8d2b8408b4 100644 --- a/.changeset/sharp-mugs-marry.md +++ b/.changeset/sharp-mugs-marry.md @@ -2,4 +2,4 @@ '@sumup/circuit-ui': minor --- -Added two new components, `Avatar` and `ImageInput`, to display and upload avatar images. +Added a new `Avatar` component to display identity or object images. diff --git a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx index 8ab1e1acf1..0815fe3586 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx +++ b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx @@ -17,13 +17,13 @@ The Avatar component displays an identity or an object image. It can be passed t ## Accessibility -The Avatar can receive alt text like any other image element. However, in many cases, alt text in this context will not make sense. +The Avatar should have alt text, like any other image element. However, it can be omitted in some cases. -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 will be redundant: assistive technology will already read out the names once. +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 is redundant: assistive technology will already read out the names once. Therefore, the alt prop is not required and defaults to `""`, effectively making the Avatar invisible to assistive technology. -Howver, bear in mind that the alt text is fundamental for accessibility if the Avatar is used without textual elements, for example as part of a products grid where only the images are shown. +However, bear in mind that the alt text is fundamental for accessibility if the Avatar is used without textual elements, for example as part of a grid of products where only the images are shown. ## Variants diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index 5b0b584694..fa02d1db19 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -77,7 +77,7 @@ const StyledImage = styled('img', { })(baseStyles); /** - * The Avatar component. + * The Avatar component displays an identity or an object image. */ export const Avatar = ({ src, diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx index 5bc3565c3b..ec6e3cd41e 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.docs.mdx @@ -14,7 +14,7 @@ The ImageInput component allows users to upload images. It can be used on its ow Refer to the `Stateful` component story for a simplified code usage example. -- **Do** use the onChange callback to immediately trigger an image upload when the user has selected one, even when the component is being used as part of a form (it will make form submission faster). +- **Do** use the `onChange` callback to immediately trigger an image upload when the user has selected one, even when the component is being used as part of a form. Faster form submissions add to a delightful user experience. - **Do not** use the ImageInput component if the image is read-only for the user. Instead, use the visual component from the ImageInput's `component` prop by itself, for example the [Avatar](Components/Avatar). ## Input states @@ -35,11 +35,11 @@ When an image upload fails, set the `invalid` prop to true to render invalid sty The ImageInput has drop-in support for the [Avatar](Components/Avatar) component as a visual element. -However, where the end result image needs to be in a landscape view, a custom component with different dimensions can be used to reflect the final image's aspect ratio. +However, a custom component can also be used in other cases to display an ImageInput with different dimensions. There are some caveats with this approach: - A custom placeholder will also need to be provided to match the custom aspect ratio. Refer to the `CustomComponent` story for a code example. -- Currently, the `ImageInput` only supports rectangular components with a fixed border-radius, in order to correctly apply state-specific styles. +- Currently, the `ImageInput` only supports rectangular components in order to correctly apply state-specific styles. Please omit border-radius styles from your custom components, they will be set to a fixed 12px radius by the ImageInput. diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index b23917811e..3c697dc5c3 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -219,7 +219,7 @@ const LoadingIcon = styled(Spinner)( const LoadingLabel = styled.span(hideVisually); /** - * ImageInput component. + * The ImageInput component allows users to upload images. */ export const ImageInput = ({ label, From 1d4c229b26c0952c49fb081cbafc4b6dbaf92a3a Mon Sep 17 00:00:00 2001 From: Robin Metral Date: Thu, 3 Jun 2021 16:06:02 +0200 Subject: [PATCH 35/36] 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. --- .../components/Avatar/Avatar.docs.mdx | 8 +- .../circuit-ui/components/Avatar/Avatar.tsx | 5 +- .../ImageInput/ImageInput.stories.tsx | 81 ++++++++++++------- .../components/ImageInput/ImageInput.tsx | 5 +- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx index 0815fe3586..7cef16b82b 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.docs.mdx +++ b/packages/circuit-ui/components/Avatar/Avatar.docs.mdx @@ -17,13 +17,11 @@ The Avatar component displays an identity or an object image. It can be passed t ## Accessibility -The Avatar should have alt text, like any other image element. However, it can be omitted in some cases. +The Avatar has a required `alt` prop to ensure that it is accessible to visually impaired users. -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 is redundant: assistive technology will already read out the names once. +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. -Therefore, the alt prop is not required and defaults to `""`, effectively making the Avatar invisible to assistive technology. - -However, bear in mind that the alt text is fundamental for accessibility if the Avatar is used without textual elements, for example as part of a grid of products where only the images are shown. +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 diff --git a/packages/circuit-ui/components/Avatar/Avatar.tsx b/packages/circuit-ui/components/Avatar/Avatar.tsx index fa02d1db19..a312b12c63 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.tsx @@ -26,10 +26,9 @@ export interface AvatarProps extends HTMLAttributes { */ src?: string; /** - * Alt text for the Avatar image. - * Defaults to "" for presentational elements (e.g. a small product image next to its name in a list). + * Alt text for the Avatar image. Set it to "" if the image is presentational. */ - alt?: string; + 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. diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx index af5f56509a..113b8e22d6 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.stories.tsx @@ -13,6 +13,7 @@ * limitations under the License. */ +/** @jsxRuntime classic */ /** @jsx jsx */ import { useState } from 'react'; import { jsx } from '@emotion/core'; @@ -71,33 +72,6 @@ export const Invalid = (): JSX.Element => ( /> ); -export const CustomComponent = (): JSX.Element => ( - Promise.resolve()} - onClear={() => {}} - loadingLabel="Uploading" - component={({ src }) => ( - ' - } - alt="" - /> - )} - /> -); - export const Stateful = (): JSX.Element => { const [imageUrl, setImageUrl] = useState(''); const [error, setError] = useState(''); @@ -149,3 +123,56 @@ export const Stateful = (): JSX.Element => { /> ); }; + +export const CustomComponentImg = (): JSX.Element => ( + Promise.resolve()} + onClear={() => {}} + loadingLabel="Uploading" + component={({ src }) => ( + ' + } + alt="" // we don't need alt text because it is set by the ImageInput, but this fixes a jsx-a11y error + /> + )} + /> +); + +CustomComponentImg.storyName = 'Custom Component (with an img element)'; + +export const CustomComponentDiv = (): JSX.Element => ( + Promise.resolve()} + onClear={() => {}} + loadingLabel="Uploading" + component={({ src }) => ( +
+ )} + /> +); + +CustomComponentDiv.storyName = 'Custom Component (with a div element)'; diff --git a/packages/circuit-ui/components/ImageInput/ImageInput.tsx b/packages/circuit-ui/components/ImageInput/ImageInput.tsx index 3c697dc5c3..fbeecd9196 100644 --- a/packages/circuit-ui/components/ImageInput/ImageInput.tsx +++ b/packages/circuit-ui/components/ImageInput/ImageInput.tsx @@ -43,7 +43,7 @@ export interface ImageInputProps * The visual component to render as an image input. It should accept an src * prop to render the image. */ - component: ({ src }: { src?: string }) => JSX.Element; + component: ({ src, alt }: { src?: string; alt: string }) => JSX.Element; /** * A callback function to call when the user has selected an image. */ @@ -224,6 +224,7 @@ const LoadingLabel = styled.span(hideVisually); export const ImageInput = ({ label, src, + alt, id: customId, clearButtonLabel, onChange, @@ -291,7 +292,7 @@ export const ImageInput = ({ /> {label} - + {src ? ( Date: Thu, 3 Jun 2021 16:17:14 +0200 Subject: [PATCH 36/36] Update Avatar spec with required alt prop --- packages/circuit-ui/components/Avatar/Avatar.spec.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/circuit-ui/components/Avatar/Avatar.spec.tsx b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx index 26d171ef2b..5188c37131 100644 --- a/packages/circuit-ui/components/Avatar/Avatar.spec.tsx +++ b/packages/circuit-ui/components/Avatar/Avatar.spec.tsx @@ -27,7 +27,7 @@ const images = { }; describe('Avatar', () => { - function renderAvatar(props: AvatarProps = {}, options = {}) { + function renderAvatar(props: AvatarProps = { alt: '' }, options = {}) { return render(, options); } @@ -40,6 +40,7 @@ describe('Avatar', () => { it.each(sizes)('should render the %s size', (size) => { const { container } = renderAvatar({ size, + alt: '', }); expect(container).toMatchSnapshot(); }); @@ -50,6 +51,7 @@ describe('Avatar', () => { const { container } = renderAvatar({ src: images[variant], variant, + alt: '', }); expect(container).toMatchSnapshot(); }, @@ -58,6 +60,7 @@ describe('Avatar', () => { it.each(variants)('should render the %s variant placeholder', (variant) => { const { container } = renderAvatar({ variant, + alt: '', }); expect(container).toMatchSnapshot(); });