-
-
Notifications
You must be signed in to change notification settings - Fork 8
[DSRN] Added BadgeStatus #471
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
Changes from 14 commits
daf6c01
b5beca4
e827ba4
f285227
146d8da
7505011
c4c8754
990de5d
bc2a237
8a431c0
246cfb3
b1c7c1c
c82433f
01bb1b0
92b3a3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| }; | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||
| </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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'>; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does
2.5work?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes