Skip to content

Commit

Permalink
feat(Avatar): add avatar component (#1510)
Browse files Browse the repository at this point in the history
- implement core component
- add in variants, sizes, and shapes
- add in new avatar icon to spritesheet
- specify default aria label
- add tests and stories
  • Loading branch information
booc0mtaco authored Mar 1, 2023
1 parent e7ced34 commit bc21f85
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 0 deletions.
54 changes: 54 additions & 0 deletions src/components/Avatar/Avatar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@import '../../design-tokens/mixins.css';

/*------------------------------------*\
# AVATAR
\*------------------------------------*/

/**
* Avatar
*/
.avatar {
display: flex;
align-items: center;
justify-content: center;

overflow: hidden;

color: var(--eds-theme-color-text-neutral-strong);
background-color: var(--eds-theme-color-background-neutral-medium);
}

.avatar:focus-visible {
@mixin focus;
}

@supports not selector(:focus-visible) {
.avatar:focus {
@mixin focus;
}
}

.avatar--sm {
@mixin eds-theme-typography-label-md;

height: var(--eds-size-4);
width: var(--eds-size-4);
}

.avatar--md {
@mixin eds-theme-typography-title-md;

height: var(--eds-size-5);
width: var(--eds-size-5);
}

.avatar--lg {
@mixin eds-theme-typography-title-md;

height: var(--eds-size-6);
width: var(--eds-size-6);
}

.avatar--circle {
border-radius: var(--eds-border-radius-round);
}
66 changes: 66 additions & 0 deletions src/components/Avatar/Avatar.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { BADGE } from '@geometricpanda/storybook-addon-badges';
import type { StoryObj, Meta } from '@storybook/react';
import type React from 'react';

import { Avatar } from './Avatar';

export default {
title: 'Components/Avatar',
component: Avatar,
args: {
size: 'md',
shape: 'circle',
variant: 'icon',
},
parameters: {
badges: [BADGE.BETA],
layout: 'centered',
},
} as Meta<Args>;

type Args = React.ComponentProps<typeof Avatar>;

export const Default: StoryObj<Args> = {
args: {},
};

export const Small: StoryObj<Args> = {
args: {
size: 'sm',
},
};

export const Medium: StoryObj<Args> = {
args: {
size: 'md',
},
};

export const Large: StoryObj<Args> = {
args: {
size: 'lg',
},
};

export const Square: StoryObj<Args> = {
args: {
shape: 'square',
},
};

export const UsingImage: StoryObj<Args> = {
args: {
variant: 'image',
src: `data:image/svg+xml,%3csvg width='38' height='37' viewBox='0 0 38 37' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M19 17.9417C16.4333 17.9417 14.3333 17.125 12.7 15.4917C11.0667 13.8583 10.25 11.7583 10.25 9.19168C10.25 6.62502 11.0667 4.52501 12.7 2.89168C14.3333 1.25835 16.4333 0.441681 19 0.441681C21.5667 0.441681 23.6667 1.25835 25.3 2.89168C26.9333 4.52501 27.75 6.62502 27.75 9.19168C27.75 11.7583 26.9333 13.8583 25.3 15.4917C23.6667 17.125 21.5667 17.9417 19 17.9417ZM0.333344 36.6667V31.1833C0.333344 29.7056 0.702788 28.4417 1.44168 27.3917C2.18057 26.3417 3.13334 25.5445 4.30001 25C6.90557 23.8333 9.40418 22.9583 11.7958 22.375C14.1875 21.7917 16.5889 21.5 19 21.5C21.4111 21.5 23.8028 21.8014 26.175 22.4042C28.5472 23.007 31.0361 23.8722 33.6417 25C34.8472 25.5445 35.8195 26.3417 36.5583 27.3917C37.2972 28.4417 37.6667 29.7056 37.6667 31.1833V36.6667H0.333344Z' fill='%235D6369'/%3e%3c/svg%3e`,
},
};

export const UsingInitials: StoryObj<Args> = {
args: {
variant: 'initials',
user: {
fullName: 'John Smith',
id: '12345',
},
},
};
6 changes: 6 additions & 0 deletions src/components/Avatar/Avatar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateSnapshots } from '@chanzuckerberg/story-utils';
import * as stories from './Avatar.stories';

describe('<Avatar />', () => {
generateSnapshots(stories);
});
111 changes: 111 additions & 0 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import clsx from 'clsx';
import React from 'react';
import Icon from '../Icon';
import styles from './Avatar.module.css';

export type UserData = {
/**
* The full name of the attached user (e.g., Jane Doe, David S. Pumpkins)
*/
fullName: string;
/**
* User ID associated with the attached user
*/
id?: string | number;
/**
* Additional data for an attached user (email, etc.)
*/
[k: string]: string | number | undefined;
};

export interface Props {
/**
* Label for the given avatar. Defaults to a string using user data.
*/
ariaLabel: string;
/**
* CSS class names that can be appended to the component.
*/
className?: string;
/**
* The shape of the avatar
*/
shape?: 'circle' | 'square';
/**
* The size of the component
*/
size?: 'sm' | 'md' | 'lg';
/**
* The URL to an image resource (loaded lazily)
*/
src?: string;
/**
* The user associated with this avatar
*/
user?: UserData;
/**
* Variants of how the avatar will be portrayed
*/
variant?: 'icon' | 'initials' | 'image';
}

function getInitials(fromName: string): string {
/**
* Scenarios:
* - User's name is spelled as first and last name: John Smith
* - User's name has a middle name or initial: John C. Smith
* - User's Name has dashes in it
*/
return fromName
.split(' ')
.map((part) => part[0])
.reduce(
(prev, curr, idx, arr) =>
idx === 0 || idx === arr.length - 1 ? prev + curr : prev,
'',
)
.toUpperCase();
}

