Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"docs:build": "docusaurus build",
"test:type": "tsc --skipLibCheck",
"test": "vitest run",
"test:watch": "vitest watch",
"release:package": "pnpm run build && changeset publish"
},
"peerDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
* @packageDocumentation Monolith에서 구성하는 공통 컴포넌트들이 구성되어있는 영역 입니다.
*/

export { Stack } from './stack/Stack';
export type { StackProps } from './stack/Stack';
export { SwitchCase } from './switch-case/SwitchCase';
38 changes: 38 additions & 0 deletions src/components/stack/Stack.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.flex {
display: flex;
}

/* flex-direction */
.flex-column { flex-direction: column; }
.flex-column-reverse { flex-direction: column-reverse; }
.flex-row { flex-direction: row; }
.flex-row-reverse { flex-direction: row-reverse; }

/* flex-wrap */
.flex-nowrap { flex-wrap: nowrap; }
.flex-wrap { flex-wrap: wrap; }
.flex-wrap-reverse { flex-wrap: wrap-reverse; }

/* align-items */
.align-center { align-items: center; }
.align-start { align-items: start; }
.align-end { align-items: end; }
.align-flex-start { align-items: flex-start; }
.align-flex-end { align-items: flex-end; }
.align-self-start { align-items: self-start; }
.align-self-end { align-items: self-end; }
.align-baseline { align-items: baseline; }
.align-stretch { align-items: stretch; }

/* justify-content */
.justify-center { justify-content: center; }
.justify-start { justify-content: start; }
.justify-end { justify-content: end; }
.justify-flex-start { justify-content: flex-start; }
.justify-flex-end { justify-content: flex-end; }
.justify-left { justify-content: left; }
.justify-right { justify-content: right; }
.justify-space-between { justify-content: space-between; }
.justify-space-around { justify-content: space-around; }
.justify-space-evenly { justify-content: space-evenly; }
.justify-stretch { justify-content: stretch; }
133 changes: 133 additions & 0 deletions src/components/stack/Stack.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { render } from '@testing-library/react';
import { Stack } from './Stack';
import '@testing-library/jest-dom';

describe('Stack', () => {
it('자식 컴포넌트들을 렌더링합니다', () => {
const { getByText } = render(
<Stack>
<div>1번</div>
<div>2번</div>
<div>3번</div>
</Stack>,
);
expect(getByText('1번')).toBeInTheDocument();
expect(getByText('2번')).toBeInTheDocument();
expect(getByText('3번')).toBeInTheDocument();
});

it('direction prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack direction='column'>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveClass('flex flex-column');
});

it('wrap prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack wrap='wrap'>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveClass('flex flex-wrap');
});

it('align prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack align='center'>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveClass('flex align-center');
});

it('justify prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack justify='center'>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveClass('flex justify-center');
});

it('gap prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack gap={10}>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveStyle({ gap: '10px' });
});

it('width prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack width={100}>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveStyle({ width: '100px' });
});

it('height prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack height={100}>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveStyle({ height: '100px' });
});

it('margin prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack m={24}>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveStyle({ margin: '24px' });
});

it('padding prop이 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack p={16}>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveStyle({ padding: '16px' });
});

it('모든 props가 정상적으로 적용됩니다', () => {
const { container } = render(
<Stack
direction='column'
wrap='nowrap'
align='center'
justify='center'
gap={10}
width={100}
height={100}
m={24}
p={16}
mt={16}
ml={24}
mr={24}
mb={16}
pt={16}
pl={24}
pr={24}
pb={16}
>
<div>1번</div>
</Stack>,
);
expect(container.firstChild).toHaveClass('flex flex-column flex-nowrap align-center justify-center');
expect(container.firstChild).toHaveStyle({
gap: '10px',
width: '100px',
height: '100px',
margin: '16px 24px 16px 24px',
padding: '16px 24px 16px 24px',
});
});
});
140 changes: 140 additions & 0 deletions src/components/stack/Stack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { PropsWithChildren, CSSProperties, HTMLAttributes, forwardRef } from 'react';
import './Stack.css';
import { buildFlexClassNames } from './utils/buildFlexClassNames';

