Skip to content

Commit

Permalink
fix(Avatar): avatar size and loading skeleton (#2880)
Browse files Browse the repository at this point in the history
* chore: remove extra component from story

* chore: add loading state to all

* fix(Avatar): avatar size + loading skeleton

* refactor: use template literal typing

* chore: add broken src example

* refactor: adjust typing

* chore: add fix changelog

* chore: add feat changelog
  • Loading branch information
cf-remylenoir authored Sep 20, 2024
1 parent 2ccfa3e commit 74b10dc
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/fuzzy-rules-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@contentful/f36-avatar": patch
---

Avatar size and loading skeleton
5 changes: 5 additions & 0 deletions .changeset/hot-ears-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@contentful/f36-avatar": feat
---

Allow custom size
27 changes: 14 additions & 13 deletions packages/components/avatar/src/Avatar/Avatar.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { css } from 'emotion';
import tokens from '@contentful/f36-tokens';
import { type AvatarProps } from './Avatar';
import { applyMuted, avatarColorMap, type ColorVariant } from './utils';
import {
applyMuted,
avatarColorMap,
getSizeInPixels,
type ColorVariant,
} from './utils';

export const getColorVariantStyles = (colorVariant: ColorVariant) => {
const colorToken: string = avatarColorMap[colorVariant];
Expand All @@ -16,14 +21,6 @@ export const getColorVariantStyles = (colorVariant: ColorVariant) => {
};
};

export const convertSizeToPixels = (size: AvatarProps['size']) =>
({
tiny: '20px',
small: '24px',
medium: '32px',
large: '48px',
}[size]);

const getInitialsFontSize = (sizePixels: string) =>
Math.round(Number(sizePixels.replace('px', '')) / 2);

Expand All @@ -37,7 +34,8 @@ export const getAvatarStyles = ({
colorVariant: ColorVariant;
}) => {
const borderRadius = variant === 'app' ? tokens.borderRadiusSmall : '100%';
const sizePixels = convertSizeToPixels(size);
const finalSize = getSizeInPixels(size);

const isMuted = colorVariant === 'muted';

return {
Expand All @@ -50,18 +48,21 @@ export const getAvatarStyles = ({
alignItems: 'center',
justifyContent: 'center',
fontStretch: 'semi-condensed',
fontSize: `${getInitialsFontSize(sizePixels)}px`,
fontSize: `${getInitialsFontSize(size)}px`,
}),
image: css({
borderRadius,
display: 'block',
}),
root: css({
borderRadius,
height: sizePixels,
height: finalSize,
overflow: 'hidden',
position: 'relative',
width: sizePixels,
width: finalSize,
svg: {
borderRadius,
},
'&::after': {
borderRadius,
bottom: 0,
Expand Down
22 changes: 13 additions & 9 deletions packages/components/avatar/src/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import {
type WithEnhancedContent,
} from '@contentful/f36-tooltip';

import { convertSizeToPixels, getAvatarStyles } from './Avatar.styles';
import type { ColorVariant } from './utils';

export type Size = 'tiny' | 'small' | 'medium' | 'large';
import { getAvatarStyles } from './Avatar.styles';
import {
getSizeInPixels,
type ColorVariant,
type Size,
type SizeInPixel,
} from './utils';

export type Variant = 'app' | 'user';

Expand All @@ -23,9 +26,10 @@ export interface AvatarProps extends CommonProps {
*/
isLoading?: boolean;
/**
* Use the available sizes or a numerical custom one, e.g. '52px'
* @default 'medium'
*/
size?: Size;
size?: Size | SizeInPixel;
initials?: string;
src?: ImageProps['src'];
/**
Expand Down Expand Up @@ -64,8 +68,8 @@ function _Avatar(
) {
// Only render the fallback when `src` is undefined or an empty string
const isFallback = Boolean(!isLoading && !src);
const styles = getAvatarStyles({ size, variant, colorVariant });
const sizePixels = convertSizeToPixels(size);
const finalSize = getSizeInPixels(size);
const styles = getAvatarStyles({ size: finalSize, variant, colorVariant });

const content = (
<div
Expand All @@ -84,9 +88,9 @@ function _Avatar(
<Image
alt={alt}
className={styles.image}
height={sizePixels}
height={finalSize}
src={src}
width={sizePixels}
width={finalSize}
/>
)}
{!!icon && <span className={styles.overlayIcon}>{icon}</span>}
Expand Down
43 changes: 43 additions & 0 deletions packages/components/avatar/src/Avatar/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import tokens from '@contentful/f36-tokens';

import { AvatarProps } from './Avatar';

export const SIZES = ['tiny', 'small', 'medium', 'large'] as const;
export type Size = (typeof SIZES)[number];
export type SizeInPixel = `${number}px`;

export type ColorVariant = keyof typeof avatarColorMap;

export const avatarColorMap = {
Expand Down Expand Up @@ -33,3 +39,40 @@ export function applyMuted(color: string): string {
// Eventually we should use `color-mix`
// return `color-mix(in srgb, ${color}, ${tokens.colorWhite} 50%)`;
}

/**
* Type guard for size variants
*
* @param size
* @returns true/false if the size is a valid size variant
*/
export const isSizeVariant = (size: string): size is Size => {
return SIZES.includes(size as Size);
};

/**
* Converts the variant size to pixels
*
* @param size
* @returns the variant size value in pixels
*/
export const convertSizeToPixels = (size: AvatarProps['size']): SizeInPixel => {
const sizes: Record<Size, SizeInPixel> = {
tiny: '20px',
small: '24px',
medium: '32px',
large: '48px',
};

return sizes[size];
};

/**
* Utility function to convert the given size variant/custom size to pixels
*
* @param size
* @returns The variant or custom size in pixels, e.g. '32px'
*/
export function getSizeInPixels(size: AvatarProps['size']): SizeInPixel {
return isSizeVariant(size) ? convertSizeToPixels(size) : size;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from 'emotion';
import tokens from '@contentful/f36-tokens';
import { type AvatarProps } from '../Avatar/';
import { convertSizeToPixels } from '../Avatar/Avatar.styles';
import { convertSizeToPixels } from '../Avatar/utils';

export const getAvatarGroupStyles = (size: AvatarProps['size']) => {
return {
Expand Down
52 changes: 43 additions & 9 deletions packages/components/avatar/stories/Avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export const Overview: Story<AvatarProps> = (args) => {
size="large"
icon={<CheckCircleIcon variant="positive" />}
/>
<Avatar
{...args}
size="75px"
icon={<CheckCircleIcon variant="positive" />}
/>
<Avatar
{...args}
size="75px"
variant="app"
icon={<CheckCircleIcon variant="positive" />}
/>
<Avatar
{...args}
size="large"
Expand Down Expand Up @@ -85,15 +96,38 @@ export const Overview: Story<AvatarProps> = (args) => {
gap="spacingS"
marginBottom="spacingM"
>
<Avatar size="tiny" variant="user" />
<Avatar size="small" variant="user" />
<Avatar size="medium" variant="user" />
<Avatar isLoading size="tiny" variant="user" />
<Avatar isLoading size="small" variant="user" />
<Avatar isLoading size="medium" variant="user" />
<Avatar isLoading size="large" variant="user" />
<Avatar size="large" variant="app" />
<Avatar size="large" variant="user" />
<Avatar size="medium" variant="app" />
<Avatar size="small" variant="app" />
<Avatar size="tiny" variant="app" />
<Avatar isLoading size="75px" variant="user" />
<Avatar isLoading size="75px" variant="app" />
<Avatar isLoading size="large" variant="app" />
<Avatar isLoading size="medium" variant="app" />
<Avatar isLoading size="small" variant="app" />
<Avatar isLoading size="tiny" variant="app" />
</Flex>

<SectionHeading as="h3" marginBottom="spacingS">
With a broken source, the loading skeleton is also rendered
</SectionHeading>

<Flex
alignItems="center"
flexDirection="row"
gap="spacingS"
marginBottom="spacingM"
>
<Avatar src="#" size="tiny" variant="user" />
<Avatar src="#" size="small" variant="user" />
<Avatar src="#" size="medium" variant="user" />
<Avatar src="#" size="large" variant="user" />
<Avatar src="#" size="75px" variant="user" />
<Avatar src="#" size="75px" variant="app" />
<Avatar src="#" size="large" variant="app" />
<Avatar src="#" size="medium" variant="app" />
<Avatar src="#" size="small" variant="app" />
<Avatar src="#" size="tiny" variant="app" />
</Flex>

<SectionHeading as="h3" marginBottom="spacingS">
Expand Down Expand Up @@ -187,7 +221,7 @@ export const BorderColors: Story<AvatarProps> = (args) => {
{/* prettier-ignore */}
<Avatar {...argsNoSrc} colorVariant={color} size="small" variant="app" />
{/* prettier-ignore */}
<Avatar {...argsNoSrc} colorVariant={color}size="tiny" variant="app" />
<Avatar {...argsNoSrc} colorVariant={color} size="tiny" variant="app" />
{/* prettier-ignore */}
<Avatar {...args} colorVariant={color} size="tiny" />
{/* prettier-ignore */}
Expand Down

0 comments on commit 74b10dc

Please sign in to comment.