Skip to content

Commit

Permalink
feat(avatar): add avatar group
Browse files Browse the repository at this point in the history
  • Loading branch information
nkrantz committed May 9, 2023
1 parent ed2ae23 commit 7313e6e
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 84 deletions.
6 changes: 6 additions & 0 deletions .changeset/clever-pears-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/avatar': minor
'@twilio-paste/core': minor
---

[Avatar] Add Avatar Group component
5 changes: 5 additions & 0 deletions .changeset/swift-pears-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@twilio-paste/codemods': patch
---

[Codemods] add AvatarGroup to `@twilio-paste/avatar` package
1 change: 1 addition & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"isExternalUrl": "@twilio-paste/core/anchor",
"secureExternalLink": "@twilio-paste/core/anchor",
"Avatar": "@twilio-paste/core/avatar",
"AvatarGroup": "@twilio-paste/core/avatar",
"Badge": "@twilio-paste/core/badge",
"badgeBaseStyles": "@twilio-paste/core/badge",
"useResizeChildIcons": "@twilio-paste/core/badge",
Expand Down
10 changes: 10 additions & 0 deletions packages/paste-core/components/avatar/__tests__/avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getCorrespondingIconSizeFromSizeToken,
getComputedTokenNames,
getInitialsFromName,
getGroupSpacing,
} from '../src/utils';

describe('Avatar', () => {
Expand Down Expand Up @@ -105,6 +106,15 @@ describe('Avatar', () => {
});
});
});

describe('getGroupSpacing', () => {
it('should return the correct space token', () => {
expect(getGroupSpacing('sizeIcon30', 'user')).toEqual('spaceNegative10');
expect(getGroupSpacing('sizeIcon100', 'user')).toEqual('spaceNegative40');
expect(getGroupSpacing('sizeIcon30', 'entity')).toEqual('spaceNegative10');
expect(getGroupSpacing('sizeIcon100', 'entity')).toEqual('spaceNegative20');
});
});
});

describe('intials', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/paste-core/components/avatar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "8.0.0",
"category": "graphic",
"status": "production",
"description": "An Avatar is a graphical representation of a user or entity.",
"description": "An Avatar is a graphical representation of a user or entity and an Avatar Group is a collection of Avatars.",
"author": "Twilio Inc.",
"license": "MIT",
"main:dev": "src/index.tsx",
Expand Down Expand Up @@ -34,6 +34,7 @@
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.0",
"@twilio-paste/icons": "^11.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/text": "^9.0.0",
Expand All @@ -53,6 +54,7 @@
"@twilio-paste/customization": "^7.0.0",
"@twilio-paste/design-tokens": "^9.0.2",
"@twilio-paste/icons": "^11.0.0",
"@twilio-paste/stack": "^7.0.0",
"@twilio-paste/style-props": "^8.0.0",
"@twilio-paste/styling-library": "^2.0.0",
"@twilio-paste/text": "^9.0.0",
Expand Down
30 changes: 30 additions & 0 deletions packages/paste-core/components/avatar/src/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {Stack} from '@twilio-paste/stack';
import {isIconSizeTokenProp} from '@twilio-paste/style-props';

import type {AvatarGroupProps, AvatarVariants} from './types';
import {getGroupSpacing} from './utils';

export const AvatarContext = React.createContext<Omit<AvatarGroupProps, 'children'>>({} as AvatarGroupProps);

export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
({variant, size, children, element = 'AVATAR_GROUP'}, ref) => {
return (
<AvatarContext.Provider value={{variant, size}}>
<Stack orientation="horizontal" spacing={getGroupSpacing(size, variant)} element={element} ref={ref}>
{children}
</Stack>
</AvatarContext.Provider>
);
}
);

AvatarGroup.displayName = 'AvatarGroup';

AvatarGroup.propTypes = {
size: isIconSizeTokenProp,
variant: PropTypes.oneOf(['user', 'entity'] as AvatarVariants[]),
children: PropTypes.node.isRequired,
element: PropTypes.string,
};
22 changes: 13 additions & 9 deletions packages/paste-core/components/avatar/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {isIconSizeTokenProp} from '@twilio-paste/style-props';

import {getComputedTokenNames, getInitialsFromName} from './utils';
import type {AvatarProps, AvatarContentProps, ColorVariants, AvatarVariants} from './types';
import {AvatarContext} from './AvatarGroup';

const DEFAULT_SIZE = 'sizeIcon70';