/** The props type of {@link Stack | 'Stack'}. */
export interface StackProps extends PropsWithChildren {
/** flex-direction 속성을 지정합니다. */
direction?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
/** flex-wrap 속성을 지정합니다. */
wrap?: 'wrap' | 'nowrap' | 'wrap-reverse';
/** align-items 속성을 지정합니다. */
align?: 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'baseline' | 'stretch';
/** justify-content 속성을 지정합니다. */
justify?:
| 'center'
| 'start'
| 'end'
| 'flex-start'
| 'flex-end'
| 'left'
| 'right'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'stretch';
/** gap 속성을 지정합니다. */
gap?: CSSProperties['gap'];
/** width 속성을 지정합니다. */
width?: number | string;
/** height 속성을 지정합니다. */
height?: number | string;
/** margin 속성을 지정합니다. */
m?: number | string;
/** margin-top 속성을 지정합니다. */
mt?: number | string;
/** margin-left 속성을 지정합니다. */
ml?: number | string;
/** margin-right 속성을 지정합니다. */
mr?: number | string;
/** margin-bottom 속성을 지정합니다. */
mb?: number | string;
/** padding 속성을 지정합니다. */
p?: number | string;
/** padding-top 속성을 지정합니다. */
pt?: number | string;
/** padding-left 속성을 지정합니다. */
pl?: number | string;
/** padding-right 속성을 지정합니다. */
pr?: number | string;
/** padding-bottom 속성을 지정합니다. */
pb?: number | string;
}

/**
* 자식 컴포넌트를 스택으로 렌더링하는 컴포넌트입니다.
*
* @category Component
* @param props - {@link StackProps}를 참조하세요.
* @returns 해당하는 컴포넌트를 반환합니다.
*
* @example
* ```tsx
* // 중앙 정렬, 10px 간격
* <Stack justify='center' align='center' gap={10}>
* {children}
* </Stack>
*
* // 양끝 정렬
* <Stack justify='space-between'>
* {children}
* </Stack>
*
* // 상단 정렬
* <Stack align='flex-start'>
* {children}
* </Stack>
*
* // 가로 방향 정렬
* <Stack direction='row'>
* {children}
* </Stack>
*
* // 세로 방향 정렬
* <Stack direction='column'>
* {children}
* </Stack>
* ```
*/
export const Stack = forwardRef<HTMLDivElement, StackProps & HTMLAttributes<HTMLDivElement>>(
(
{
children,
className,
direction,
wrap,
align,
justify,
gap,
width,
height,
m,
mt,
ml,
mr,
mb,
p,
pt,
pl,
pr,
pb,
...props
},
ref,
) => {
const flex_classes = buildFlexClassNames({ direction, wrap, align, justify });
const combined_classes = [flex_classes, className].filter(Boolean).join(' ');

const spacing_style: CSSProperties = {
...(gap !== undefined && { gap }),
...(width !== undefined && { width }),
...(height !== undefined && { height }),
...(m !== undefined && { margin: m }),
...(mt !== undefined && { marginTop: mt }),
...(ml !== undefined && { marginLeft: ml }),
...(mr !== undefined && { marginRight: mr }),
...(mb !== undefined && { marginBottom: mb }),
...(p !== undefined && { padding: p }),
...(pt !== undefined && { paddingTop: pt }),
...(pl !== undefined && { paddingLeft: pl }),
...(pr !== undefined && { paddingRight: pr }),
...(pb !== undefined && { paddingBottom: pb }),
};

return (
<div ref={ref} className={combined_classes} style={spacing_style} {...props}>
{children}
</div>
);
},
);
33 changes: 33 additions & 0 deletions src/components/stack/utils/buildFlexClassNames.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { buildFlexClassNames } from './buildFlexClassNames';

describe('buildFlexClassNames', () => {
it('파라미터가 없는 경우 flex만 반환합니다.', () => {
const classNames = buildFlexClassNames({});
expect(classNames).toEqual('flex');
});

it('flex-direction 파라미터가 있는 경우 flex-direction 클래스를 반환합니다.', () => {
const classNames = buildFlexClassNames({ direction: 'row' });
expect(classNames).toEqual('flex flex-row');
});

it('flex-wrap 파라미터가 있는 경우 flex-wrap 클래스를 반환합니다.', () => {
const classNames = buildFlexClassNames({ wrap: 'wrap' });
expect(classNames).toEqual('flex flex-wrap');
});

it('align-items 파라미터가 있는 경우 align-items 클래스를 반환합니다.', () => {
const classNames = buildFlexClassNames({ align: 'center' });
expect(classNames).toEqual('flex align-center');
});

it('justify-content 파라미터가 있는 경우 justify-content 클래스를 반환합니다.', () => {
const classNames = buildFlexClassNames({ justify: 'center' });
expect(classNames).toEqual('flex justify-center');
});

it('모든 파라미터가 있는 경우 모든 클래스를 반환합니다.', () => {
const classNames = buildFlexClassNames({ direction: 'row', wrap: 'wrap', align: 'center', justify: 'center' });
expect(classNames).toEqual('flex flex-row flex-wrap align-center justify-center');
});
});
Loading