diff --git a/docs/docs/hooks/useEnterKey.md b/docs/docs/hooks/useEnterKey.md new file mode 100644 index 0000000..fa94b3e --- /dev/null +++ b/docs/docs/hooks/useEnterKey.md @@ -0,0 +1,88 @@ +# useEnterKey + +Enter 키 입력 시 콜백 함수 실행과 버튼 클릭을 처리하는 커스텀 React Hook입니다. + +입력 요소에 포커스된 상태에서 Enter 키(또는 지정된 키)를 누르면 콜백 함수를 실행하고 연결된 버튼을 자동으로 클릭합니다. 한글 입력 중(IME 조합 중)에는 동작하지 않도록 처리되어 있어 안전합니다. + +## 🔗 사용법 + +```tsx +const { targetRef } = useEnterKey(options); +``` + +### 매개변수(options) + +- `callback: () => void | Promise` + - 키 입력 시 실행할 콜백 함수 + - 동기 함수와 비동기 함수 모두 지원 + +- `buttonRef?: RefObject` + - 자동으로 클릭할 버튼의 ref (선택사항) + - 키 입력 시 해당 버튼의 `click()` 메서드가 호출됩니다 + +### 반환값 + +`{ targetRef }` + +| 속성 | 타입 | 설명 | +| ----------- | ---------------------------------- | -------------------------------- | +| `targetRef` | `RefObject` | 키 입력을 감지할 대상 요소의 ref | + +--- + +## ✅ 예시 + +### 기본 사용법 (Enter 키로 폼 제출) + +```tsx +import { useEnterKey } from './hooks/useEnterKey'; + +function LoginForm() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const buttonRef = useRef(null); + + const handleSubmit = () => { + console.log('로그인 시도:', { username, password }); + }; + + const { targetRef: usernameRef } = useEnterKey({ + callback: handleSubmit, + buttonRef, + }); + + const { targetRef: passwordRef } = useEnterKey({ + callback: handleSubmit, + buttonRef, + }); + + return ( +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+ ); +} +``` + +## 📋 주요 특징 + +- **IME 조합 처리**: 한글 입력 중(`isComposing` 상태)에는 동작하지 않아 의도치 않은 실행을 방지합니다 +- **포커스 기반 동작**: 대상 요소에 포커스된 상태에서만 키 입력을 감지합니다 +- **비동기 지원**: 콜백 함수로 비동기 함수도 사용할 수 있습니다 +- **이벤트 기본 동작 방지**: `preventDefault()`로 기본 키 동작을 차단합니다 diff --git a/packages/hooks/src/libs/useEnterKey.spec.tsx b/packages/hooks/src/libs/useEnterKey.spec.tsx new file mode 100644 index 0000000..6724553 --- /dev/null +++ b/packages/hooks/src/libs/useEnterKey.spec.tsx @@ -0,0 +1,81 @@ +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { useRef } from 'react'; +import { useEnterKey } from './useEnterKey'; + +function TestComponent({ callback }: { callback: jest.Mock }) { + const buttonRef = useRef(null); + const { targetRef } = useEnterKey({ callback, buttonRef }); + + return ( +
+ + +
+ ); +} + +describe('useEnterKey', () => { + it('입력 요소에 포커스된 상태에서 Enter 키를 누르면 callback이 실행된다', () => { + const callback = jest.fn(); + const { getByTestId } = render(); + + const input = getByTestId('input'); + input.focus(); + + fireEvent.keyDown(window, { key: 'Enter', code: 'Enter' }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('입력 요소에 포커스된 상태에서 Enter 키를 누르면 버튼 클릭 이벤트가 발생한다', () => { + const callback = jest.fn(); + const { getByTestId } = render(); + + const input = getByTestId('input'); + const button = getByTestId('button'); + const buttonClick = jest.fn(); + button.onclick = buttonClick; + + input.focus(); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + expect(buttonClick).toHaveBeenCalledTimes(1); + }); + + it('입력 요소에 포커스되지 않은 상태에서 Enter 키를 누르면 아무 동작도 하지 않는다', () => { + const callback = jest.fn(); + render(); + + fireEvent.keyDown(window, { key: 'Enter', code: 'Enter' }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('다른 키를 누르면 callback이나 버튼 클릭 이벤트가 발생하지 않는다', () => { + const callback = jest.fn(); + const { getByTestId } = render(); + + const input = getByTestId('input'); + input.focus(); + + fireEvent.keyDown(input, { key: 'a', code: 'KeyA' }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('callback이 비동기 함수일 경우에도 정상적으로 실행된다', async () => { + const callback = jest.fn(async () => Promise.resolve()); + const { getByTestId } = render(); + + const input = getByTestId('input'); + input.focus(); + + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + await waitFor(() => { + expect(callback).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/hooks/src/libs/useEnterKey.ts b/packages/hooks/src/libs/useEnterKey.ts new file mode 100644 index 0000000..7b01be7 --- /dev/null +++ b/packages/hooks/src/libs/useEnterKey.ts @@ -0,0 +1,31 @@ +import { RefObject, useEffect, useRef } from 'react'; + +interface UseEnterKeyOptions { + callback: () => void | Promise; + buttonRef?: RefObject; +} + +export function useEnterKey({ callback, buttonRef }: UseEnterKeyOptions) { + const targetRef = useRef(null); + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + if (document.activeElement === targetRef.current && !e.isComposing) { + callbackRef.current?.(); + buttonRef?.current?.click(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [buttonRef]); + + return { targetRef }; +}