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: Textarea 컴포넌트 구현 #150

Merged
merged 18 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
43ef88c
Merge branch 'develop' into feat/#146-textarea
seocylucky Aug 10, 2024
afb1018
feat: Textarea 컴포넌트 구현 (#146)
seocylucky Aug 21, 2024
39342f1
Revert "feat: Textarea 컴포넌트 구현 (#146)"
seocylucky Aug 21, 2024
1896852
Merge branch 'develop' into feat/#146-textarea
seocylucky Aug 21, 2024
9346d0c
feat: Textarea 컴포넌트 구현 (#146)
seocylucky Aug 21, 2024
360a374
fix: 사라져버린 Pagination, useRadioGroup export 살리기
seocylucky Aug 21, 2024
3a106d8
refactor: currentLength 코드 삭제 -> value.length로 처리
seocylucky Aug 21, 2024
b4ade14
refactor: Textarea 타입 중복 정의 제거
seocylucky Aug 21, 2024
68640e1
refactor: StyledContainer에서 사용 안하는 StyledTextareaProps 삭제
seocylucky Aug 21, 2024
31b354b
refactor: e.target.value -> 선언한 newValue로 변경
seocylucky Aug 21, 2024
edf0eca
style: textStatusNegative -> lineStatusPositive
seocylucky Aug 22, 2024
e16a681
style: border 관련 스타일 line 스타일로 적용
seocylucky Aug 23, 2024
d3cc9ff
style: error 상황일 때와 focus 상황일 때 border 코드 수정
seocylucky Aug 23, 2024
ae0e28e
feat: value prop 삭제, width height 타입 변경
seocylucky Aug 28, 2024
c92abeb
feat: 스크롤바 커스텀 작업
seocylucky Aug 28, 2024
f878050
docs: onValueChange prop 관련 문서 작성
seocylucky Aug 28, 2024
8c9b2ad
feat: helperText 로직 수정
seocylucky Aug 28, 2024
c0a8521
style: 스크롤바와 텍스트 여백 padding: 6px 추가
seocylucky Aug 30, 2024
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
119 changes: 119 additions & 0 deletions src/components/Textarea/Textarea.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Canvas, Meta, Controls } from '@storybook/blocks';
import * as TextareaStories from './Textarea.stories';
import { Textarea } from './Textarea';
import React from 'react';

<Meta of={TextareaStories} />

# Textarea

사용자가 텍스트를 입력하는 필드로, 여러 줄의 텍스트 입력이 필요한 경우 사용됩니다. 다양한 상태와 속성을 지원하여 사용자 경험을 향상시킬 수 있습니다.
seocylucky marked this conversation as resolved.
Show resolved Hide resolved

<Canvas of={TextareaStories.Default} />
<Controls />

<br />
<br />

## 사용법

Textarea의 기본 사용법입니다.

필수 프로퍼티인 `width`와 `height`를 사용하여 Textarea의 크기를 설정해주세요.

```tsx
import { Textarea } from '@yourssu/design-system-react';
```

```tsx
<Textarea width="343px" height="187px" />
```

이외의 프로퍼티들은 하단의 예시에서 확인할 수 있습니다.

<br />
<br />

## 예시

### placeholder

Textarea에 표시되는 짧은 안내 문구로, 사용자가 입력해야 할 내용의 예시를 보여줍니다.

```tsx
<Textarea width="343px" height="187px" placeholder="Enter text here..." />
```

<Canvas of={TextareaStories.Placeholder} withSource="none" />

<br />
<br />

### helperText

Textarea에 사용자가 올바르게 입력할 수 있도록 돕는 텍스트입니다.

```tsx
<Textarea
width="343px"
height="187px"
helperText="helperText입니다."
placeholder="Enter text here..."
/>
```

<Canvas of={TextareaStories.HelperText} withSource="none" />

<br />
<br />

### maxLength

Textarea에 사용자가 입력할 수 있는 최대 글자 수를 제한합니다.

```tsx
<Textarea width="343px" height="187px" maxLength={50} placeholder="Max 50 characters" />
```

<Canvas of={TextareaStories.MaxLength} withSource="none" />

<br />
<br />

### disabled

Textarea에 어떠한 입력을 할 수 없도록 막습니다.

```tsx
<Textarea
width="343px"
height="187px"
disabled={true}
helperText="Text Inputting"
placeholder="This field is disabled"
/>
```

<Canvas of={TextareaStories.Disabled} withSource="none" />

<br />
<br />

### error

Textarea에 잘못된 값이 입력되었을 때 사용자에게 오류 상태를 표시하는 데 사용됩니다.

```tsx
<Textarea
width="343px"
height="187px"
error={true}
helperText="Text Inputting"
placeholder="There is an error"
/>
```

<Canvas of={TextareaStories.Error} withSource="none" />

<br />
<br />
119 changes: 119 additions & 0 deletions src/components/Textarea/Textarea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// import { useState } from 'react';

import { Meta, StoryObj } from '@storybook/react';

import { Textarea } from './Textarea';
import { TextareaProps } from './Textarea.type';

const meta: Meta<typeof Textarea> = {
title: 'Components/Textarea',
component: Textarea,
argTypes: {
width: {
control: 'text',
description: 'Textarea의 가로 길이',
},
height: {
control: 'text',
description: 'Textarea의 세로 길이',
},
placeholder: {
control: 'text',
description: 'Textarea의 placeholder 텍스트',
},
maxLength: {
control: 'number',
description: 'Textarea의 최대 입력 가능 글자 수',
},
helperText: {
control: 'text',
description: 'Textarea에 올바르게 입력할 수 있도록 돕는 텍스트',
},
disabled: {
control: 'boolean',
description: 'Textarea의 비활성화 여부',
},
error: {
control: 'boolean',
description: 'Textarea의 에러 여부',
},
},
parameters: {
layout: 'centered',
},
};

export default meta;
type Story = StoryObj<typeof Textarea>;

export const Default: Story = {
args: {
width: '343px',
height: '187px',
placeholder: 'Enter text here...',
helperText: 'Text Inputting',
},
};

export const Placeholder: Story = {
args: {
width: '343px',
height: '187px',
placeholder: 'Enter text here...',
},
};

export const HelperText: Story = {
args: {
width: '343px',
height: '187px',
placeholder: 'Enter text here...',
helperText: 'helperText입니다.',
},
};

export const Disabled: Story = {
args: {
width: '343px',
height: '187px',
placeholder: 'This field is disabled',
helperText: 'Text Inputting',
disabled: true,
},
};

export const Error: Story = {
args: {
width: '343px',
height: '187px',
error: true,
placeholder: 'There is an error',
helperText: 'Text Inputting',
},
};

const MaxLengthTest = ({
width = '343px',
height = '187px',
maxLength = 50,
placeholder = 'Max 50 characters',
helperText = '',
disabled = false,
error = false,
}: TextareaProps) => {
return (
<Textarea
width={width}
height={height}
maxLength={maxLength}
placeholder={placeholder}
helperText={helperText}
disabled={disabled}
error={error}
/>
);
};

export const MaxLength: Story = {
render: MaxLengthTest,
};
56 changes: 56 additions & 0 deletions src/components/Textarea/Textarea.style.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

스크롤이 생겼을 때 디자인이 아직 적용 전인거 같네요!

스토리북 피그마
image image

Copy link
Member Author

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.

@nijuy 스크롤바 적용 완료하였습니다! 이게 스크롤바 적용이 패딩값을 무시하고 textarea 전체에 맞게 스크롤바가 생겨서 StyledTextareaWrapper로 감싸서 패딩값 적용된 컨테이너 안에 스크롤바가 생긴 형태(피그마와 같은 형태)로 작업하였습니다!

윈도우 환경 확인 부탁드려욥!

Copy link
Collaborator

Choose a reason for hiding this comment

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

잘 보입니다!
image

다만 첫번째 사진은 단어 길이 때문에 줄바꿈이 생겨서 스크롤과 단어 사이 여백이 충분한데,
그렇지 않은 경우에 글자랑 스크롤바 사이가 너무 딱 붙어있어서..🥲

디자인 자체가 1로 되어있어서 줄바꿈이 안 일어나는 경우 글자와 스크롤 사이 여백이 고려됐는지 잘 모르겠네요!
한번 물어보는 건 어떨까요??
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

6px이라네용 굿.........
바로 머지할 수 있게 미리 어푸룹 해둘게요 이거 말곤 진짜진짜 안보임!!!!

image

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { styled } from 'styled-components';

import { TextareaProps } from './Textarea.type';

interface StyledTextareaProps {
$width?: TextareaProps['width'];
$height?: TextareaProps['height'];
$error?: TextareaProps['error'];
}

export const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;

export const StyledTextarea = styled.textarea<StyledTextareaProps>`
width: ${({ $width }) => $width};
height: ${({ $height }) => $height};
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
padding: 16px;
${({ theme }) => theme.typo.B3_Rg_14}
resize: none;

box-sizing: border-box;
border: ${({ theme, $error }) =>
$error ? `1px solid ${theme.semantic.color.textStatusNegative}` : 'none'};
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
border-radius: ${({ theme }) => theme.semantic.radius.m}px;
background-color: ${({ theme }) => theme.semantic.color.bgBasicLight};
color: ${({ theme }) => theme.semantic.color.textBasicPrimary};

caret-color: ${({ theme, $error }) =>
$error ? theme.semantic.color.textStatusNegative : theme.semantic.color.lineStatusPositive};

outline-color: ${({ theme }) => theme.semantic.color.lineStatusPositive};
outline-width: 1px;
seocylucky marked this conversation as resolved.
Show resolved Hide resolved

&::placeholder {
color: ${({ theme }) => theme.semantic.color.textBasicTertiary};
}

&:disabled {
background-color: ${({ theme }) => theme.semantic.color.bgBasicLight};
cursor: not-allowed;

&::placeholder {
color: ${({ theme }) => theme.semantic.color.textBasicDisabled};
}
}
`;

export const StyledHelperText = styled.div<StyledTextareaProps>`
margin-left: 4px;
${({ theme }) => theme.typo.C2_Rg_12}
color: ${({ theme, $error }) =>
$error ? theme.semantic.color.textStatusNegative : theme.semantic.color.textBasicTertiary};
`;
44 changes: 44 additions & 0 deletions src/components/Textarea/Textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { forwardRef, useState } from 'react';

import { StyledTextarea, StyledContainer, StyledHelperText } from './Textarea.style';
import { TextareaProps } from './Textarea.type';

export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
(
{ width, height, maxLength, helperText, placeholder, disabled, error, onValueChange, ...props },
ref
) => {
const [value, setValue] = useState('');

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
if (maxLength && newValue.length > maxLength) {
return;
}
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
setValue(newValue);
onValueChange?.(newValue);
};

return (
<StyledContainer>
<StyledTextarea
ref={ref}
$width={width}
$height={height}
value={value}
onChange={handleChange}
maxLength={maxLength}
placeholder={placeholder}
disabled={disabled}
$error={error}
{...props}
/>
<StyledHelperText $error={error}>
{helperText && helperText} {maxLength && `(${value.length}/${maxLength})`}
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
</StyledHelperText>
</StyledContainer>
);
}
);

