Skip to content

Commit

Permalink
feat(avatar): add element customization (#1742)
Browse files Browse the repository at this point in the history
* feat(avatar): add element customization
  • Loading branch information
gloriliale committed Aug 5, 2021
1 parent 5374cd7 commit d14381b
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .changeset/angry-lizards-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/avatar': minor
'@twilio-paste/core': minor
---

[Avatar] Enable Avatar to respect customizations set on the customization provider. Avatar now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exports[`Avatar image should render responsive css with an image 1`] = `
overflow: hidden;
width: sizeIcon30;
height: sizeIcon30;
color: colorText;
}
@media screen and (min-width:40em) {
Expand Down Expand Up @@ -48,7 +49,8 @@ exports[`Avatar image should render responsive css with an image 1`] = `
<div
class="emotion-1"
data-paste-element="BOX"
color="colorText"
data-paste-element="AVATAR"
overflow="hidden"
>
<img
Expand All @@ -71,6 +73,7 @@ exports[`Avatar intials should render responsive css 1`] = `
overflow: hidden;
width: sizeIcon10;
height: sizeIcon10;
color: colorText;
}
@media screen and (min-width:40em) {
Expand All @@ -90,7 +93,7 @@ exports[`Avatar intials should render responsive css 1`] = `
.emotion-0 {
margin: 0;
padding: 0;
color: colorText;
color: inherit;
font-size: fontSize10;
line-height: lineHeight10;
display: block;
Expand All @@ -116,12 +119,13 @@ exports[`Avatar intials should render responsive css 1`] = `
<div
class="emotion-1"
data-paste-element="BOX"
color="colorText"
data-paste-element="AVATAR"
overflow="hidden"
>
<abbr
class="emotion-0"
color="colorText"
color="inherit"
data-paste-element="TEXT"
display="block"
font-size="fontSize10,fontSize10,fontSize60"
Expand Down
36 changes: 36 additions & 0 deletions packages/paste-core/components/avatar/__tests__/avatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {UserIcon} from '@twilio-paste/icons/esm/UserIcon';
import {Box} from '@twilio-paste/box';
import {CustomizationProvider} from '@twilio-paste/customization';
import {matchers} from 'jest-emotion';
// @ts-ignore typescript doesn't like js imports
import axe from '../../../../../.jest/axe-helper';
import {Avatar} from '../src';
Expand All @@ -13,6 +15,8 @@ import {
getInitialsFromName,
} from '../src/utils';

expect.extend(matchers);

describe('Avatar', () => {
describe('Utils', () => {
describe('getInitialsFromName', () => {
Expand Down Expand Up @@ -158,6 +162,38 @@ describe('Avatar', () => {
});
});

describe('Customization', () => {
it('should set an element data attribute for Avatar', () => {
render(<Avatar data-testid="avatar" size="sizeIcon20" name="avatar example" icon={UserIcon} />);
const avatarComponent = screen.getByTestId('avatar');
expect(avatarComponent.getAttribute('data-paste-element')).toEqual('AVATAR');
});
it('should set a custom element data attribute for Avatar', () => {
render(<Avatar data-testid="avatar" element="FOO" size="sizeIcon20" name="avatar example" icon={UserIcon} />);
const avatarComponent = screen.getByTestId('avatar');
expect(avatarComponent.getAttribute('data-paste-element')).toEqual('FOO');
});

it('should add custom styles to Avatar', () => {
render(
<CustomizationProvider baseTheme="default" elements={{AVATAR: {backgroundColor: 'colorBackgroundAvailable'}}}>
<Avatar data-testid="avatar" size="sizeIcon20" name="avatar example" />
</CustomizationProvider>
);
const renderedAvatar = screen.getByTestId('avatar');
expect(renderedAvatar).toHaveStyleRule('background-color', 'rgb(20,176,83)');
});
it('should add custom styles to custom element Avatar', () => {
render(
<CustomizationProvider baseTheme="default" elements={{FOO: {backgroundColor: 'colorBackgroundAvailable'}}}>
<Avatar data-testid="avatar" element="FOO" size="sizeIcon20" name="avatar example" />
</CustomizationProvider>
);
const renderedAvatar = screen.getByTestId('avatar');
expect(renderedAvatar).toHaveStyleRule('background-color', 'rgb(20,176,83)');
});
});

describe('accessibility', () => {
it('should have no accessibility violations', async () => {
const {container} = render(
Expand Down
16 changes: 10 additions & 6 deletions packages/paste-core/components/avatar/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import {Text} from '@twilio-paste/text';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import {isIconSizeTokenProp} from '@twilio-paste/style-props';
import {getComputedTokenNames, getInitialsFromName} from './utils';
import type {AvatarProps} from './types';
import type {AvatarProps, AvatarContentProps} from './types';

const DEFAULT_SIZE = 'sizeIcon70';

const AvatarContents: React.FC<AvatarProps> = ({name, size = DEFAULT_SIZE, src, icon: Icon}) => {
const AvatarContents: React.FC<AvatarContentProps> = ({name, size = DEFAULT_SIZE, src, icon: Icon}) => {
const computedTokenNames = getComputedTokenNames(size);
if (Icon != null) {
if (typeof Icon !== 'function' || typeof Icon.displayName !== 'string' || !Icon.displayName.includes('Icon')) {
throw new Error('[Paste Avatar]: icon prop expected to be a Paste icon only.');
}
return (
<Box maxWidth="100%" size={size} display="flex" alignItems="center" justifyContent="center">
<Icon decorative={false} title={name} size={computedTokenNames.iconSize} color="colorText" />
<Icon decorative={false} title={name} size={computedTokenNames.iconSize} />
</Box>
);
}
Expand All @@ -33,14 +33,15 @@ const AvatarContents: React.FC<AvatarProps> = ({name, size = DEFAULT_SIZE, src,
textAlign="center"
textDecoration="none"
title={name}
color="inherit"
>
{getInitialsFromName(name)}
</Text>
);
};

const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
({name, children, size = DEFAULT_SIZE, src, icon, ...props}, ref) => {
({name, children, size = DEFAULT_SIZE, element = 'AVATAR', src, icon, ...props}, ref) => {
if (name === undefined) {
console.error('[Paste Avatar]: name prop is required');
}
Expand All @@ -53,16 +54,18 @@ const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
<Box
{...safelySpreadBoxProps(props)}
as="div"
element={element}
backgroundColor="colorBackgroundUser"
borderRadius="borderRadiusCircle"
overflow="hidden"
ref={ref}
size={size}
color="colorText"
>
{src ? (
<AvatarContents name={name} size={size} src={src} {...props} />
<AvatarContents name={name} size={size} src={src} />
) : (
<AvatarContents name={name} size={size} icon={icon} {...props} />
<AvatarContents name={name} size={size} icon={icon} />
)}
</Box>
);
Expand All @@ -74,6 +77,7 @@ Avatar.displayName = 'Avatar';
Avatar.propTypes = {
size: isIconSizeTokenProp,
name: PropTypes.string.isRequired,
element: PropTypes.string,
src: (props) => {
// eslint-disable-next-line no-new
if (props.src && props.icon) new Error('[Paste Avatar]: do not set both src and icon on Avatar');
Expand Down
7 changes: 7 additions & 0 deletions packages/paste-core/components/avatar/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {IconSize} from '@twilio-paste/style-props';
import type {GenericIconProps} from '@twilio-paste/icons/esm/types';
import type {BoxProps} from '@twilio-paste/box';

type AvatarImage = {
src?: string;
Expand All @@ -11,7 +12,13 @@ type AvatarIcon = {
};

export type AvatarProps = React.HTMLAttributes<'div'> &
Pick<BoxProps, 'element'> &
(AvatarImage | AvatarIcon) & {
name: string;
size?: IconSize;
};

export type AvatarContentProps = (AvatarImage | AvatarIcon) & {
name: string;
size?: IconSize;
};
39 changes: 39 additions & 0 deletions packages/paste-core/components/avatar/stories/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';
import {Stack} from '@twilio-paste/stack';
import {Box} from '@twilio-paste/box';
import {CustomizationProvider} from '@twilio-paste/customization';
import {UserIcon} from '@twilio-paste/icons/esm/UserIcon';
import {Avatar} from '../src';

Expand Down Expand Up @@ -115,3 +117,40 @@ export const ResponsiveIcon = (): React.ReactNode => {
ResponsiveImage.story = {
parameters: {chromatic: {delay: 3000}},
};

export const CustomAvatar = (): React.ReactNode => {
return (
<>
<Stack orientation="horizontal" spacing="space40">
<Avatar size="sizeIcon40" name="Breonna Taylor" />
<Avatar size="sizeIcon50" name="avatar example" src="./avatars/avatar-sizeIcon50.png" />
<Avatar size="sizeIcon60" name="avatar example" icon={UserIcon} />
</Stack>
<CustomizationProvider
baseTheme="default"
elements={{
AVATAR: {
backgroundColor: 'colorBackgroundTrial',
},
AANG: {
color: 'colorTextWeakest',
backgroundColor: 'colorBackgroundPrimary',
},
}}
>
<Box marginBottom="space60" marginTop="space60">
<Stack orientation="horizontal" spacing="space40">
<Avatar size="sizeIcon40" name="Breonna Taylor" />
<Avatar size="sizeIcon50" name="avatar example" src="./avatars/avatar-sizeIcon50.png" />
<Avatar size="sizeIcon60" name="avatar example" icon={UserIcon} />
</Stack>
</Box>
<Stack orientation="horizontal" spacing="space40">
<Avatar element="AANG" size="sizeIcon40" name="Breonna Taylor" />
<Avatar element="AANG" size="sizeIcon50" name="avatar example" src="./avatars/avatar-sizeIcon50.png" />
<Avatar element="AANG" size="sizeIcon60" name="avatar example" icon={UserIcon} />
</Stack>
</CustomizationProvider>
</>
);
};

0 comments on commit d14381b

Please sign in to comment.