From 6c6e479c8b61673ea58a9d73d1b49656d6a5ab55 Mon Sep 17 00:00:00 2001
From: Chaeyeon Seo <94633589+seocylucky@users.noreply.github.com>
Date: Fri, 30 Aug 2024 13:12:44 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20Textarea=20=EC=BB=B4=ED=8F=AC=EB=84=8C?=
=?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#150)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: Textarea 컴포넌트 구현 (#146)
* Revert "feat: Textarea 컴포넌트 구현 (#146)"
This reverts commit afb1018ba013b66bce471d1071534e2241c38bc7.
* feat: Textarea 컴포넌트 구현 (#146)
* fix: 사라져버린 Pagination, useRadioGroup export 살리기
* refactor: currentLength 코드 삭제 -> value.length로 처리
* refactor: Textarea 타입 중복 정의 제거
(plaholder, disabled, maxLength)
* refactor: StyledContainer에서 사용 안하는 StyledTextareaProps 삭제
* refactor: e.target.value -> 선언한 newValue로 변경
* style: textStatusNegative -> lineStatusPositive
* style: border 관련 스타일 line 스타일로 적용
* style: error 상황일 때와 focus 상황일 때 border 코드 수정
* feat: value prop 삭제, width height 타입 변경
* feat: 스크롤바 커스텀 작업
* docs: onValueChange prop 관련 문서 작성
* feat: helperText 로직 수정
* style: 스크롤바와 텍스트 여백 padding: 6px 추가
---
src/components/Textarea/Textarea.mdx | 146 +++++++++++++++++++
src/components/Textarea/Textarea.stories.tsx | 146 +++++++++++++++++++
src/components/Textarea/Textarea.style.ts | 86 +++++++++++
src/components/Textarea/Textarea.tsx | 66 +++++++++
src/components/Textarea/Textarea.type.ts | 7 +
src/components/Textarea/index.ts | 2 +
src/components/index.ts | 6 +-
7 files changed, 457 insertions(+), 2 deletions(-)
create mode 100644 src/components/Textarea/Textarea.mdx
create mode 100644 src/components/Textarea/Textarea.stories.tsx
create mode 100644 src/components/Textarea/Textarea.style.ts
create mode 100644 src/components/Textarea/Textarea.tsx
create mode 100644 src/components/Textarea/Textarea.type.ts
create mode 100644 src/components/Textarea/index.ts
diff --git a/src/components/Textarea/Textarea.mdx b/src/components/Textarea/Textarea.mdx
new file mode 100644
index 0000000..a3c16ae
--- /dev/null
+++ b/src/components/Textarea/Textarea.mdx
@@ -0,0 +1,146 @@
+import { Canvas, Meta, Controls } from '@storybook/blocks';
+import * as TextareaStories from './Textarea.stories';
+import { Textarea } from './Textarea';
+import React from 'react';
+
+
+
+# Textarea
+
+사용자가 텍스트를 입력하는 필드로, 여러 줄의 텍스트 입력이 필요한 경우 사용됩니다. 다양한 상태와 속성을 지원하여 사용자 경험을 향상시킬 수 있습니다.
+
+
+
+
+
+
+
+## 사용법
+
+Textarea의 기본 사용법입니다.
+
+필수 프로퍼티인 `width`와 `height`를 사용하여 Textarea의 크기를 설정해주세요.
+
+```tsx
+import { Textarea } from '@yourssu/design-system-react';
+```
+
+```tsx
+
+```
+
+이외의 프로퍼티들은 하단의 예시에서 확인할 수 있습니다.
+
+
+
+
+## 예시
+
+### placeholder
+
+Textarea에 표시되는 짧은 안내 문구로, 사용자가 입력해야 할 내용의 예시를 보여줍니다.
+
+```tsx
+
+```
+
+
+
+
+
+
+### helperText
+
+Textarea에 사용자가 올바르게 입력할 수 있도록 돕는 텍스트입니다.
+
+```tsx
+
+```
+
+
+
+
+
+
+### maxLength
+
+Textarea에 사용자가 입력할 수 있는 최대 글자 수를 제한합니다.
+
+```tsx
+
+```
+
+
+
+
+
+
+### disabled
+
+Textarea에 어떠한 입력을 할 수 없도록 막습니다.
+
+```tsx
+
+```
+
+
+
+
+
+
+### error
+
+Textarea에 잘못된 값이 입력되었을 때 사용자에게 오류 상태를 표시하는 데 사용됩니다.
+
+```tsx
+
+```
+
+
+
+
+
+
+### onValueChange
+
+Textarea에 텍스트를 입력하거나 변경할 때 호출되는 콜백 함수입니다. 이 함수는 현재 입력된 텍스트 값을 매개변수로 전달하여, 외부에서 상태를 관리하거나 추가적인 로직을 실행할 수 있게 합니다.
+
+```tsx
+const [text, setText] = useState('');
+
+const handleValueChange = (newValue: string) => {
+ setText(newValue);
+};
+
+return (
+
+);
+```
+
+
+
+
+
diff --git a/src/components/Textarea/Textarea.stories.tsx b/src/components/Textarea/Textarea.stories.tsx
new file mode 100644
index 0000000..ba7c527
--- /dev/null
+++ b/src/components/Textarea/Textarea.stories.tsx
@@ -0,0 +1,146 @@
+import { useState } from 'react';
+
+import { Meta, StoryObj } from '@storybook/react';
+
+import { Textarea } from './Textarea';
+import { TextareaProps } from './Textarea.type';
+
+const meta: Meta = {
+ 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;
+
+const ControlledComponent = (args: TextareaProps) => {
+ return ;
+};
+
+const OnChangeComponent = (args: TextareaProps) => {
+ const [text, setText] = useState('');
+
+ const handleValueChange = (newValue: string) => {
+ setText(newValue);
+ };
+
+ return (
+
+ );
+};
+
+export const Default: Story = {
+ render: (args) => ,
+ args: {
+ width: '100%',
+ height: 'auto',
+ placeholder: 'Enter text here...',
+ helperText: 'Text Inputting',
+ },
+};
+
+export const Placeholder: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ placeholder: 'Enter text here...',
+ },
+};
+
+export const HelperText: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ placeholder: 'Enter text here...',
+ helperText: 'helperText입니다.',
+ },
+};
+
+export const Disabled: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ placeholder: 'This field is disabled',
+ helperText: 'Text Inputting',
+ disabled: true,
+ },
+};
+
+export const Error: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ error: true,
+ placeholder: 'There is an error',
+ helperText: 'Text Inputting',
+ },
+};
+
+export const MaxLength: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ maxLength: 50,
+ placeholder: 'Max 50 characters',
+ helperText: '',
+ disabled: false,
+ error: false,
+ },
+};
+
+export const OnValueChange: Story = {
+ render: (args) => ,
+ args: {
+ width: '343px',
+ height: '187px',
+ maxLength: 50,
+ placeholder: 'onValueChange...',
+ helperText: '',
+ disabled: false,
+ error: false,
+ },
+};
diff --git a/src/components/Textarea/Textarea.style.ts b/src/components/Textarea/Textarea.style.ts
new file mode 100644
index 0000000..90dcf3e
--- /dev/null
+++ b/src/components/Textarea/Textarea.style.ts
@@ -0,0 +1,86 @@
+import { styled } from 'styled-components';
+
+import { TextareaProps } from './Textarea.type';
+
+interface StyledTextareaProps {
+ $width?: TextareaProps['width'];
+ $height?: TextareaProps['height'];
+ $error?: TextareaProps['error'];
+ $isFocused?: boolean;
+}
+
+export const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`;
+
+export const StyledTextareaWrapper = styled.div`
+ width: ${({ $width }) => $width ?? '100%'};
+ height: ${({ $height }) => $height ?? 'auto'};
+ padding: 16px;
+ background-color: ${({ theme }) => theme.semantic.color.bgBasicLight};
+ border: ${({ theme, $error }) =>
+ $error ? `1px solid ${theme.semantic.color.lineStatusNegative}` : 'none'};
+ border-radius: ${({ theme }) => theme.semantic.radius.s}px;
+ box-sizing: border-box;
+ border: ${({ theme, $error, $isFocused }) =>
+ $error
+ ? `1px solid ${theme.semantic.color.lineStatusNegative}`
+ : $isFocused
+ ? `1px solid ${theme.semantic.color.lineStatusPositive}`
+ : 'none'};
+`;
+
+export const StyledTextarea = styled.textarea`
+ width: 100%;
+ height: 100%;
+ padding-right: 6px;
+ resize: none;
+ ${({ theme }) => theme.typo.B3_Rg_14}
+
+ box-sizing: border-box;
+ border: none;
+ background-color: transparent;
+ color: ${({ theme }) => theme.semantic.color.textBasicPrimary};
+
+ caret-color: ${({ theme, $error }) =>
+ $error ? theme.semantic.color.lineStatusNegative : theme.semantic.color.lineStatusPositive};
+
+ &:focus {
+ outline: none;
+ }
+
+ &::placeholder {
+ color: ${({ theme }) => theme.semantic.color.textBasicTertiary};
+ }
+
+ &:disabled {
+ background-color: transparent;
+ cursor: not-allowed;
+
+ &::placeholder {
+ color: ${({ theme }) => theme.semantic.color.textBasicDisabled};
+ }
+ }
+
+ &::-webkit-scrollbar {
+ width: 2px;
+ border-radius: 2px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: ${({ theme }) => theme.semantic.color.lineBasicMedium};
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: ${({ theme }) => theme.semantic.color.lineBasicStrong};
+ }
+`;
+
+export const StyledHelperText = styled.div`
+ margin-left: 4px;
+ ${({ theme }) => theme.typo.C2_Rg_12}
+ color: ${({ theme, $error }) =>
+ $error ? theme.semantic.color.textStatusNegative : theme.semantic.color.textBasicTertiary};
+`;
diff --git a/src/components/Textarea/Textarea.tsx b/src/components/Textarea/Textarea.tsx
new file mode 100644
index 0000000..cb33d29
--- /dev/null
+++ b/src/components/Textarea/Textarea.tsx
@@ -0,0 +1,66 @@
+import { forwardRef, useState } from 'react';
+
+import {
+ StyledTextarea,
+ StyledContainer,
+ StyledHelperText,
+ StyledTextareaWrapper,
+} from './Textarea.style';
+import { TextareaProps } from './Textarea.type';
+
+export const Textarea = forwardRef(
+ (
+ { width, height, maxLength, helperText, placeholder, disabled, error, onValueChange, ...props },
+ ref
+ ) => {
+ const [value, setValue] = useState('');
+
+ const [isFocused, setIsFocused] = useState(false);
+
+ const handleFocus = () => {
+ setIsFocused(true);
+ };
+
+ const handleBlur = () => {
+ setIsFocused(false);
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ if (maxLength && newValue.length > maxLength) {
+ return;
+ }
+ setValue(newValue);
+ onValueChange?.(newValue);
+ };
+
+ return (
+
+
+
+
+
+ {maxLength ? `(${value.length}/${maxLength})` : helperText && helperText}
+
+
+ );
+ }
+);
+
+Textarea.displayName = 'Textarea';
diff --git a/src/components/Textarea/Textarea.type.ts b/src/components/Textarea/Textarea.type.ts
new file mode 100644
index 0000000..f27b5fc
--- /dev/null
+++ b/src/components/Textarea/Textarea.type.ts
@@ -0,0 +1,7 @@
+export interface TextareaProps extends React.TextareaHTMLAttributes {
+ width: React.CSSProperties['width'];
+ height: React.CSSProperties['height'];
+ helperText?: string;
+ error?: boolean;
+ onValueChange?: (value: string) => void;
+}
diff --git a/src/components/Textarea/index.ts b/src/components/Textarea/index.ts
new file mode 100644
index 0000000..72dced2
--- /dev/null
+++ b/src/components/Textarea/index.ts
@@ -0,0 +1,2 @@
+export { Textarea } from './Textarea';
+export type { TextareaProps } from './Textarea.type';
diff --git a/src/components/index.ts b/src/components/index.ts
index f0556f5..85d88b6 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -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';
@@ -42,4 +41,7 @@ export type {
RadioGroupSizeType,
RadioGroupOrientationType,
RadioGroupValueChangeEvent,
-} from './RadioGroup';
\ No newline at end of file
+} from './RadioGroup';
+
+export { Textarea } from './Textarea';
+export type { TextareaProps } from './Textarea';