Skip to content

Commit 5a82c1a

Browse files
committed
feat: ChatInput 멀티라인 입력 및 자동 높이 조정 추가
1 parent dbdbd7a commit 5a82c1a

1 file changed

Lines changed: 24 additions & 6 deletions

File tree

src/shared/ui/ChatInput/ChatInput.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { SendIcon } from '@shared/assets/icons';
2-
import { useState, type ChangeEventHandler, type KeyboardEventHandler } from 'react';
2+
import { useLayoutEffect, useRef, useState, type ChangeEventHandler, type KeyboardEventHandler } from 'react';
33
import { chatInputVariants } from './ChatInput.variants';
44

5+
const TEXTAREA_LINE_HEIGHT = 24;
6+
const TEXTAREA_MAX_LINES = 4;
7+
58
export interface ChatInputProps {
69
placeholder?: string;
710
onSend: (message: string) => void;
@@ -12,6 +15,7 @@ export interface ChatInputProps {
1215
export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputProps) => {
1316
const [internalMessage, setInternalMessage] = useState('');
1417
const [isFocused, setFocused] = useState(false);
18+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
1519

1620
const isControlled = value !== undefined;
1721
const message = isControlled ? value : internalMessage;
@@ -23,7 +27,7 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
2327
sendState: isFilled ? 'active' : 'inactive',
2428
});
2529

26-
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
30+
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (event) => {
2731
const nextValue = event.target.value;
2832
if (isControlled) {
2933
onChange?.(nextValue);
@@ -46,18 +50,31 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
4650
}
4751
};
4852

49-
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
50-
if (event.key === 'Enter' && !event.nativeEvent.isComposing) {
53+
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {
54+
if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
5155
event.preventDefault();
5256
handleSend();
5357
}
5458
};
5559

60+
useLayoutEffect(() => {
61+
const element = textareaRef.current;
62+
if (!element) {
63+
return;
64+
}
65+
66+
element.style.height = 'auto';
67+
const maxHeight = TEXTAREA_LINE_HEIGHT * TEXTAREA_MAX_LINES;
68+
const nextHeight = Math.min(element.scrollHeight, maxHeight);
69+
element.style.height = `${nextHeight}px`;
70+
element.style.overflowY = element.scrollHeight > maxHeight ? 'auto' : 'hidden';
71+
}, [message]);
72+
5673
return (
5774
<div className={styles.root()}>
5875
<div className={styles.wrapper()}>
59-
<input
60-
type="text"
76+
<textarea
77+
ref={textareaRef}
6178
value={message}
6279
onFocus={() => setFocused(true)}
6380
onBlur={() => setFocused(false)}
@@ -66,6 +83,7 @@ export const ChatInput = ({ placeholder, onSend, value, onChange }: ChatInputPro
6683
placeholder={placeholder}
6784
aria-label={placeholder ?? 'Chat input'}
6885
onKeyDown={handleKeyDown}
86+
rows={1}
6987
/>
7088
<button type="button" className={styles.button()} onClick={handleSend} disabled={!isFilled} aria-label="Send">
7189
<SendIcon className={styles.icon()} aria-hidden="true" />

0 commit comments

Comments
 (0)