Skip to content

Commit 562b400

Browse files
solimantLFDanLu
andauthored
Implement Avatar component (#2104)
* Implement Avatar component * Use TS no-redeclare ESLint rule * Make useAvatar support more element types * Create useAvatar stories * Consolidate useAvatar into Avatar Co-authored-by: Daniel Lu <[email protected]>
1 parent 5360a76 commit 562b400

File tree

15 files changed

+410
-0
lines changed

15 files changed

+410
-0
lines changed

packages/@adobe/spectrum-css-temp/components/avatar/skin.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ governing permissions and limitations under the License.
1212

1313
.spectrum-Avatar {
1414
opacity: var(--spectrum-avatar-small-opacity);
15+
outline: none;
16+
transition: box-shadow var(--spectrum-global-animation-duration-100) ease-out;
1517

1618
&.is-disabled {
1719
opacity: var(--spectrum-avatar-small-opacity-disabled);

packages/@adobe/spectrum-css-temp/vars/spectrum-global.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@
451451
--spectrum-alias-heading-xxs-text-size: var(--spectrum-global-dimension-font-size-100);
452452
--spectrum-alias-heading6-margin-top: var(--spectrum-global-dimension-font-size-75);
453453
--spectrum-alias-heading-xxs-margin-top: var(--spectrum-global-dimension-font-size-75);
454+
--spectrum-alias-avatar-size-50: var(--spectrum-global-dimension-size-200);
455+
--spectrum-alias-avatar-size-75: var(--spectrum-global-dimension-size-225);
456+
--spectrum-alias-avatar-size-200: var(--spectrum-global-dimension-size-275);
457+
--spectrum-alias-avatar-size-300: var(--spectrum-global-dimension-size-325);
458+
--spectrum-alias-avatar-size-500: var(--spectrum-global-dimension-size-400);
459+
--spectrum-alias-avatar-size-700: var(--spectrum-global-dimension-size-500);
454460
}
455461

456462
.spectrum--darkest,

packages/@adobe/spectrum-css-temp/vars/spectrum-large.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
--spectrum-global-dimension-size-200: 20px;
3131
--spectrum-global-dimension-size-225: 22px;
3232
--spectrum-global-dimension-size-250: 25px;
33+
--spectrum-global-dimension-size-275: 28px;
3334
--spectrum-global-dimension-size-300: 30px;
35+
--spectrum-global-dimension-size-325: 32px;
3436
--spectrum-global-dimension-size-350: 35px;
3537
--spectrum-global-dimension-size-400: 40px;
3638
--spectrum-global-dimension-size-450: 45px;
@@ -71,6 +73,9 @@
7173
--spectrum-global-dimension-font-size-1100: 55px;
7274
--spectrum-global-dimension-font-size-1200: 62px;
7375
--spectrum-global-dimension-font-size-1300: 70px;
76+
--spectrum-alias-avatar-size-100: 26px;
77+
--spectrum-alias-avatar-size-400: 36px;
78+
--spectrum-alias-avatar-size-600: 46px;
7479
--spectrum-actionbutton-touch-hit-x: var(--spectrum-global-dimension-static-size-50);
7580
--spectrum-actionbutton-touch-hit-y: var(--spectrum-global-dimension-static-size-50);
7681
--spectrum-actionbutton-emphasized-touch-hit-x: var(--spectrum-global-dimension-static-size-50);

packages/@adobe/spectrum-css-temp/vars/spectrum-medium.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
--spectrum-global-dimension-size-200: 16px;
3131
--spectrum-global-dimension-size-225: 18px;
3232
--spectrum-global-dimension-size-250: 20px;
33+
--spectrum-global-dimension-size-275: 22px;
3334
--spectrum-global-dimension-size-300: 24px;
35+
--spectrum-global-dimension-size-325: 26px;
3436
--spectrum-global-dimension-size-350: 28px;
3537
--spectrum-global-dimension-size-400: 32px;
3638
--spectrum-global-dimension-size-450: 36px;
@@ -71,6 +73,9 @@
7173
--spectrum-global-dimension-font-size-1100: 45px;
7274
--spectrum-global-dimension-font-size-1200: 50px;
7375
--spectrum-global-dimension-font-size-1300: 60px;
76+
--spectrum-alias-avatar-size-100: var(--spectrum-global-dimension-size-250);
77+
--spectrum-alias-avatar-size-400: var(--spectrum-global-dimension-size-350);
78+
--spectrum-alias-avatar-size-600: var(--spectrum-global-dimension-size-450);
7479
--spectrum-actionbutton-touch-hit-x: var(--spectrum-global-dimension-static-size-100);
7580
--spectrum-actionbutton-touch-hit-y: var(--spectrum-global-dimension-static-size-100);
7681
--spectrum-actionbutton-emphasized-touch-hit-x: var(--spectrum-global-dimension-static-size-100);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @react-spectrum/avatar
2+
3+
This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {Avatar} from '../';
14+
import {Meta, Story} from '@storybook/react';
15+
import React from 'react';
16+
import {SpectrumAvatarProps} from '@react-types/avatar';
17+
18+
const SRC_URL_1 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';
19+
const SRC_URL_2 = 'https://i.imgur.com/xIe7Wlb.png';
20+
21+
const meta: Meta<SpectrumAvatarProps> = {
22+
title: 'Avatar',
23+
component: Avatar
24+
};
25+
26+
export default meta;
27+
28+
const AvatarTemplate: Story<SpectrumAvatarProps> = (args) => (
29+
<Avatar {...args} />
30+
);
31+
32+
export const Default = AvatarTemplate.bind({});
33+
Default.args = {src: SRC_URL_1};
34+
Default.storyName = 'default';
35+
36+
export const Disabled = AvatarTemplate.bind({});
37+
Disabled.args = {isDisabled: true, src: SRC_URL_1};
38+
Disabled.storyName = 'isDisabled';
39+
40+
export const CustomSize = AvatarTemplate.bind({});
41+
CustomSize.args = {size: 'avatar-size-700', src: SRC_URL_2};
42+
CustomSize.storyName = 'with custom size';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
export * from './src';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@react-spectrum/avatar",
3+
"version": "3.0.0-alpha.0",
4+
"description": "Spectrum UI components in React",
5+
"license": "Apache-2.0",
6+
"private": true,
7+
"main": "dist/main.js",
8+
"module": "dist/module.js",
9+
"types": "dist/types.d.ts",
10+
"source": "src/index.ts",
11+
"files": [
12+
"dist",
13+
"src"
14+
],
15+
"sideEffects": [
16+
"*.css"
17+
],
18+
"targets": {
19+
"main": {
20+
"includeNodeModules": [
21+
"@adobe/spectrum-css-temp"
22+
]
23+
},
24+
"module": {
25+
"includeNodeModules": [
26+
"@adobe/spectrum-css-temp"
27+
]
28+
}
29+
},
30+
"repository": {
31+
"type": "git",
32+
"url": "https://github.com/adobe/react-spectrum"
33+
},
34+
"dependencies": {
35+
"@babel/runtime": "^7.6.2",
36+
"@react-aria/utils": "^3.8.2",
37+
"@react-spectrum/utils": "^3.6.1",
38+
"@react-types/avatar": "3.0.0-alpha.0",
39+
"@react-types/shared": "^3.7.1"
40+
},
41+
"devDependencies": {
42+
"@adobe/spectrum-css-temp": "3.0.0-alpha.1"
43+
},
44+
"peerDependencies": {
45+
"@react-spectrum/provider": "^3.2.1",
46+
"react": "^16.8.0 || ^17.0.0-rc.1"
47+
},
48+
"publishConfig": {
49+
"access": "public"
50+
}
51+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {classNames, dimensionValue, useDOMRef, useStyleProps} from '@react-spectrum/utils';
14+
import {DOMRef} from '@react-types/shared';
15+
import {filterDOMProps} from '@react-aria/utils';
16+
import React, {forwardRef} from 'react';
17+
import {SpectrumAvatarProps} from '@react-types/avatar';
18+
import styles from '@adobe/spectrum-css-temp/components/avatar/vars.css';
19+
import {useProviderProps} from '@react-spectrum/provider';
20+
21+
const DEFAULT_SIZE = 'avatar-size-100';
22+
const SIZE_RE = /^size-\d+/;
23+
24+
function Avatar(props: SpectrumAvatarProps, ref: DOMRef<HTMLImageElement>) {
25+
const {
26+
alt = '',
27+
isDisabled,
28+
size,
29+
src,
30+
...otherProps
31+
} = useProviderProps(props);
32+
33+
const {styleProps} = useStyleProps(otherProps);
34+
const domRef = useDOMRef(ref);
35+
36+
const domProps = filterDOMProps(otherProps);
37+
38+
// Casting `size` as `any` since `isNaN` expects a `number`, but we want it
39+
// to handle `string` numbers; e.g. '300' as opposed to 300
40+
const sizeValue = typeof size !== 'number' && (SIZE_RE.test(size) || !isNaN(size as any))
41+
? dimensionValue(DEFAULT_SIZE) // override disallowed size values
42+
: dimensionValue(size || DEFAULT_SIZE);
43+
44+
return (
45+
<img
46+
{...styleProps}
47+
{...domProps}
48+
alt={alt}
49+
className={classNames(
50+
styles,
51+
'spectrum-Avatar',
52+
{
53+
'is-disabled': isDisabled
54+
},
55+
styleProps.className)}
56+
ref={domRef}
57+
src={src}
58+
style={{
59+
...styleProps.style,
60+
...(sizeValue && {height: sizeValue, width: sizeValue})
61+
}} />
62+
);
63+
}
64+
65+
/**
66+
* An avatar is a thumbnail representation of an entity, such as a user or an organization.
67+
*/
68+
const _Avatar = forwardRef(Avatar);
69+
export {_Avatar as Avatar};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/// <reference types="css-module-types" />
14+
15+
export * from './Avatar';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2021 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {Avatar} from '../';
14+
import {Meta, Story} from '@storybook/react';
15+
import React from 'react';
16+
import {SpectrumAvatarProps} from '@react-types/avatar';
17+
18+
const SRC_URL_1 = 'https://mir-s3-cdn-cf.behance.net/project_modules/disp/690bc6105945313.5f84bfc9de488.png';
19+
const SRC_URL_2 = 'https://i.imgur.com/xIe7Wlb.png';
20+
21+
const meta: Meta<SpectrumAvatarProps> = {
22+
title: 'Avatar',
23+
component: Avatar
24+
};
25+
26+
export default meta;
27+
28+
const AvatarTemplate: Story<SpectrumAvatarProps> = (args) => (
29+
<Avatar {...args} />
30+
);
31+
32+
export const Default = AvatarTemplate.bind({});
33+
Default.args = {src: SRC_URL_1};
34+
Default.storyName = 'default';
35+
36+
export const Disabled = AvatarTemplate.bind({});
37+
Disabled.args = {isDisabled: true, src: SRC_URL_1};
38+
Disabled.storyName = 'isDisabled';
39+
40+
export const WithAltText = AvatarTemplate.bind({});
41+
WithAltText.args = {alt: 'Pensive', src: SRC_URL_2};
42+
WithAltText.storyName = 'with alt text';
43+
44+
export const CustomSize = AvatarTemplate.bind({});
45+
CustomSize.args = {...WithAltText.args, size: 'avatar-size-700'};
46+
CustomSize.storyName = 'with custom size';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {Avatar} from '../';
2+
import React from 'react';
3+
import {render, screen} from '@testing-library/react';
4+
import * as Utils from '@react-spectrum/utils';
5+
6+
describe('Avatar', () => {
7+
it('renders an avatar image', () => {
8+
render(<Avatar src="http://localhost/some_image.png" />);
9+
expect(screen.getByRole('img')).toBeInTheDocument();
10+
expect(screen.getByRole('img')).toHaveAttribute('src', 'http://localhost/some_image.png');
11+
});
12+
13+
it('can render an avatar image with an alt', () => {
14+
render(<Avatar src="http://localhost/some_image.png" alt="Test avatar" />);
15+
expect(screen.getByAltText(/test avatar/i)).toBeInTheDocument();
16+
});
17+
18+
describe('when given a custom size', () => {
19+
it('supports custom sizes in units, such as pixels', () => {
20+
render(<Avatar src="http://localhost/some_image.png" size="80px" />);
21+
expect(screen.getByRole('img')).toHaveStyle({
22+
height: '80px',
23+
width: '80px'
24+
});
25+
});
26+
27+
it('supports custom sizes in numbers', () => {
28+
render(<Avatar src="http://localhost/some_image.png" size={80} />);
29+
expect(screen.getByRole('img')).toHaveStyle({
30+
height: '80px',
31+
width: '80px'
32+
});
33+
});
34+
35+
// Spying on dimensionValue since we're unable to use toHaveStyle effectively with CSS vars
36+
// See https://github.com/testing-library/jest-dom/issues/322
37+
it('supports predefined avatar sizes', () => {
38+
const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
39+
render(<Avatar src="http://localhost/some_image.png" size="avatar-size-700" />);
40+
expect(dimensionValueSpy).not.toHaveBeenCalledWith('avatar-size-100');
41+
});
42+
43+
it('defaults to default size when size is size-XXXX', () => {
44+
const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
45+
render(<Avatar src="http://localhost/some_image.png" size="size-100" />);
46+
expect(dimensionValueSpy).toHaveBeenCalledWith('avatar-size-100');
47+
});
48+
49+
it('defaults to default size when size is a string number', () => {
50+
const dimensionValueSpy = jest.spyOn(Utils, 'dimensionValue');
51+
render(<Avatar src="http://localhost/some_image.png" size="100" />);
52+
expect(dimensionValueSpy).toHaveBeenCalledWith('avatar-size-100');
53+
});
54+
});
55+
56+
it('supports custom class names', () => {
57+
render(<Avatar src="http://localhost/some_image.png" UNSAFE_className="my-class" />);
58+
expect(screen.getByRole('img')).toHaveAttribute('class', expect.stringContaining('my-class'));
59+
});
60+
61+
it('supports style props', () => {
62+
render(<Avatar src="http://localhost/some_image.png" isHidden />);
63+
expect(screen.getByRole('img', {hidden: true})).toBeInTheDocument();
64+
});
65+
66+
it('supports custom DOM props', () => {
67+
render(<Avatar src="http://localhost/some_image.png" data-testid="Test avatar" />);
68+
expect(screen.getByTestId(/test avatar/i)).toBeInTheDocument();
69+
});
70+
71+
describe('when isDisabled = true', () => {
72+
it('renders a disabled avatar image', () => {
73+
render(<Avatar src="http://localhost/some_image.png" isDisabled />);
74+
expect(screen.getByRole('img')).toHaveAttribute('class', expect.stringMatching(/disabled/i));
75+
});
76+
});
77+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# @react-types/avatar
2+
3+
This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.

0 commit comments

Comments
 (0)