Skip to content

[DSR] Added Box component #724

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

Merged
merged 11 commits into from
Jun 6, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { BoxSpacing } from '../../types';

export const TWCLASSMAP_BOX_GAP: Record<BoxSpacing, string> = {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
5: 'gap-5',
6: 'gap-6',
7: 'gap-7',
8: 'gap-8',
9: 'gap-9',
10: 'gap-10',
11: 'gap-11',
12: 'gap-12',
};
132 changes: 132 additions & 0 deletions packages/design-system-react/src/components/Box/Box.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import {
BoxFlexDirection,
BoxFlexWrap,
BoxAlignItems,
BoxJustifyContent,
} from '../../types';
import { Text } from '../Text';

import { Box } from './Box';
import type { BoxProps } from './Box.types';
import README from './README.mdx';

const meta: Meta<BoxProps> = {
title: 'React Components/Box',
component: Box,
parameters: {
docs: {
page: README,
},
},
argTypes: {
flexDirection: {
control: 'select',
options: Object.keys(BoxFlexDirection),
mapping: BoxFlexDirection,
description: 'The flex-direction style of the component.',
},
flexWrap: {
control: 'select',
options: Object.keys(BoxFlexWrap),
mapping: BoxFlexWrap,
description: 'The flex-wrap style of the component.',
},
gap: {
control: 'number',
description: `The gap between the component's children.`,
},
alignItems: {
control: 'select',
options: Object.keys(BoxAlignItems),
mapping: BoxAlignItems,
description: 'The align-items style of the component.',
},
justifyContent: {
control: 'select',
options: Object.keys(BoxJustifyContent),
mapping: BoxJustifyContent,
description: 'The justify-content style of the component.',
},
className: {
control: 'text',
description:
'Optional prop for additional CSS classes to be applied to the Box component.',
},
},
};

export default meta;

type Story = StoryObj<BoxProps>;
const BoxStory: React.FC<BoxProps> = (args) => {
return (
<Box {...args}>
<Text>Text 1</Text>
<Text>Text 2</Text>
<Text>Text 3</Text>
</Box>
);
};

export const Default: Story = {
render: (args) => <BoxStory {...args} />,
};

export const FlexDirection: Story = {
args: {
flexDirection: BoxFlexDirection.Row,
gap: 2,
},
render: (args) => <BoxStory {...args} />,
};

export const FlexWrap: Story = {
args: {
flexDirection: BoxFlexDirection.Row,
flexWrap: BoxFlexWrap.Wrap,
gap: 2,
className: 'w-1/2',
},
render: (args) => (
<Box {...args}>
<Text>Long text item 1</Text>
<Text>Long text item 2</Text>
<Text>Long text item 3</Text>
<Text>Long text item 4</Text>
</Box>
),
};

export const Gap: Story = {
args: {
gap: 4,
},
render: (args) => <BoxStory {...args} />,
};

export const AlignItems: Story = {
args: {
alignItems: BoxAlignItems.Center,
className: 'h-1/2',
},
render: (args) => <BoxStory {...args} />,
};

export const JustifyContent: Story = {
args: {
flexDirection: BoxFlexDirection.Row,
justifyContent: BoxJustifyContent.Between,
},
render: (args) => <BoxStory {...args} />,
};

export const ClassName: Story = {
args: {
className:
'border-2 border-dashed border-warning-default bg-warning-muted p-3',
},
render: (args) => <BoxStory {...args} />,
};
146 changes: 146 additions & 0 deletions packages/design-system-react/src/components/Box/Box.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { render, screen } from '@testing-library/react';
import React from 'react';

import {
BoxAlignItems,
BoxFlexDirection,
BoxFlexWrap,
BoxJustifyContent,
} from '../../types';

import { Box } from './Box';
import { TWCLASSMAP_BOX_GAP } from './Box.constants';

describe('Box', () => {
it('renders children and style', () => {
render(
<Box data-testid="box" style={{ margin: 4 }}>
<span>Hello</span>
</Box>,
);
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.getByTestId('box')).toHaveStyle({ margin: '4px' });
});

it('applies default flex class', () => {
render(<Box data-testid="box" />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
});

it('applies flexDirection prop', () => {
render(<Box data-testid="box" flexDirection={BoxFlexDirection.Column} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(BoxFlexDirection.Column);
});

it('applies flexWrap prop', () => {
render(<Box data-testid="box" flexWrap={BoxFlexWrap.Wrap} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(BoxFlexWrap.Wrap);
});

it('applies gap prop using spacing scale', () => {
render(<Box data-testid="box" gap={4} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(TWCLASSMAP_BOX_GAP[4]);
});

it('applies alignItems prop', () => {
render(<Box data-testid="box" alignItems={BoxAlignItems.Center} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(BoxAlignItems.Center);
});

it('applies justifyContent prop', () => {
render(
<Box data-testid="box" justifyContent={BoxJustifyContent.Between} />,
);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(BoxJustifyContent.Between);
});

it('applies className prop', () => {
render(<Box data-testid="box" className="custom-class bg-red-500 p-4" />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass('custom-class');
expect(box).toHaveClass('p-4');
expect(box).toHaveClass('bg-red-500');
});

it('applies all flex props together', () => {
render(
<Box
data-testid="box"
flexDirection={BoxFlexDirection.Row}
flexWrap={BoxFlexWrap.Wrap}
gap={2}
alignItems={BoxAlignItems.Center}
justifyContent={BoxJustifyContent.Between}
className="extra-class"
/>,
);

const box = screen.getByTestId('box');
const expectedClasses = [
'flex',
BoxFlexDirection.Row,
BoxFlexWrap.Wrap,
TWCLASSMAP_BOX_GAP[2],
BoxAlignItems.Center,
BoxJustifyContent.Between,
'extra-class',
];

expectedClasses.forEach((className) => {
expect(box).toHaveClass(className);
});
});

it('handles gap prop with value 0', () => {
render(<Box data-testid="box" gap={0} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(TWCLASSMAP_BOX_GAP[0]);
});

it('handles gap prop with maximum value', () => {
render(<Box data-testid="box" gap={12} />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');
expect(box).toHaveClass(TWCLASSMAP_BOX_GAP[12]);
});

it('does not apply gap class when gap is undefined', () => {
render(<Box data-testid="box" />);
const box = screen.getByTestId('box');
expect(box).toHaveClass('flex');

// Check that no gap classes are applied
Object.values(TWCLASSMAP_BOX_GAP).forEach((gapClass) => {
expect(box).not.toHaveClass(gapClass);
});
});

it('forwards other props to the div element', () => {
const mockClick = jest.fn();
render(
<Box
data-testid="box"
role="main"
aria-label="Test box"
onClick={mockClick}
/>,
);
const box = screen.getByTestId('box');
expect(box).toHaveAttribute('role', 'main');
expect(box).toHaveAttribute('aria-label', 'Test box');
expect(box.tagName).toBe('DIV');
});
});
34 changes: 34 additions & 0 deletions packages/design-system-react/src/components/Box/Box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

import { twMerge } from '../../utils/tw-merge';

import { TWCLASSMAP_BOX_GAP } from './Box.constants';
import type { BoxProps } from './Box.types';

export const Box = ({
flexDirection,
flexWrap,
gap,
alignItems,
justifyContent,
className = '',
style,
children,
...props
}: BoxProps) => {
const mergedClassName = twMerge(
'flex',
flexDirection,
flexWrap,
gap !== undefined ? TWCLASSMAP_BOX_GAP[gap] : '',
alignItems,
justifyContent,
className,
);

return (
<div className={mergedClassName} style={style} {...props}>
{children}
</div>
);
};
37 changes: 37 additions & 0 deletions packages/design-system-react/src/components/Box/Box.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ComponentProps } from 'react';

import type {
BoxFlexDirection,
BoxFlexWrap,
BoxSpacing,
BoxAlignItems,
BoxJustifyContent,
} from '../../types';

export type BoxProps = ComponentProps<'div'> & {
/**
* The flex-direction style of the component.
*/
flexDirection?: BoxFlexDirection;
/**
* The flex-wrap style of the component.
*/
flexWrap?: BoxFlexWrap;
/**
* The gap between the component's children.
* Use 0-12 for a gap of 0px-48px.
*/
gap?: BoxSpacing;
/**
* The align-items style of the component.
*/
alignItems?: BoxAlignItems;
/**
* The justify-content style of the component.
*/
justifyContent?: BoxJustifyContent;
/**
* Optional prop for additional CSS classes to be applied to the Box component.
*/
className?: string;
};
Loading

Unchanged files with check annotations Beta

}
declare module '*.svg' {
import type * as React from 'react';

Check warning on line 7 in packages/design-system-react/global.d.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

Import name `React` must match one of the following formats: camelCase
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
Neutral = 'neutral',
Info = 'info',
Success = 'success',
Error = 'error',

Check warning on line 75 in packages/design-system-react-native/src/types/index.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

'Error' is already a global variable
Warning = 'warning',
}
Edit = 'Edit',
EncryptedAdd = 'EncryptedAdd',
Eraser = 'Eraser',
Error = 'Error',

Check warning on line 461 in packages/design-system-react-native/src/types/index.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

'Error' is already a global variable
Ethereum = 'Ethereum',
Exchange = 'Exchange',
ExpandVertical = 'ExpandVertical',
Eye = 'Eye',
FaceId = 'FaceId',
Feedback = 'Feedback',
File = 'File',

Check warning on line 474 in packages/design-system-react-native/src/types/index.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

'File' is already a global variable
Filter = 'Filter',
Fingerprint = 'Fingerprint',
Fire = 'Fire',
Login = 'Login',
Logout = 'Logout',
Mail = 'Mail',
Map = 'Map',

Check warning on line 518 in packages/design-system-react-native/src/types/index.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

'Map' is already a global variable
Menu = 'Menu',
MessageQuestion = 'MessageQuestion',
Messages = 'Messages',
it('throws an error for an invalid variant', () => {
const consoleErrorMock = jest
.spyOn(console, 'error')
.mockImplementation(() => {}); // Suppress error logs

Check warning on line 39 in packages/design-system-react-native/src/components/Button/Button.test.tsx

GitHub Actions / Lint, build, and test / Lint (22.x)

Unexpected empty arrow function
expect(() =>
render(
positionXOffset = 0,
positionYOffset = 0,
customPosition,
twClassName = '',

Check warning on line 23 in packages/design-system-react-native/src/components/BadgeWrapper/BadgeWrapper.tsx

GitHub Actions / Lint, build, and test / Lint (22.x)

'twClassName' is assigned a value but never used. Allowed unused args must match /[_]+/u
style,
...props
}: BadgeWrapperProps) => {
};
const { getByTestId } = render(<TestComponent />);
const badgeIcon = getByTestId('badge-icon');
expect(badgeIcon.props.style[1]).toEqual(customStyle);

Check warning on line 55 in packages/design-system-react-native/src/components/BadgeIcon/BadgeIcon.test.tsx

GitHub Actions / Lint, build, and test / Lint (22.x)

Use `toStrictEqual()` instead
expect(badgeIcon.props.accessibilityLabel).toBe('badge-icon');
});
});
const { getByTestId } = render(<TestComponent />);
const container = getByTestId('badge-count');
// The container style is an array; customStyle should be included.
expect(container.props.style).toEqual(

Check warning on line 160 in packages/design-system-react-native/src/components/BadgeCount/BadgeCount.test.tsx

GitHub Actions / Lint, build, and test / Lint (22.x)

Use `toStrictEqual()` instead
expect.arrayContaining([customStyle]),
);
expect(container.props.accessibilityLabel).toBe('badge');
declare module '*.svg' {
import type * as React from 'react';

Check warning on line 2 in packages/design-system-react-native/global.d.ts

GitHub Actions / Lint, build, and test / Lint (22.x)

Import name `React` must match one of the following formats: camelCase
import type { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;