Skip to content

Commit

Permalink
feat(ui): FieldBox 컴포넌트 구현 (#177)
Browse files Browse the repository at this point in the history
* feat: add FieldBox

* feat: publish FieldBox.Label

* feat: publish FieldBox BottomAddon

* refactor: move sub-components to components folder.

* fix: interface name

* feat: Add ErrorMessage Component.

* docs: FieldBox Storybook docs

* cs

* add accessibility arrts.

* fix: remove context.ts

* Enhancing Storybook documentation.

* fix
  • Loading branch information
Brokyeom authored Oct 24, 2024
1 parent 8650489 commit 95401ea
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-ducks-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sopt-makers/ui': minor
---

Add FieldBox Component.
48 changes: 30 additions & 18 deletions apps/docs/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import "./App.css";
import './App.css';

import { ChangeEvent, useState } from "react";
import { ChangeEvent, useState } from 'react';

import SearchField from "../../../packages/ui/Input/SearchField";
import { Test } from "@sopt-makers/ui";
import TextArea from "../../../packages/ui/Input/TextArea";
import TextField from "../../../packages/ui/Input/TextField";
import '@sopt-makers/ui/dist/index.css';

import { FieldBox, SearchField, Test, TextArea, TextField } from '@sopt-makers/ui';
import { colors } from '@sopt-makers/colors';

function App() {
const [input, setInput] = useState("");
const [textarea, setTextarea] = useState("");
const [search, setSearch] = useState("");
const [input, setInput] = useState('');
const [textarea, setTextarea] = useState('');
const [search, setSearch] = useState('');

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value);
Expand Down Expand Up @@ -43,39 +43,51 @@ function App() {
};

const handleSearchReset = () => {
setSearch("");
setSearch('');
};

return (
<>
<Test text="Test Component" size="big" color="blue" />
<Test text='Test Component' size='big' color='blue' />
<TextField<string>
placeholder="Placeholder..."
placeholder='Placeholder...'
required
labelText="Label"
descriptionText="description"
labelText='Label'
descriptionText='description'
validationFn={inputValidation}
value={input}
onChange={handleInputChange}
/>
<TextArea
placeholder="Placeholder..."
placeholder='Placeholder...'
required
topAddon={{ labelText: "Label", descriptionText: "description" }}
topAddon={{ labelText: 'Label', descriptionText: 'description' }}
rightAddon={{ onClick: () => handleTextareaSubmit() }}
validationFn={textareaValidation}
errorMessage="Error Message"
errorMessage='Error Message'
value={textarea}
onChange={handleTextareaChange}
maxLength={300}
/>
<SearchField
placeholder="Placeholder..."
placeholder='Placeholder...'
value={search}
onChange={handleSearchChange}
onSubmit={handleSearchSubmit}
onReset={handleSearchReset}
/>
<div style={{ padding: '20px', backgroundColor: colors.secondary }} />
<FieldBox
topAddon={<FieldBox.Label label='안녕?' description='디스크립션' required />}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<div style={{ color: colors.white }}>레프트애드온</div>}
rightAddon={<div style={{ color: colors.white }}>롸이트애드온</div>}
/>
}
>
<span style={{ color: colors.white }}>여긴 본문</span>
</FieldBox>
</>
);
}
Expand Down
207 changes: 207 additions & 0 deletions apps/docs/src/stories/FieldBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
FieldBox,
FieldBoxProps,
FieldBoxLabelProps,
FieldBoxBottomAddonProps,
FieldBoxLabel,
TextField,
TextArea,
Radio,
CheckBox,
Chip,
Button,
} from '@sopt-makers/ui';
import { Meta, StoryObj } from '@storybook/react';

type FieldBoxStoryProps = FieldBoxProps & FieldBoxLabelProps & FieldBoxBottomAddonProps;

