Skip to content

Commit

Permalink
Merge branch 'main' of github.com:MetaMask/metamask-design-system int…
Browse files Browse the repository at this point in the history
…o dsrn/avatar-favicon
  • Loading branch information
brianacnguyen committed Feb 28, 2025
2 parents 4ca80a4 + 64c05d9 commit 10dad1d
Show file tree
Hide file tree
Showing 21 changed files with 802 additions and 5 deletions.
2 changes: 2 additions & 0 deletions apps/storybook-react-native/.storybook/storybook.requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ try {
const getStories = () => {
return {
"./../../packages/design-system-react-native/src/components/AvatarFavicon/AvatarFavicon.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarFavicon/AvatarFavicon.stories.tsx"),
"./../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx"),
"./../../packages/design-system-react-native/src/components/Button/Button.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/Button.stories.tsx"),
"./../../packages/design-system-react-native/src/components/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/variants/ButtonPrimary/ButtonPrimary.stories.tsx"),
"./../../packages/design-system-react-native/src/components/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/variants/ButtonSecondary/ButtonSecondary.stories.tsx"),
Expand All @@ -60,6 +61,7 @@ const getStories = () => {
"./../../packages/design-system-react-native/src/primitives/ButtonAnimated/ButtonAnimated.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ButtonAnimated/ButtonAnimated.stories.tsx"),
"./../../packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ButtonBase/ButtonBase.stories.tsx"),
"./../../packages/design-system-react-native/src/primitives/ImageOrSvg/ImageOrSvg.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/ImageOrSvg/ImageOrSvg.stories.tsx"),
"./../../packages/design-system-react-native/src/primitives/Jazzicon/Jazzicon.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/Jazzicon/Jazzicon.stories.tsx"),
"./../../packages/design-system-react-native/src/primitives/TextOrChildren/TextOrChildren.stories.tsx": require("../../../packages/design-system-react-native/src/primitives/TextOrChildren/TextOrChildren.stories.tsx"),
"./../../packages/design-system-react-native/src/temp-components/Spinner/Spinner.stories.tsx": require("../../../packages/design-system-react-native/src/temp-components/Spinner/Spinner.stories.tsx"),
};
Expand Down
1 change: 1 addition & 0 deletions packages/design-system-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@metamask/design-system-twrnc-preset": "workspace:^",
"react": "^18.2.0",
"react-native": "^0.72.15",
"react-native-jazzicon": "^0.1.2",
"react-native-reanimated": "3.3.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { AvatarIconProps } from './AvatarIcon.types';
import { AvatarIconSeverity } from './AvatarIcon.types';
import { IconSize, IconColor } from '../Icon';
import { AvatarIconSize, AvatarBaseShape } from '../../shared/enums';

// Mappings
export const TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR: Record<
AvatarIconSeverity,
string
> = {
[AvatarIconSeverity.Default]: 'bg-background-muted',
[AvatarIconSeverity.Info]: 'bg-info-muted',
[AvatarIconSeverity.Success]: 'bg-success-muted',
[AvatarIconSeverity.Error]: 'bg-error-muted',
[AvatarIconSeverity.Warning]: 'bg-warning-muted',
};
export const MAP_AVATARICON_SIZE_ICONSIZE: Record<AvatarIconSize, IconSize> = {
[AvatarIconSize.Xs]: IconSize.Xs, // 16px avatar -> 12px icon
[AvatarIconSize.Sm]: IconSize.Sm, // 24px avatar -> 16px icon
[AvatarIconSize.Md]: IconSize.Md, // 32px avatar -> 20px icon
[AvatarIconSize.Lg]: IconSize.Lg, // 40px avatar -> 24px icon
[AvatarIconSize.Xl]: IconSize.Xl, // 48px avatar -> 32px icon
};
export const MAP_AVATARICON_SEVERITY_ICONCOLOR: Record<
AvatarIconSeverity,
IconColor
> = {
[AvatarIconSeverity.Default]: IconColor.IconAlternative,
[AvatarIconSeverity.Info]: IconColor.InfoDefault,
[AvatarIconSeverity.Success]: IconColor.SuccessDefault,
[AvatarIconSeverity.Error]: IconColor.ErrorDefault,
[AvatarIconSeverity.Warning]: IconColor.WarningDefault,
};