/**
* BETA: This component is still a work in progress and is subject to change.
*
* `import {Avatar} from "@chanzuckerberg/eds";`
*
* Representation of a single, unique user, keyed by the user name
*/
export const Avatar = ({
ariaLabel,
className,
shape = 'circle',
size = 'md',
user,
variant = 'initials',
src,
...other
}: Props) => {
const componentClassName = clsx(
styles['avatar'],
shape && styles[`avatar--${shape}`],
size && styles[`avatar--${size}`],
variant && styles[`avatar--${variant}`],
className,
);

const descriptiveLabel =
ariaLabel ??
`Avatar for ${!user && 'unknown'} user ${user?.fullName || ''}`;

return (
<div
aria-label={descriptiveLabel}
className={componentClassName}
role="img"
{...other}
>
{variant === 'initials' && (user ? getInitials(user.fullName) : '??')}
{variant === 'icon' && <Icon name="avatar" purpose="decorative" />}
{variant === 'image' && <img alt="user" src={src} />}
</div>
);
};
124 changes: 124 additions & 0 deletions src/components/Avatar/__snapshots__/Avatar.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Avatar /> Default story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--circle avatar--md avatar--icon"
role="img"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
</div>
`;

exports[`<Avatar /> Large story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--circle avatar--lg avatar--icon"
role="img"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
</div>
`;

exports[`<Avatar /> Medium story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--circle avatar--md avatar--icon"
role="img"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
</div>
`;

exports[`<Avatar /> Small story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--circle avatar--sm avatar--icon"
role="img"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
</div>
`;

exports[`<Avatar /> Square story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--square avatar--md avatar--icon"
role="img"
>
<svg
aria-hidden="true"
class="icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
</div>
`;

exports[`<Avatar /> UsingImage story renders snapshot 1`] = `
<div
aria-label="Avatar for unknown user "
class="avatar avatar--circle avatar--md avatar--image"
role="img"
>
<img
alt="user"
src="data:image/svg+xml,%3csvg width='38' height='37' viewBox='0 0 38 37' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M19 17.9417C16.4333 17.9417 14.3333 17.125 12.7 15.4917C11.0667 13.8583 10.25 11.7583 10.25 9.19168C10.25 6.62502 11.0667 4.52501 12.7 2.89168C14.3333 1.25835 16.4333 0.441681 19 0.441681C21.5667 0.441681 23.6667 1.25835 25.3 2.89168C26.9333 4.52501 27.75 6.62502 27.75 9.19168C27.75 11.7583 26.9333 13.8583 25.3 15.4917C23.6667 17.125 21.5667 17.9417 19 17.9417ZM0.333344 36.6667V31.1833C0.333344 29.7056 0.702788 28.4417 1.44168 27.3917C2.18057 26.3417 3.13334 25.5445 4.30001 25C6.90557 23.8333 9.40418 22.9583 11.7958 22.375C14.1875 21.7917 16.5889 21.5 19 21.5C21.4111 21.5 23.8028 21.8014 26.175 22.4042C28.5472 23.007 31.0361 23.8722 33.6417 25C34.8472 25.5445 35.8195 26.3417 36.5583 27.3917C37.2972 28.4417 37.6667 29.7056 37.6667 31.1833V36.6667H0.333344Z' fill='%235D6369'/%3e%3c/svg%3e"
/>
</div>
`;

exports[`<Avatar /> UsingInitials story renders snapshot 1`] = `
<div
aria-label="Avatar for false user John Smith"
class="avatar avatar--circle avatar--md avatar--initials"
role="img"
>
JS
</div>
`;
1 change: 1 addition & 0 deletions src/components/Avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Avatar as default } from './Avatar';
20 changes: 20 additions & 0 deletions src/components/Icon/__snapshots__/Icon.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,26 @@ exports[`<Icon /> IconGrid story renders snapshot 1`] = `
autorenew
</span>
</li>
<li
class="icon-grid__item"
>
<svg
aria-hidden="true"
class="icon icon-grid__icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 10.9667C9.53337 10.9667 8.33337 10.5 7.40004 9.56667C6.46671 8.63334 6.00004 7.43334 6.00004 5.96668C6.00004 4.50001 6.46671 3.30001 7.40004 2.36668C8.33337 1.43334 9.53337 0.966675 11 0.966675C12.4667 0.966675 13.6667 1.43334 14.6 2.36668C15.5334 3.30001 16 4.50001 16 5.96668C16 7.43334 15.5334 8.63334 14.6 9.56667C13.6667 10.5 12.4667 10.9667 11 10.9667ZM0.333374 21.6667V18.5333C0.333374 17.6889 0.544485 16.9667 0.966707 16.3667C1.38893 15.7667 1.93337 15.3111 2.60004 15C4.08893 14.3333 5.51671 13.8333 6.88337 13.5C8.25004 13.1667 9.62226 13 11 13C12.3778 13 13.7445 13.1722 15.1 13.5167C16.4556 13.8611 17.8778 14.3556 19.3667 15C20.0556 15.3111 20.6112 15.7667 21.0334 16.3667C21.4556 16.9667 21.6667 17.6889 21.6667 18.5333V21.6667H0.333374Z"
/>
</svg>
<span
class="icon-grid__text"
>
avatar
</span>
</li>
<li
class="icon-grid__item"
>
Expand Down
Loading

0 comments on commit bc21f85

Please sign in to comment.