Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const getStories = () => {
"./../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx"),
"./../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx"),
"./../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx"),
"./../../packages/design-system-react-native/src/components/BadgeStatus/BadgeStatus.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeStatus/BadgeStatus.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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BadgeStatusStatus, BadgeStatusSize } from './BadgeStatus.types';

// Mappings
export const TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE: Record<
BadgeStatusStatus,
string
> = {
[BadgeStatusStatus.Active]: 'bg-success-default border-success-default',
[BadgeStatusStatus.PartiallyActive]:
'bg-background-default border-success-default',
[BadgeStatusStatus.Inactive]: 'bg-icon-muted border-icon-muted',
[BadgeStatusStatus.New]: 'bg-primary-default border-primary-default',
[BadgeStatusStatus.Attention]: 'bg-error-default border-error-default',
};

export const TWCLASSMAP_BADGESTATUS_SIZE: Record<BadgeStatusSize, string> = {
[BadgeStatusSize.Md]: 'h-2 w-2', // 8px width and height
[BadgeStatusSize.Lg]: 'h-2.5 w-2.5', // 10px width and height
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 2.5 work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { View, ViewProps } from 'react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';

import BadgeStatus from './BadgeStatus';
import type { BadgeStatusProps } from './BadgeStatus.types';
import { BadgeStatusStatus, BadgeStatusSize } from './BadgeStatus.types';

const meta: Meta<BadgeStatusProps> = {
title: 'Components/BadgeStatus',
component: BadgeStatus,
argTypes: {
size: {
control: 'select',
options: BadgeStatusSize,
},
status: {
control: 'select',
options: BadgeStatusStatus,
},
hasBorder: {
control: 'boolean',
},
twClassName: {
control: 'text',
},
},
};

export default meta;

const BadgeStatusStoryWrapper: React.FC<ViewProps> = ({
children,
...props
}) => {
const tw = useTailwind();
return (
<View {...props} style={[tw`bg-warning-muted`, props.style]}>
{children}
</View>
);
};

type Story = StoryObj<BadgeStatusProps>;

export const Default: Story = {
args: {
size: BadgeStatusSize.Md,
status: BadgeStatusStatus.Active,
hasBorder: true,
twClassName: '',
Comment on lines +48 to +51
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice to read! Nice work

},
render: (args) => (
<BadgeStatusStoryWrapper>
<BadgeStatus {...args} />
</BadgeStatusStoryWrapper>
),
};

export const Sizes: Story = {
render: () => (
<BadgeStatusStoryWrapper style={{ gap: 16 }}>
{Object.keys(BadgeStatusSize).map((sizeKey) => (
<BadgeStatus
key={sizeKey}
size={BadgeStatusSize[sizeKey as keyof typeof BadgeStatusSize]}
status={BadgeStatusStatus.Active}
/>
))}
</BadgeStatusStoryWrapper>
),
};

export const Statuses: Story = {
render: () => (
<BadgeStatusStoryWrapper style={{ gap: 16 }}>
{Object.keys(BadgeStatusStatus).map((statusKey) => (
<BadgeStatus
key={statusKey}
status={
BadgeStatusStatus[statusKey as keyof typeof BadgeStatusStatus]
}
/>
))}
</BadgeStatusStoryWrapper>
),
};

export const HasBorder: Story = {
render: () => (
<BadgeStatusStoryWrapper style={{ gap: 16 }}>
{Object.keys(BadgeStatusStatus).map((statusKey) => (
<View key={statusKey} style={{ gap: 4 }}>
<BadgeStatus
status={
BadgeStatusStatus[statusKey as keyof typeof BadgeStatusStatus]
}
/>
<BadgeStatus
status={
BadgeStatusStatus[statusKey as keyof typeof BadgeStatusStatus]
}
hasBorder={false}
/>
</View>
))}
</BadgeStatusStoryWrapper>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import BadgeStatus from './BadgeStatus';
import { BadgeStatusStatus, BadgeStatusSize } from './BadgeStatus.types';
import {
TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE,
TWCLASSMAP_BADGESTATUS_SIZE,
} from './BadgeStatus.constants';
import { useTailwind } from '@metamask/design-system-twrnc-preset';

describe('BadgeStatus', () => {
it('renders with default props and status Active', () => {
let expectedOuter;
let expectedInner;
const TestComponent = () => {
const tw = useTailwind();
const finalSize = BadgeStatusSize.Md;
expectedOuter = tw`
self-start
rounded-full
border-2 border-background-default
`;
expectedInner = tw`
rounded-full
border-2
${TWCLASSMAP_BADGESTATUS_SIZE[finalSize]}
${TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE[BadgeStatusStatus.Active]}
`;
return <BadgeStatus status={BadgeStatusStatus.Active} testID="badge" />;
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
expect(badge.props.style[0]).toStrictEqual(expectedOuter);
// The inner view is rendered as the child of the outer View.
const inner = badge.props.children[1];
expect(inner.props.style[0]).toStrictEqual(expectedInner);
});

it('renders without border when hasBorder is false', () => {
let expectedOuter;
const TestComponent = () => {
const tw = useTailwind();
expectedOuter = tw`
self-start
rounded-full
`;
return (
<BadgeStatus
status={BadgeStatusStatus.New}
hasBorder={false}
testID="badge"
/>
);
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
expect(badge.props.style[0]).toStrictEqual(expectedOuter);
});

it('applies custom style to the outer container', () => {
const customStyle = { margin: 10 };
const TestComponent = () => {
return (
<BadgeStatus
status={BadgeStatusStatus.Inactive}
style={customStyle}
testID="badge"
/>
);
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
// The outer container style is an array; the second element should equal customStyle.
expect(badge.props.style[1]).toStrictEqual(customStyle);
});

it('forwards additional props to the outer container', () => {
const extraProp = { accessibilityLabel: 'status-badge' };
const TestComponent = () => {
return (
<BadgeStatus
status={BadgeStatusStatus.Attention}
testID="badge"
{...extraProp}
/>
);
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
expect(badge.props.accessibilityLabel).toStrictEqual('status-badge');
});

it('renders with custom size and status PartiallyActive', () => {
let expectedInner;
const customSize = BadgeStatusSize.Lg; // For example, '10'
const TestComponent = () => {
const tw = useTailwind();
expectedInner = tw`
rounded-full
border-2
${TWCLASSMAP_BADGESTATUS_SIZE[customSize]}
${TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE[BadgeStatusStatus.PartiallyActive]}
`;
return (
<BadgeStatus
status={BadgeStatusStatus.PartiallyActive}
size={customSize}
testID="badge"
/>
);
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
const inner = badge.props.children[1];
expect(inner.props.style[0]).toStrictEqual(expectedInner);
});

it('uses default size and hasBorder when not provided', () => {
let expectedOuter;
let expectedInner;
const TestComponent = () => {
const tw = useTailwind();
const defaultSize = BadgeStatusSize.Md;
expectedOuter = tw`
self-start
rounded-full
border-2 border-background-default
`;
expectedInner = tw`
rounded-full
border-2
${TWCLASSMAP_BADGESTATUS_SIZE[defaultSize]}
${TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE[BadgeStatusStatus.Active]}
`;
return <BadgeStatus status={BadgeStatusStatus.Active} testID="badge" />;
};

const { getByTestId } = render(<TestComponent />);
const badge = getByTestId('badge');
expect(badge.props.style[0]).toStrictEqual(expectedOuter);
const inner = badge.props.children[1];
expect(inner.props.style[0]).toStrictEqual(expectedInner);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import React from 'react';
import { View } from 'react-native';

import {
TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE,
TWCLASSMAP_BADGESTATUS_SIZE,
} from './BadgeStatus.constants';
import type { BadgeStatusProps } from './BadgeStatus.types';
import { BadgeStatusSize } from './BadgeStatus.types';

const BadgeStatus = ({
status,
size = BadgeStatusSize.Md,
hasBorder = true,
twClassName = '',
style,
...props
}: BadgeStatusProps) => {
const tw = useTailwind();

return (
<View
style={[
tw`
self-start
rounded-full
${hasBorder ? 'border-2 border-background-default' : ''}
${twClassName}`,
style,
]}
{...props}
>
<View
style={tw`bg-background-default absolute top-0 left-0 bottom-0 right-0 rounded-full`}
/>
<View
style={[
tw`
rounded-full
border-2
${TWCLASSMAP_BADGESTATUS_SIZE[size]}
${TWCLASSMAP_BADGESTATUS_STATUS_CIRCLE[status]}
`,
]}
/>
Comment on lines +46 to +47
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: Consider adding a prop to allow access to this child element.

Suggested change
]}
/>
]}
{...innerBadgeProps}
/>

</View>
);
};

export default BadgeStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ViewProps } from 'react-native';

/**
* The status of BadgeStatus
*/
export enum BadgeStatusStatus {
Active = 'active',
PartiallyActive = 'partiallyactive',
Inactive = 'inactive',
New = 'new',
Attention = 'attention',
}
Comment on lines +6 to +12
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider adding comments here to explain what these mean in detail. It's somewhat of a niche component.

/**
* The size of BadgeStatus
*/
export enum BadgeStatusSize {
/**
* Represents a medium badge status size (8px).
*/
Md = 'Md',
/**
* Represents a large avatar size (10px).
*/
Lg = 'Lg',
}
/**
* BadgeStatus component props.
*/
export type BadgeStatusProps = {
/**
* Optional prop to control the status of the badge
* Possible values:
* - BadgeStatusStatus.Active.
* - BadgeStatusStatus.PartiallyActive.
* - BadgeStatusStatus.Inactive.
* - BadgeStatusStatus.New.
* - BadgeStatusStatus.Attention.
*/
status: BadgeStatusStatus;
/**
* Optional prop to determine whether the badge should display a border
* @default true
*/
hasBorder?: boolean;
/**
* Optional prop to control the size of the BadgeStatus
* Possible values:
* - BadgeStatusSize.Md (8px),
* - BadgeStatusSize.Lg (10px),
* @default AvatarBaseSize.Md
*/
size?: BadgeStatusSize;
/**
* Optional prop to add twrnc overriding classNames.
*/
twClassName?: string;
} & Omit<ViewProps, 'children'>;
Loading
Loading