/**
* FieldBox는 입력을 받는 각좀 필드류 컴포넌트(TextField, TextArea, Select등)의 Wrapper역할을 하는 컴포넌트로,<br/>
* 서브 컴포넌트로 `FieldBox.Label`, `FieldBox.BottomAddon`, FieldBox.ErrorMessage`컴포넌트를 제공합니다.<br/>
*
* #### 예시 코드
* ```tsx
* <FieldBox>
* topAddon={<FieldBox.Label label='안녕?' description='디스크립션' required />}
* bottomAddon={
* <FieldBox.BottomAddon
* leftAddon={<div style={{ color: colors.white }}>레프트애드온</div>}
* rightAddon={<div style={{ color: colors.white }}>롸이트애드온</div>}
* />
* }>
* <span style={{ color: colors.white }}>여긴 본문</span>
* </FieldBox>
* ```
*
* #### Sub-Components
*
* |컴포넌트명|설명
* |-------|--------|
* |FieldBox|FieldBox의 Root 컴포넌트.
* |FieldBox.Label|TopAddon 영역에 사용, Label과 description, required prop을 통해 제어가능.
* |FieldBox.BottomAddon|BottomAddon 속성에 사용할 수 있는 Wrapper 컴포넌트, leftAddon, rightAddon prop 제공.
* |FieldBox.ErrorMessage|BottomAddon > leftAddon에 사용하는 것을 권장, 에러메시지를 출력할 수 있는 텍스트 컴포넌트.
*/

const meta: Meta<FieldBoxStoryProps> = {
title: 'Components/FieldBox',
component: FieldBox,
tags: ['autodocs'],
argTypes: {
label: { controls: 'string', description: `<FieldBox.Label /> 컴포넌트의 label 영역을 설정합니다.` },
description: { controls: 'string', description: `<FieldBox.Label /> 컴포넌트의 description 영역을 설정합니다.` },
required: { controls: 'boolean', description: `<FieldBox.Label /> 컴포넌트의 required 속성을 설정합니다.` },
style: { control: false },
},
args: { style: { width: '100%', minWidth: '335px' }, label: 'Label', description: 'Description', required: false },
};

export default meta;

export const WithTextField: StoryObj<FieldBoxStoryProps> = {
render: (args) => {
return (
<FieldBox
topAddon={
<FieldBoxLabel label={args.label} description={args.description} required={args.required}></FieldBoxLabel>
}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<FieldBox.ErrorMessage message='ErrorMessage' />}
rightAddon={
<Button theme='blue' size='sm'>
Button
</Button>
}
/>
}
{...args}
>
<TextField value='value' />
</FieldBox>
);
},
};

export const WithTextArea: StoryObj<FieldBoxStoryProps> = {
render: (args) => {
return (
<FieldBox
topAddon={
<FieldBoxLabel label={args.label} description={args.description} required={args.required}></FieldBoxLabel>
}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<FieldBox.ErrorMessage message='ErrorMessage' />}
rightAddon={
<Button theme='blue' size='sm'>
Button
</Button>
}
/>
}
{...args}
>
<TextArea value='value' />
</FieldBox>
);
},
};

export const WithRadio: StoryObj<FieldBoxStoryProps> = {
args: { label: '파트', description: '파트를 선택해주세요.', required: true },
render: (args) => {
return (
<FieldBox
topAddon={
<FieldBoxLabel label={args.label} description={args.description} required={args.required}></FieldBoxLabel>
}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<FieldBox.ErrorMessage message='ErrorMessage' />}
rightAddon={
<Button theme='blue' size='sm'>
입력 완료
</Button>
}
/>
}
{...args}
>
<div style={{ display: 'flex', gap: '10px' }}>
<Radio label='기획' size='lg' checked />
<Radio label='디자인' size='lg' />
<Radio label='안드로이드' size='lg' />
<Radio label='iOS' size='lg' />
<Radio label='웹' size='lg' />
<Radio label='서버' size='lg' />
</div>
</FieldBox>
);
},
};