Textarea.displayName = 'Textarea';
8 changes: 8 additions & 0 deletions src/components/Textarea/Textarea.type.ts
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
width: string;
height: string;
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
helperText?: string;
error?: boolean;
value?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

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

value의 역할이 궁금합니다!
외부에서 value prop을 넘겨주면 사용자가 값을 변경해도 prop으로 넘긴 value로 값이 고정되어 버리니까 입력값이 화면에 반영이 안돼요 (˘・_・˘)

value

Copy link
Member Author

@seocylucky seocylucky Aug 22, 2024

Choose a reason for hiding this comment

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

음 사실 value를 prop으로 정한 이유가 사용자가 value를 자신이 정한 value 값을 prop으로 넣어서 사용할 수 있도록 하려는 의도 였습니다!!

생각해보니 내부에서 제어(usestate)를 하게 되니 적용이 되지 않겠네요..

음 value와 onChage를 외부에서 prop으로 제어하도록 하는게 맞는 방법인지, 내부에서 상태 관리를 하는게 맞는 방법인지 고민고민,,, 입니당....

Copy link
Collaborator

Choose a reason for hiding this comment

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

생각해보니 내부에서 제어(usestate)를 하게 되니 적용이 되지 않겠네요..

사실 제가 입력값이 화면에 반영이 안돼요 라고 표현했던 경우는 사용처에서 잘못 사용한 거긴 해요.
(onValueChange를 넘기지 않고 value만 설정했음)

image

value: A string. Controls the text inside the text area.

When you pass value, you must also pass an onChange handler () that updates the passed value. - https://react.dev/reference/react-dom/components/textarea

value prop을 전달할 거면 onValueChange도 같이 주거나 readOnly로 설정하는 게 올바른 사용이고,
그렇게 하지 않았을 때 리액트에서 사진과 같은 콘솔 경고를 띄워주는데

image

지금 Textarea에는 onChange={handleChange}가 있으니까 value만 넘겨도 경고가 안 뜨는 점이 좀 걸렸어요 저는!
(어쨌든 onValueChange가 없어서 문제가 발생해도 경고가 뜨지 않으니 잘못된 사용을 단번에 알아차리기 어렵다고 생각)

지금처럼 value prop을 전달할 수 있게 갈 거라면 적절한 예외 처리 + 문서 수정이 필요하다고 생각합니다 @.@

Copy link
Contributor

Choose a reason for hiding this comment

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

사용하는 입장에서 가장 편한 방식은

  • 내부에서 상태 관리
  • 외부(사용하는곳)에서는 onValueChange 이벤트를 사용해서 내부 value를 받아올 수 있도록

하는거라고 생각해요

이 방법을 사용하는 가장 대표적인 라이브러리가 react-hook-form 이긴하져

Copy link
Member Author

Choose a reason for hiding this comment

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

value prop 삭제하는 방안 으로 갔습니다!

  • 내부에서 상태 관리
  • 외부(사용하는곳)에서는 onValueChange 이벤트를 사용해서 내부 value를 받아올 수 있도록

이 방식으로 수정했습니다 확인 부탁드려욥!

onValueChange?: (value: string) => void;
}
2 changes: 2 additions & 0 deletions src/components/Textarea/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Textarea } from './Textarea';
export type { TextareaProps } from './Textarea.type';
6 changes: 4 additions & 2 deletions src/components/index.ts
seocylucky marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type { TabsProps, TabListProps, TabProps, TabPanelProps } from './Tabs';
export { Fab } from './Fab';
export type { FabHierarchy, FabProps, FabSize } from './Fab';


export { Pagination } from './Pagination';
export type { PaginationProps } from './Pagination';

Expand All @@ -42,4 +41,7 @@ export type {
RadioGroupSizeType,
RadioGroupOrientationType,
RadioGroupValueChangeEvent,
} from './RadioGroup';
} from './RadioGroup';

export { Textarea } from './Textarea';
export type { TextareaProps } from './Textarea';