From cbfb3f73e2780ed052d70d85ab77e5c15f2687a7 Mon Sep 17 00:00:00 2001 From: Mateusz Kleszcz Date: Mon, 11 Sep 2023 13:52:46 +0200 Subject: [PATCH] refactor: TET-207 avatar (#51) --- src/components/Avatar/Avatar.props.ts | 28 +- src/components/Avatar/Avatar.styles.ts | 262 +++++++++++++++--- src/components/Avatar/Avatar.test.tsx | 75 +++++ src/components/Avatar/Avatar.tsx | 35 +-- .../Avatar/stylesBuilder/stylesBuilder.ts | 24 +- .../Avatar/types/AvatarAppearance.type.ts | 17 ++ .../Avatar/types/AvatarEmphasis.type.ts | 1 + .../Avatar/{ => types}/AvatarShape.type.ts | 0 .../Avatar/types/AvatarSize.type.ts | 1 + src/components/Avatar/types/index.ts | 7 + 10 files changed, 362 insertions(+), 88 deletions(-) create mode 100644 src/components/Avatar/Avatar.test.tsx create mode 100644 src/components/Avatar/types/AvatarAppearance.type.ts create mode 100644 src/components/Avatar/types/AvatarEmphasis.type.ts rename src/components/Avatar/{ => types}/AvatarShape.type.ts (100%) create mode 100644 src/components/Avatar/types/AvatarSize.type.ts create mode 100644 src/components/Avatar/types/index.ts diff --git a/src/components/Avatar/Avatar.props.ts b/src/components/Avatar/Avatar.props.ts index 001305b9..7c4f6ec7 100644 --- a/src/components/Avatar/Avatar.props.ts +++ b/src/components/Avatar/Avatar.props.ts @@ -1,13 +1,12 @@ -import { ImgHTMLAttributes } from 'react'; +import type { ImgHTMLAttributes } from 'react'; -import { AvatarConfig } from './Avatar.styles'; -import { AvatarShape } from './AvatarShape.type'; -import { Appearance } from '../../types/Appearance'; - -import { Emphasis } from '@/types/Emphasis'; -import { MarginProps } from '@/types/MarginProps'; -import { Size } from '@/types/Size'; -import { DeepPartial } from '@/utility-types/DeepPartial'; +import type { AvatarConfig } from './Avatar.styles'; +import type { + AvatarAppearanceColors, + AvatarEmphasis, + AvatarShape, + AvatarSize, +} from './types'; export type AvatarProps = ( | { @@ -17,12 +16,13 @@ export type AvatarProps = ( initials?: never; } | { - appearance?: Appearance; - emphasis?: Emphasis; + img?: never; + appearance?: AvatarAppearanceColors; + emphasis?: AvatarEmphasis; initials: string; } ) & { shape?: AvatarShape; - size?: Size; - custom?: DeepPartial; -} & MarginProps; + size?: AvatarSize; + custom?: AvatarConfig; +}; diff --git a/src/components/Avatar/Avatar.styles.ts b/src/components/Avatar/Avatar.styles.ts index c473bb1a..06cf9483 100644 --- a/src/components/Avatar/Avatar.styles.ts +++ b/src/components/Avatar/Avatar.styles.ts @@ -1,28 +1,26 @@ -import { AvatarShape } from './AvatarShape.type'; +import type { AvatarAppearance, AvatarShape } from './types'; -import { fromEntries } from '@/services/fromEntries'; -import { Appearance, appearances } from '@/types/Appearance'; -import { BaseProps } from '@/types/BaseProps'; +import type { BaseProps } from '@/types/BaseProps'; import { Emphasis } from '@/types/Emphasis'; -import { Size, sizes } from '@/types/Size'; +import { Size } from '@/types/Size'; export type AvatarConfig = { - nestedImage: BaseProps; - shape: Record; - size: Record; - appearance: Record< - Appearance | 'image', - { - emphasis: Record; - } + shape?: Partial>; + size?: Partial>; + appearance?: Partial< + Record< + AvatarAppearance, + { + emphasis?: Partial>; + } + > >; + innerElements?: { + image?: BaseProps; + }; } & BaseProps; export const defaultConfig = { - nestedImage: { - w: '100%', - h: '100%', - }, overflow: 'hidden', position: 'relative', display: 'flex', @@ -36,18 +34,33 @@ export const defaultConfig = { borderRadius: 'large', }, }, - size: sizes.reduce( - (acc, size) => ({ - ...acc, - [size]: { - w: size, - h: size, - text: `body-${size.includes('xSmall') ? 'strong-xSmall' : size}`, - }, - }), - {} as Record, - ), - + size: { + large: { + w: 'large', + h: 'large', + text: 'body-large', + }, + medium: { + w: 'medium', + h: 'medium', + text: 'body-medium', + }, + small: { + w: 'small', + h: 'small', + text: 'body-small', + }, + xSmall: { + w: 'xSmall', + h: 'xSmall', + text: 'body-strong-xSmall', + }, + '2xSmall': { + w: '2xSmall', + h: '2xSmall', + text: 'body-strong-xSmall', + }, + }, appearance: { image: { emphasis: { @@ -55,22 +68,179 @@ export const defaultConfig = { low: {}, }, }, - ...fromEntries( - appearances.map((appearance) => [ - appearance, - { - emphasis: { - high: { - color: 'nonSemantic-white-content-primary', - backgroundColor: `nonSemantic-${appearance}-background-strong`, - }, - low: { - color: `nonSemantic-${appearance}-content-primary`, - backgroundColor: `nonSemantic-${appearance}-background-muted`, - }, - }, - }, - ]), - ), + blue: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-blue-background-strong', + }, + low: { + color: 'nonSemantic-blue-content-primary', + backgroundColor: 'nonSemantic-blue-background-muted', + }, + }, + }, + green: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-green-background-strong', + }, + low: { + color: 'nonSemantic-green-content-primary', + backgroundColor: 'nonSemantic-green-background-muted', + }, + }, + }, + grey: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-grey-background-strong', + }, + low: { + color: 'nonSemantic-grey-content-primary', + backgroundColor: 'nonSemantic-grey-background-muted', + }, + }, + }, + red: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-red-background-strong', + }, + low: { + color: 'nonSemantic-red-content-primary', + backgroundColor: 'nonSemantic-red-background-muted', + }, + }, + }, + orange: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-orange-background-strong', + }, + low: { + color: 'nonSemantic-orange-content-primary', + backgroundColor: 'nonSemantic-orange-background-muted', + }, + }, + }, + raspberry: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-raspberry-background-strong', + }, + low: { + color: 'nonSemantic-raspberry-content-primary', + backgroundColor: 'nonSemantic-raspberry-background-muted', + }, + }, + }, + magenta: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-magenta-background-strong', + }, + low: { + color: 'nonSemantic-magenta-content-primary', + backgroundColor: 'nonSemantic-magenta-background-muted', + }, + }, + }, + purple: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-purple-background-strong', + }, + low: { + color: 'nonSemantic-purple-content-primary', + backgroundColor: 'nonSemantic-purple-background-muted', + }, + }, + }, + grape: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-grape-background-strong', + }, + low: { + color: 'nonSemantic-grape-content-primary', + backgroundColor: 'nonSemantic-grape-background-muted', + }, + }, + }, + violet: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-violet-background-strong', + }, + low: { + color: 'nonSemantic-violet-content-primary', + backgroundColor: 'nonSemantic-violet-background-muted', + }, + }, + }, + cyan: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-cyan-background-strong', + }, + low: { + color: 'nonSemantic-cyan-content-primary', + backgroundColor: 'nonSemantic-cyan-background-muted', + }, + }, + }, + teal: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-teal-background-strong', + }, + low: { + color: 'nonSemantic-teal-content-primary', + backgroundColor: 'nonSemantic-teal-background-muted', + }, + }, + }, + aquamarine: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-aquamarine-background-strong', + }, + low: { + color: 'nonSemantic-aquamarine-content-primary', + backgroundColor: 'nonSemantic-aquamarine-background-muted', + }, + }, + }, + emerald: { + emphasis: { + high: { + color: 'nonSemantic-white-content-primary', + backgroundColor: 'nonSemantic-emerald-background-strong', + }, + low: { + color: 'nonSemantic-emerald-content-primary', + backgroundColor: 'nonSemantic-emerald-background-muted', + }, + }, + }, + }, + innerElements: { + image: { + w: '100%', + h: '100%', + }, }, } satisfies AvatarConfig; diff --git a/src/components/Avatar/Avatar.test.tsx b/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 00000000..62fded08 --- /dev/null +++ b/src/components/Avatar/Avatar.test.tsx @@ -0,0 +1,75 @@ +import { Avatar } from './Avatar'; +import { render } from '../../tests/render'; + +import { customPropTester } from '@/tests/customPropTester'; + +const getAvatar = (jsx: JSX.Element) => { + const { getByTestId } = render(jsx); + + return getByTestId('avatar'); +}; + +const getImage = (jsx: JSX.Element) => { + const { queryByTestId } = render(jsx); + + return queryByTestId('avatar-image'); +}; + +describe('Avatar', () => { + customPropTester( + , + { + containerId: 'avatar', + props: { + appearance: [ + 'blue', + 'green', + 'grey', + 'red', + 'orange', + 'raspberry', + 'magenta', + 'purple', + 'grape', + 'violet', + 'cyan', + 'teal', + 'aquamarine', + 'emerald', + 'image', + ], + }, + innerElements: { + image: [], + }, + }, + ); + + it('should render the avatar', () => { + const avatar = getAvatar(); + expect(avatar).toBeInTheDocument(); + }); + + it('should render the correct initials', () => { + const avatar = getAvatar(); + expect(avatar).toHaveTextContent('M'); + }); + + it('should render the image', () => { + const image = getImage( + , + ); + expect(image).toBeInTheDocument(); + }); + + it('should not render the image if initials are provided', () => { + const image = getImage(); + expect(image).toBeNull(); + }); +}); diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 034d1e55..dcfc429d 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -1,21 +1,22 @@ -import { FC, ImgHTMLAttributes, useMemo } from 'react'; +import { FC, useMemo } from 'react'; -import { AvatarProps } from './Avatar.props'; +import type { AvatarProps } from './Avatar.props'; import { stylesBuilder } from './stylesBuilder'; import { tet } from '@/tetrisly'; -import { MarginProps } from '@/types/MarginProps'; +import type { MarginProps } from '@/types/MarginProps'; -export const Avatar: FC = ({ +export const Avatar: FC = ({ appearance = 'blue', emphasis = 'low', shape = 'rounded', size = 'medium', - custom = {}, + custom, initials, - ...rest + img, + ...restProps }) => { - const { nestedImage, ...styles } = useMemo( + const styles = useMemo( () => stylesBuilder({ custom, @@ -24,21 +25,13 @@ export const Avatar: FC = ({ [custom, appearance, emphasis, shape, size], ); - const [img, marginProps] = extractImage(rest); - return ( - - {img !== null ? : initials} + + {img ? ( + + ) : ( + initials + )} ); }; - -function extractImage(obj: T) { - if ('img' in obj) { - const { img, ...marginProps } = obj as { - img: Omit, 'color'>; - } & MarginProps; - return [img, marginProps] as const; - } - return [null, obj] as const; -} diff --git a/src/components/Avatar/stylesBuilder/stylesBuilder.ts b/src/components/Avatar/stylesBuilder/stylesBuilder.ts index b0e404ec..0171b79b 100644 --- a/src/components/Avatar/stylesBuilder/stylesBuilder.ts +++ b/src/components/Avatar/stylesBuilder/stylesBuilder.ts @@ -1,7 +1,13 @@ -import { AvatarProps } from '../Avatar.props'; +import type { AvatarProps } from '../Avatar.props'; import { defaultConfig } from '../Avatar.styles'; import { mergeConfigWithCustom } from '@/services'; +import type { BaseProps } from '@/types/BaseProps'; + +type AvatarStylesBuilder = { + container: BaseProps; + image: BaseProps; +}; export const stylesBuilder = ({ custom, @@ -11,19 +17,23 @@ export const stylesBuilder = ({ variant: Required< Pick >; -}) => { +}): AvatarStylesBuilder => { const { appearance: appearanceStyles, shape: shapeStyles, size: sizeStyles, - ...base + innerElements: { image }, + ...restContainerStyles } = mergeConfigWithCustom({ defaultConfig, custom }); const { appearance, emphasis, shape, size } = variant; return { - ...base, - ...appearanceStyles[appearance].emphasis[emphasis], - ...shapeStyles[shape], - ...sizeStyles[size], + container: { + ...appearanceStyles[appearance].emphasis[emphasis], + ...shapeStyles[shape], + ...sizeStyles[size], + ...restContainerStyles, + }, + image, }; }; diff --git a/src/components/Avatar/types/AvatarAppearance.type.ts b/src/components/Avatar/types/AvatarAppearance.type.ts new file mode 100644 index 00000000..e25e7f97 --- /dev/null +++ b/src/components/Avatar/types/AvatarAppearance.type.ts @@ -0,0 +1,17 @@ +export type AvatarAppearanceColors = + | 'blue' + | 'green' + | 'grey' + | 'red' + | 'orange' + | 'raspberry' + | 'magenta' + | 'purple' + | 'grape' + | 'violet' + | 'cyan' + | 'teal' + | 'aquamarine' + | 'emerald'; + +export type AvatarAppearance = AvatarAppearanceColors | 'image'; diff --git a/src/components/Avatar/types/AvatarEmphasis.type.ts b/src/components/Avatar/types/AvatarEmphasis.type.ts new file mode 100644 index 00000000..6de886a7 --- /dev/null +++ b/src/components/Avatar/types/AvatarEmphasis.type.ts @@ -0,0 +1 @@ +export type AvatarEmphasis = 'low' | 'high'; diff --git a/src/components/Avatar/AvatarShape.type.ts b/src/components/Avatar/types/AvatarShape.type.ts similarity index 100% rename from src/components/Avatar/AvatarShape.type.ts rename to src/components/Avatar/types/AvatarShape.type.ts diff --git a/src/components/Avatar/types/AvatarSize.type.ts b/src/components/Avatar/types/AvatarSize.type.ts new file mode 100644 index 00000000..a64d9bd0 --- /dev/null +++ b/src/components/Avatar/types/AvatarSize.type.ts @@ -0,0 +1 @@ +export type AvatarSize = 'large' | 'medium' | 'small' | 'xSmall' | '2xSmall'; diff --git a/src/components/Avatar/types/index.ts b/src/components/Avatar/types/index.ts new file mode 100644 index 00000000..b2e63056 --- /dev/null +++ b/src/components/Avatar/types/index.ts @@ -0,0 +1,7 @@ +export type { + AvatarAppearance, + AvatarAppearanceColors, +} from './AvatarAppearance.type'; +export type { AvatarEmphasis } from './AvatarEmphasis.type'; +export type { AvatarShape } from './AvatarShape.type'; +export type { AvatarSize } from './AvatarSize.type';