Expand All @@ -17,9 +18,13 @@ const AvatarContents: React.FC<React.PropsWithChildren<AvatarContentProps>> = ({
src,
icon: Icon,
}) => {
const computedTokenNames = getComputedTokenNames(size);
const {size: groupSize} = React.useContext(AvatarContext);
const computedTokenNames = getComputedTokenNames(groupSize || size);

if (src != null) {
return <Box as="img" alt={name} maxWidth="100%" src={src} size={size} title={name} />;
return (
<Box as="img" alt={name} maxWidth="100%" boxShadow="shadowBorderWeaker" src={src} size={size} title={name} />
);
}
if (Icon != null) {
if (!isValidElementType(Icon) || typeof Icon.displayName !== 'string' || !Icon.displayName.includes('Icon')) {
Expand Down Expand Up @@ -78,7 +83,7 @@ const variants: Record<AvatarVariants, BoxStyleProps> = {
borderRadius: 'borderRadiusCircle',
},
entity: {
borderRadius: 'borderRadius20',
borderRadius: 'borderRadius30',
},
};

Expand All @@ -101,14 +106,12 @@ const shadowVariants: Record<ColorVariants, BoxStyleProps> = {
};

const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
(
{name, children, size = DEFAULT_SIZE, element = 'AVATAR', src, icon, color = 'default', variant = 'user', ...props},
ref
) => {
({name, size = DEFAULT_SIZE, element = 'AVATAR', src, icon, color = 'default', variant = 'user', ...props}, ref) => {
if (name === undefined) {
// eslint-disable-next-line no-console
console.error('[Paste Avatar]: name prop is required');
}
const {variant: groupVariant, size: groupSize} = React.useContext(AvatarContext);

return (
<Box
Expand All @@ -119,10 +122,10 @@ const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
textAlign="center"
flexShrink={0}
ref={ref}
size={size}
size={groupSize || size}
{...(src ? undefined : shadowVariants[color])}
{...colorVariants[color]}
{...variants[variant]}
{...variants[groupVariant || variant]}
>
<AvatarContents name={name} size={size} icon={icon} src={src} />
</Box>
Expand Down Expand Up @@ -152,3 +155,4 @@ Avatar.propTypes = {
};

export {Avatar};
export {AvatarGroup} from './AvatarGroup';
7 changes: 7 additions & 0 deletions packages/paste-core/components/avatar/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ export type AvatarContentProps = {
icon?: React.FC<React.PropsWithChildren<GenericIconProps>>;
src?: string;
};

export type AvatarGroupProps = React.HTMLAttributes<'div'> &
Pick<BoxProps, 'element'> & {
size: IconSize;
variant: AvatarVariants;
children: NonNullable<React.ReactNode>;
};
47 changes: 47 additions & 0 deletions packages/paste-core/components/avatar/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import type {
IconSize,
LineHeight,
FontSize,
SpaceOptions,
} from '@twilio-paste/style-props';

import type {AvatarVariants} from './types';

export const getInitialsFromName = (fullname: string): string => {
return fullname
.split(' ')
Expand Down Expand Up @@ -119,3 +122,47 @@ export const getComputedTokenNames = (
}
throw new Error('[Avatar]: size must be a string or an array');
};

/*
* Spacing for Avatar Group
*/

export const getGroupSpacing = (size: IconSize, variant: AvatarVariants): SpaceOptions => {
if (variant === 'user') {
switch (size) {
case 'sizeIcon10':
case 'sizeIcon20':
case 'sizeIcon30':
return 'spaceNegative10';
case 'sizeIcon40':
case 'sizeIcon50':
case 'sizeIcon60':
case 'sizeIcon70':
return 'spaceNegative20';
case 'sizeIcon80':
case 'sizeIcon90':
return 'spaceNegative30';
case 'sizeIcon100':
return 'spaceNegative40';
case 'sizeIcon110':
return 'spaceNegative50';
}
} else if (variant === 'entity') {
switch (size) {
case 'sizeIcon10':
case 'sizeIcon20':
case 'sizeIcon30':
return 'spaceNegative10';
case 'sizeIcon40':
case 'sizeIcon50':
case 'sizeIcon60':
case 'sizeIcon70':
case 'sizeIcon80':
case 'sizeIcon90':
case 'sizeIcon100':
case 'sizeIcon110':
return 'spaceNegative20';
}
}
return 'spaceNegative20';
};
73 changes: 72 additions & 1 deletion packages/paste-core/components/avatar/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {UserIcon} from '@twilio-paste/icons/esm/UserIcon';
import {BusinessIcon} from '@twilio-paste/icons/esm/BusinessIcon';
import {useTheme} from '@twilio-paste/theme';

import {Avatar} from '../src';
import {Avatar, AvatarGroup} from '../src';

// eslint-disable-next-line import/no-default-export
export default {
Expand Down Expand Up @@ -115,6 +115,77 @@ export const Variants = (): React.ReactNode => {
);
};

export const Grouped = (): React.ReactNode => {
return (
<Stack orientation="horizontal" spacing="space50">
<Stack orientation="vertical" spacing="space30">
<AvatarGroup size="sizeIcon30" variant="user">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon70" variant="user">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon90" variant="user">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon100" variant="user">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon110" variant="user">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
</Stack>
<Stack orientation="vertical" spacing="space30">
<AvatarGroup size="sizeIcon30" variant="entity">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon70" variant="entity">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon90" variant="entity">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon100" variant="entity">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
<AvatarGroup size="sizeIcon110" variant="entity">
<Avatar name="First Avatar" />
<Avatar name="Second Avatar" />
<Avatar name="Third Avatar" />
</AvatarGroup>
</Stack>
</Stack>
);
};

export const GroupedUsingContext = (): React.ReactNode => {
return (
<AvatarGroup size="sizeIcon70" variant="entity">
<Avatar size="sizeIcon10" variant="entity" name="First Avatar" />
<Avatar size="sizeIcon110" name="Second Avatar" />
<Avatar variant="user" name="Third Avatar" />
</AvatarGroup>
);
};

export const ResponsiveInitials = (): React.ReactNode => {
return (
<Stack orientation="horizontal" spacing="space40">
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7313e6e

Please sign in to comment.