Skip to content
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

feat(ui): FieldBox 컴포넌트 구현 #177

Merged
merged 14 commits into from
Oct 24, 2024
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';

Copy link
Member

@suwonthugger suwonthugger Oct 23, 2024

Choose a reason for hiding this comment

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

이렇게 css 파일 불러오지 않으면 왜 스타일이 적용되지 않는지 궁금한데 혹시 알고계신분 계실까요 ,,, ? 빌드된 파일에서 불러오는거면 스타일이 적용되어있어야하는게 아닌가 싶어서유

Copy link
Member

Choose a reason for hiding this comment

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

빌드된 파일에서 불러오는거면 스타일이 적용되어있어야하는게 아닌가 싶어서유

요게 무슨 말씀인지 궁금하네요!!!

Copy link
Member Author

@Brokyeom Brokyeom Oct 23, 2024

Choose a reason for hiding this comment

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

요거 빌드 파일에서 컴포넌트 파일과 css가 따로 빌드 되면서, 사용할때는 css 파일을 import 해야하기 때문인데, 아무래도 이렇게 import 하는 방식이 저도 불편하지 않나? 라는 의문이 계속 들기는 합니다.

다른 오픈소스들을 보면 (Chakra, Radix Theme) 들을 보면 Provider를 루트에 감싸서 css를 제공하는 것 같은데, css파일을 직접 import 하지 않고도 css를 효과적으로 적용할 수 있는 방법이 있다면 적용하는게 좋을 것 같습니다.

Copy link
Member

@suwonthugger suwonthugger Oct 24, 2024

Choose a reason for hiding this comment

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

빌드된 파일에서 불러오는거면 스타일이 적용되어있어야하는게 아닌가 싶어서유

요게 무슨 말씀인지 궁금하네요!!!

형겸이형 댓글 읽고 생각해보니가 빌드되면 css가 분리되네요 ㅋㅋ 흠 잘못된 말입니다

Copy link
Member

Choose a reason for hiding this comment

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

요거 빌드 파일에서 컴포넌트 파일과 css가 따로 빌드 되면서, 사용할때는 css 파일을 import 해야하기 때문인데, 아무래도 이렇게 import 하는 방식이 저도 불편하지 않나? 라는 의문이 계속 들기는 합니다.

다른 오픈소스들을 보면 (Chakra, Radix Theme) 들을 보면 Provider를 루트에 감싸서 css를 제공하는 것 같은데, css파일을 직접 import 하지 않고도 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>
</>
Comment on lines +79 to +90
Copy link
Member Author

Choose a reason for hiding this comment

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

예제 코드 추가했습니다. 해당 부분 봐 주시면 됩니다!

);
}
Expand Down
191 changes: 191 additions & 0 deletions apps/docs/src/stories/FieldBox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { colors } from '@sopt-makers/colors';
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>
* ```
*/

const meta: Meta<FieldBoxStoryProps> = {
title: 'Components/FieldBox',
component: FieldBox,
tags: ['autodocs'],
argTypes: {
label: { control: 'string' },
description: { control: 'string' },
required: { control: 'boolean' },
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={<div style={{ color: colors.white }}>레프트애드온</div>}
rightAddon={<div style={{ color: colors.white }}>롸이트애드온</div>}
/>
}
{...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={<div style={{ color: colors.white }}>레프트애드온</div>}
rightAddon={<div style={{ color: colors.white }}>롸이트애드온</div>}
/>
}
{...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={<div style={{ color: colors.white }}>레프트애드온</div>}
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={<div style={{ color: colors.white }}>레프트애드온</div>}
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';
27 changes: 27 additions & 0 deletions packages/ui/FieldBox/components/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { HTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { requiredMarkStyle, TopAddonDescriptionStyle, TopAddonLabelStyle } from '../style.css';

export interface FieldBoxLabelProps extends HTMLAttributes<HTMLDivElement> {
label: string;
description: string;
required: boolean;
}

export const FieldBoxLabel = forwardRef<HTMLDivElement, FieldBoxLabelProps>((props, forwardedRef) => {
const { required, label, description } = props;

return (
<div aria-label={label} aria-required={required} ref={forwardedRef}>
<label className={TopAddonLabelStyle}>
<span>
{label}
{required ? <span className={requiredMarkStyle}>*</span> : null}
</span>
<p className={TopAddonDescriptionStyle}>{description}</p>
</label>
</div>
);
});

FieldBoxLabel.displayName = 'FieldBoxLabel';
3 changes: 3 additions & 0 deletions packages/ui/FieldBox/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Label';
export * from './BottomAddon';
export * from './ErrorMessage';
Empty file added packages/ui/FieldBox/context.ts
Copy link
Member

Choose a reason for hiding this comment

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

혹시 이 파일 사용하지 않는다면 삭제해도 될 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

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

bea327c에서 수정했습니다.
처음에 컨텍스트 사용하려고 파일 만들어두고 사용은 안했네요 ㅎ 일단 삭제했습니다.

Empty file.
2 changes: 2 additions & 0 deletions packages/ui/FieldBox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './FieldBox';
export * from './components';
Loading
Loading