// Defaults
export const DEFAULT_AVATARICON_PROPS: Required<
Pick<AvatarIconProps, 'size' | 'shape' | 'severity'>
> = {
size: AvatarIconSize.Md,
shape: AvatarBaseShape.Circle,
severity: AvatarIconSeverity.Default,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { View } from 'react-native';

import AvatarIcon from './AvatarIcon';
import { DEFAULT_AVATARICON_PROPS } from './AvatarIcon.constants';
import type { AvatarIconProps } from './AvatarIcon.types';
import { AvatarSize } from '../../shared/enums';
import { IconName } from '../Icon';
import { AvatarIconSeverity } from './AvatarIcon.types';

const meta: Meta<AvatarIconProps> = {
title: 'Components/AvatarIcon',
component: AvatarIcon,
argTypes: {
size: {
control: 'select',
options: AvatarSize,
},
severity: {
control: 'select',
options: AvatarIconSeverity,
},
iconName: {
control: 'select',
options: IconName,
},
twClassName: {
control: 'text',
},
},
};

export default meta;

type Story = StoryObj<AvatarIconProps>;

export const Default: Story = {
args: {
size: DEFAULT_AVATARICON_PROPS.size,
severity: DEFAULT_AVATARICON_PROPS.severity,
iconName: IconName.Arrow2UpRight,
twClassName: '',
},
};

export const Sizes: Story = {
render: () => (
<View style={{ gap: 16 }}>
{Object.keys(AvatarSize).map((sizeKey) => (
<AvatarIcon
key={sizeKey}
size={AvatarSize[sizeKey as keyof typeof AvatarSize]}
iconName={IconName.Arrow2UpRight}
/>
))}
</View>
),
};

export const Severities: Story = {
render: () => (
<View style={{ gap: 16 }}>
{Object.keys(AvatarIconSeverity).map((severityKey) => (
<AvatarIcon
key={severityKey}
severity={
AvatarIconSeverity[severityKey as keyof typeof AvatarIconSeverity]
}
iconName={IconName.Arrow2UpRight}
/>
))}
</View>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { render } from '@testing-library/react-native';

import { IconName } from '../Icon';
import { AvatarIconSize } from '../../shared/enums';
import { generateAvatarIconContainerClassNames } from './AvatarIcon.utilities';
import {
DEFAULT_AVATARICON_PROPS,
TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR,
} from './AvatarIcon.constants';
import AvatarIcon from './AvatarIcon';
import { AvatarIconSeverity } from './AvatarIcon.types';

describe('AvatarIcon', () => {
describe('generateAvatarIconContainerClassNames', () => {
it('returns correct class names for default state', () => {
const classNames = generateAvatarIconContainerClassNames({});
expect(classNames).toStrictEqual(
`${TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR[DEFAULT_AVATARICON_PROPS.severity]}`,
);
});

it('applies correct severity class', () => {
Object.values(AvatarIconSeverity).forEach((severity) => {
const expectedClass =
TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR[severity];
const classNames = generateAvatarIconContainerClassNames({ severity });
expect(classNames).toStrictEqual(expectedClass);
});
});

it('appends additional Tailwind class names', () => {
const classNames = generateAvatarIconContainerClassNames({
twClassName: 'shadow-lg ring-2',
});
expect(classNames).toStrictEqual(
`${TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR[DEFAULT_AVATARICON_PROPS.severity]} shadow-lg ring-2`,
);
});

it('applies severity and additional classes together correctly', () => {
const severity = AvatarIconSeverity.Success;
const classNames = generateAvatarIconContainerClassNames({
severity,
twClassName: 'border border-green-500',
});
expect(classNames).toStrictEqual(
`${TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR[severity]} border border-green-500`,
);
});
});
describe('AvatarIcon Component', () => {
it('renders with default props', () => {
const { getByTestId: getByTestIdIcon } = render(
<AvatarIcon
iconName={IconName.Add}
iconProps={{ testID: 'inner-icon' }}
/>,
);
const icon = getByTestIdIcon('inner-icon');

expect(icon.props.name).toStrictEqual(IconName.Add);
});

it('renders with custom props', () => {
const customSize = AvatarIconSize.Lg;
const customSeverity = AvatarIconSeverity.Error;
const customIconProps = { testID: 'custom-icon', extraProp: 'value' };

// Render separately to test the Icon props.
const { getByTestId: getIcon } = render(
<AvatarIcon
iconName={IconName.Close}
size={customSize}
severity={customSeverity}
iconProps={customIconProps}
/>,
);
const icon = getIcon('custom-icon');
expect(icon.props.name).toStrictEqual(IconName.Close);
expect(icon.props.extraProp).toStrictEqual('value');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import React, { useMemo } from 'react';

import {
DEFAULT_AVATARICON_PROPS,
MAP_AVATARICON_SIZE_ICONSIZE,
MAP_AVATARICON_SEVERITY_ICONCOLOR,
} from './AvatarIcon.constants';
import type { AvatarIconProps } from './AvatarIcon.types';
import { generateAvatarIconContainerClassNames } from './AvatarIcon.utilities';
import Icon, { IconProps } from '../Icon';
import AvatarBase from '../../primitives/AvatarBase';

const AvatarIcon = ({
size = DEFAULT_AVATARICON_PROPS.size,
shape = DEFAULT_AVATARICON_PROPS.shape,
severity = DEFAULT_AVATARICON_PROPS.severity,
iconName,
iconProps,
twClassName = '',
style,
...props
}: AvatarIconProps) => {
const tw = useTailwind();
const twContainerClassNames = useMemo(() => {
return generateAvatarIconContainerClassNames({
severity,
twClassName,
});
}, [severity, twClassName]);

return (
<AvatarBase
size={size}
shape={shape}
style={[tw`${twContainerClassNames}`, style]}
accessibilityRole="image"
{...props}
>
<Icon
name={iconName}
size={MAP_AVATARICON_SIZE_ICONSIZE[size]}
color={MAP_AVATARICON_SEVERITY_ICONCOLOR[severity]}
{...iconProps}
/>
</AvatarBase>
);
};

export default AvatarIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AvatarBaseProps } from '../../primitives/AvatarBase';
import { IconName, IconProps } from '../Icon';

export enum AvatarIconSeverity {
Default = 'default',
Info = 'info',
Success = 'success',
Error = 'error',
Warning = 'warning',
}

/**
* AvatarIcon component props.
*/
export type AvatarIconProps = {
/**
* Optional prop to control the severity of the avatar
* @default AvatarIconSeverity.Default
*/
severity?: AvatarIconSeverity;
/**
* Optional prop to specify an icon to show
*/
iconName: IconName;
/**
* Optional prop to pass additional properties to the icon
*/
iconProps?: Omit<IconProps, 'name'>;
} & Omit<AvatarBaseProps, 'children' | 'fallbackText' | 'fallbackTextProps'>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable jsdoc/check-param-names */
/* eslint-disable jsdoc/require-param */
import type { AvatarIconProps } from './AvatarIcon.types';
import {
TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR,
DEFAULT_AVATARICON_PROPS,
} from './AvatarIcon.constants';

/**
* Generates a Tailwind class name string for the background color of an avatar icon.
*
* This function constructs a class name string based on the icon's `severity`
* and optional additional Tailwind class names.
*
* @param severity - The severity level of the avatar icon, affecting background color.
* @param twClassName - Additional Tailwind class names for customization.
* @returns A string of Tailwind class names representing the avatar icon's container styles.
*
* Example:
* ```
* const classNames = generateAvatarIconContainerClassNames({
* severity: 'error',
* twClassName: 'border border-red-500',
* });
*
* console.log(classNames);
* // Output: "bg-error border border-red-500"
* ```
*/
export const generateAvatarIconContainerClassNames = ({
severity = DEFAULT_AVATARICON_PROPS.severity,
twClassName = '',
}: Partial<AvatarIconProps>): string => {
return `${TWCLASSMAP_AVATARICON_SEVERITY_BACKGROUNDCOLOR[severity]} ${twClassName}`.trim();
};
Loading

0 comments on commit 10dad1d

Please sign in to comment.