Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Avatar): avatar size and loading skeleton #2880

Merged
merged 8 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading