Skip to content

Commit 362963f

Browse files
authored
[#43] ✨ TagInput 구현 / ♻️ TextInput, PasswordInput 리팩토링 (#92)
* [#43]✨ Implement TextInput component * [#43] ♻️ Refactor TextInput with tailwind-merge and update baseStyles * [#43] ✨ Create IconList component with svg icons * [#43] 💄 Add essential styles for button, input and select in globals.css * [#43] ✨ Implement PasswordInput component * [#43] ✨ Create useToggle hook * [#43] ✅ add storybook stories for PasswordInput component * [#43] 🐛 Fix Storybook bug preventing SVGR components from loading * [#43] ♻️ Update PasswordInput Storybook to ensure icon displays correctly inside input field * [#43] ♻️ Refactor useToggle hook to return an object * [#43] ♻️ Refactor for accessibility, update useToggle hook usage, and remove props extension * [#43] ♻️ Remove className prop from TextInputProps * [#43] ♻️ Extend TextInputProps to accept additional props * [#43] 🚚 Change extension from tsx to ts * [#43] ♻️ TextInput props and wrapping div structure * [#43] ♻️ Refactoring PasswordInput props * [#43] ✨ Complete basic functionality for TagInput component * [#43] ♻️ Update and optimize Storybook * [#43] ✨ Implement tag addition and deletion fuctionalitiy * [#43] 🔧 Resolve bulid warnings
1 parent 591dc7e commit 362963f

File tree

6 files changed

+92
-25
lines changed

6 files changed

+92
-25
lines changed

src/components/common/input/PasswordInput.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
import { IcEyeClosed, IcEyeOpen } from '@/assets/IconList'
2-
import { FieldValues, UseFormRegister } from 'react-hook-form'
32

43
import { useToggle } from '@/hooks/useToggle'
54

65
import { TextInput, TextInputProps } from './TextInput'
76

8-
interface PasswordInputProps
9-
extends Omit<TextInputProps, 'startAdornment' | 'endAdornment' | 'type'> {
10-
error?: boolean
11-
register?: ReturnType<UseFormRegister<FieldValues>>
12-
className?: string
13-
fullWidth?: boolean
14-
}
7+
type PasswordInputProps = Omit<
8+
TextInputProps,
9+
'startAdornment' | 'endAdornment' | 'type'
10+
>
1511

1612
export const PasswordInput = ({
17-
error = false,
18-
register,
19-
className = '',
20-
fullWidth = false,
2113
...props
2214
}: PasswordInputProps): JSX.Element => {
2315
const { isOpen: showPassword, toggle: toggleShowPassword } = useToggle()
@@ -29,9 +21,7 @@ export const PasswordInput = ({
2921

3022
return (
3123
<TextInput
32-
{...register}
3324
type={showPassword ? 'text' : 'password'}
34-
fullWidth={fullWidth}
3525
endAdornment={
3626
<button
3727
aria-label={showPassword ? '비밀번호 숨김' : '비밀번호 보임'}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { IcSearch } from '@/assets/IconList'
2+
import { useState } from 'react'
3+
import { Controller, useFormContext } from 'react-hook-form'
4+
5+
import { DeletableChip } from '../chip'
6+
import { TextInput, TextInputProps } from './TextInput'
7+
8+
interface TagProps {
9+
id: string
10+
label: string
11+
}
12+
13+
interface TagInputProps extends Omit<TextInputProps, 'endAdornment'> {
14+
name: string
15+
}
16+
17+
export const TagInput = ({ name, ...props }: TagInputProps): JSX.Element => {
18+
const { control, setValue, getValues } = useFormContext()
19+
const [inputValue, setInputValue] = useState<string>('')
20+
21+
const handleKeyDown = (
22+
event: React.KeyboardEvent<HTMLInputElement>
23+
): void => {
24+
if (event.key === 'Enter' && inputValue.trim()) {
25+
event.preventDefault()
26+
const newTag: TagProps = {
27+
id: crypto.randomUUID(),
28+
label: inputValue.trim(),
29+
}
30+
const currentTags = getValues(name) || []
31+
setValue(name, [...currentTags, newTag])
32+
setInputValue('')
33+
}
34+
}
35+
36+
const handleTagDelete = (id: string): void => {
37+
const updatedTags =
38+
getValues(name)?.filter((tag: TagProps) => tag.id !== id) || []
39+
setValue(name, updatedTags)
40+
}
41+
42+
return (
43+
<div>
44+
<Controller
45+
name={name}
46+
control={control}
47+
render={({ field: { value: _value, onChange: _onChange } }) => (
48+
<TextInput
49+
value={inputValue}
50+
onChange={e => setInputValue(e.target.value)}
51+
onKeyDown={handleKeyDown}
52+
endAdornment={
53+
<IcSearch width={24} height={24} aria-label='검색 아이콘' />
54+
}
55+
{...props}
56+
/>
57+
)}
58+
/>
59+
<div className='mt-10 flex flex-wrap gap-x-4'>
60+
{getValues(name)?.map((tag: TagProps) => (
61+
<DeletableChip
62+
key={tag.id}
63+
label={tag.label}
64+
onDelete={() => handleTagDelete(tag.id)}
65+
/>
66+
))}
67+
</div>
68+
</div>
69+
)
70+
}

src/components/common/input/TextInput.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import clsx from 'clsx'
2-
import { ReactNode } from 'react'
32
import { FieldValues, UseFormRegister } from 'react-hook-form'
43
import { twMerge } from 'tailwind-merge'
54

65
export interface TextInputProps
76
extends React.InputHTMLAttributes<HTMLInputElement> {
87
error?: boolean
98
register?: ReturnType<UseFormRegister<FieldValues>>
10-
startAdornment?: ReactNode
11-
endAdornment?: ReactNode
9+
startAdornment?: React.ReactElement
10+
endAdornment?: React.ReactElement
1211
fullWidth?: boolean
1312
}
1413

@@ -20,8 +19,8 @@ export const TextInput = ({
2019
type = 'text',
2120
error = false,
2221
register,
23-
startAdornment = '',
24-
endAdornment = '',
22+
startAdornment,
23+
endAdornment,
2524
className = '',
2625
fullWidth = false,
2726
...props
@@ -40,7 +39,7 @@ export const TextInput = ({
4039
)
4140

4241
return (
43-
<div className='relative'>
42+
<div className={clsx('relative', fullWidth ? 'w-full' : 'w-min')}>
4443
{startAdornment && (
4544
<span className='absolute left-14 top-10'>{startAdornment}</span>
4645
)}

src/stories/common/input/PasswordInput.stories.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { fn } from '@storybook/test'
33
import { PasswordInput } from '@/components/common/input/PasswordInput'
44

55
export default {
6-
title: 'Example/PasswordInput',
6+
title: 'Common/Input/PasswordInput',
77
component: PasswordInput,
88
parameters: {
9-
layout: 'fullscreen',
9+
layout: 'padded',
1010
},
1111
tags: ['input', 'password'],
1212
argTypes: {
@@ -21,13 +21,18 @@ export default {
2121
export const Default = {
2222
args: {
2323
placeholder: '비밀번호를 입력하세요.',
24-
fullWidth: true,
2524
},
2625
}
2726

2827
export const Error = {
2928
args: {
3029
error: true,
30+
},
31+
}
32+
33+
export const FullWidth = {
34+
args: {
35+
placeholder: '비밀번호를 입력하세요.',
3136
fullWidth: true,
3237
},
3338
}

src/stories/common/input/TextInput.stories.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { TextInput } from '@/components/common/input/TextInput'
44

55
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
66
export default {
7-
title: 'Example/TextInput',
7+
title: 'Common/Input/TextInput',
88
component: TextInput,
99
parameters: {
1010
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
11-
layout: 'fullscreen',
11+
layout: 'padded',
1212
},
1313
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
1414
tags: ['common', 'input'],
@@ -24,18 +24,21 @@ export default {
2424
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
2525
export const Default = {
2626
args: {
27+
placeholder: '텍스트를 입력하세요.',
2728
primary: true,
2829
},
2930
}
3031

3132
export const Error = {
3233
args: {
34+
placeholder: '비밀번호를 입력하세요.',
3335
error: true,
3436
},
3537
}
3638

3739
export const FullWidth = {
3840
args: {
41+
placeholder: '텍스트를 입력하세요.',
3942
fullWidth: true,
4043
},
4144
}

0 commit comments

Comments
 (0)