export const WithCheckBox: StoryObj<FieldBoxStoryProps> = {
render: (args) => {
return (
<FieldBox
topAddon={
<FieldBoxLabel label={args.label} description={args.description} required={args.required}></FieldBoxLabel>
}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<FieldBox.ErrorMessage message='ErrorMessage' />}
rightAddon={
<Button theme='blue' size='sm'>
입력 완료
</Button>
}
/>
}
{...args}
>
<div style={{ display: 'flex', gap: '10px' }}>
<CheckBox label='CheckBox1' size='lg' checked />
<CheckBox label='CheckBox2' size='lg' />
<CheckBox label='CheckBox3' size='lg' />
</div>
</FieldBox>
);
},
};

export const WithChip: StoryObj<FieldBoxStoryProps> = {
args: {
label: '경력',
description: '정규직으로 근무한 경력을 기준으로 선택해주세요.',
required: true,
},
render: (args) => {
return (
<FieldBox
topAddon={
<FieldBoxLabel label={args.label} description={args.description} required={args.required}></FieldBoxLabel>
}
bottomAddon={
<FieldBox.BottomAddon
leftAddon={<FieldBox.ErrorMessage message='경력을 선택해주세요' />}
rightAddon={
<Button theme='blue' size='sm' disabled>
선택 완료
</Button>
}
/>
}
{...args}
>
<div style={{ display: 'flex', gap: '10px' }}>
<Chip size='sm'>아직 없어요</Chip>
<Chip size='sm'>인턴 경험만 있어요</Chip>
<Chip size='sm'>주니어(0~3년)</Chip>
<Chip size='sm'>미들(4~8년)</Chip>
<Chip size='sm'>시니어(9년 이상)</Chip>
</div>
</FieldBox>
);
},
};
28 changes: 28 additions & 0 deletions packages/ui/FieldBox/FieldBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { forwardRef } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import { BottomAddon, FieldBoxErrorMessage, FieldBoxLabel } from './components';

export interface FieldBoxProps extends HTMLAttributes<HTMLDivElement> {
topAddon?: ReactNode;
bottomAddon?: ReactNode;
}

const FieldBoxImpl = forwardRef<HTMLDivElement, FieldBoxProps>((props, forwardedRef) => {
const { topAddon, bottomAddon, children, ...restProps } = props;

return (
<div ref={forwardedRef} {...restProps}>
{topAddon}
<div>{children}</div>
<div>{bottomAddon}</div>
</div>
);
});

FieldBoxImpl.displayName = 'FieldBoxImpl';

export const FieldBox = Object.assign(FieldBoxImpl, {
Label: FieldBoxLabel,
BottomAddon,
ErrorMessage: FieldBoxErrorMessage,
});
21 changes: 21 additions & 0 deletions packages/ui/FieldBox/components/BottomAddon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react';
import { bottomAddonContainerStyle } from '../style.css';

export interface FieldBoxBottomAddonProps extends HTMLAttributes<HTMLDivElement> {
leftAddon?: ReactNode;
rightAddon?: ReactNode;
}

export const BottomAddon = forwardRef<HTMLDivElement, FieldBoxBottomAddonProps>((props, forwardedRef) => {
const { leftAddon, rightAddon } = props;

return (
<div className={bottomAddonContainerStyle} ref={forwardedRef}>
{leftAddon}
{rightAddon}
</div>
);
});

BottomAddon.displayName = 'BottomAddon';
20 changes: 20 additions & 0 deletions packages/ui/FieldBox/components/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import AlertCircleIcon from 'Input/icons/AlertCircleIcon';
import { forwardRef, type HTMLAttributes } from 'react';
import { errorMessage } from '../style.css';

export interface FieldBoxErrorMessageProps extends HTMLAttributes<HTMLDivElement> {
message: string;
}

export const FieldBoxErrorMessage = forwardRef<HTMLDivElement, FieldBoxErrorMessageProps>((props, forwardedRef) => {
const { message } = props;

return (
<div className={errorMessage} ref={forwardedRef}>
<AlertCircleIcon />
<p>{message}</p>
</div>
);
});

FieldBoxErrorMessage.displayName = 'FieldBoxErrorMessage';
Loading

0 comments on commit 95401ea

Please sign in to comment.