import { useRef } from 'react';
import styled from '@emotion/styled';
import COLOR from '@/constants/COLOR';
//props
interface IInputBlock {
className?: string;
item: string;
index: number; // 인풋창 위치 확인용
codeArr: string[]; // 전체 입력코드확인
disabled?: boolean;
onChange: (code: string[]) => void; //coderArr 수정용
}
const InputBlock = (props: IInputBlock) => {
const { className, item, index, codeArr, onChange, disabled } = props;
const inputRef = useRef<HTMLInputElement>(null);
const value = codeArr[index];
const setValue = (value: string, position: number = 0) => {
const nextCodeArr = [...codeArr];
nextCodeArr[index + position] = value;
onChange(nextCodeArr);
};
// 포커스된 인풋 전 인풋 ref
const nextInput = inputRef.current?.nextElementSibling as HTMLInputElement;
// 포커스된 인풋 다음 인풋 ref
const previousInput = inputRef.current
?.previousElementSibling as HTMLInputElement;
// 붙여넣기
const _onPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
//1. 클립보드에 있는 복사된 텍스트를 가져온다.
//2. 텍스트가 number가 아니면 함수를 종료한다.
//3. 커서위치와, 인풋 인덱스를 확인하여 code array 값을 변경한다.
const clipboardData = e.clipboardData;
const text = clipboardData.getData('text/plain');
if (isNaN(Number(text))) return;
const valueArr = text.split('');
const nextCodeArr = [...codeArr];
valueArr.map((e, i) => {
const plusIndex = inputRef.current?.selectionStart === 0 ? 0 : 1;
nextCodeArr.splice(index + i + plusIndex, 1, e);
});
onChange(nextCodeArr);
};
// 값입력, onChange
const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
//1. 입력된 값이 number 가아니면 리턴한다.
//2. 이미 입력된 값이 있을 경우 커서 위치 확인
//3. 커서가 앞이면 해당 인풋창 값 변경
//4. 커서가 뒤이면 다음 인풋창 값 변경 및 다음 인풋창으로 포커시 이동
//5. 이미 입력된 값이 없으면 입력
let value = e.target.value;
if (isNaN(Number(value))) return;
if (String(value).length > 1) {
//커서 위치에 따른 분기처리
if (nextInput && inputRef.current?.selectionStart === 2) {
setValue(value[1], +1);
nextInput.focus();
}
if (inputRef.current?.selectionStart === 1) {
setValue(value[0]);
}
return;
}
setValue(value);
};
// 좌우 화살표 및 BackSpace 처리
const _onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
//1. 입력 키 확인
//2. 이전/다음 인풋 확인
//3. 커서위치 확인
//4. 해당 로직 실행
if ((e.key === 'arrowRight' || e.key === 'ArrowRight') && nextInput) {
if (
// index === 0 &&
inputRef.current?.selectionStart === 0 &&
value !== ''
) {
inputRef.current?.setSelectionRange(1, 1);
} else {
nextInput.focus();
}
}
if ((e.key === 'arrowLeft' || e.key === 'ArrowLeft') && previousInput) {
if (
// codeArr.length - 1 === index &&
inputRef.current?.selectionStart === 1
) {
inputRef.current?.setSelectionRange(0, 0);
} else {
previousInput.focus();
}
}
if (e.key === 'Backspace' && previousInput) {
if (inputRef.current?.selectionStart === 1) {
const nextCodeArr = [...codeArr];
nextCodeArr[index] = '';
onChange(nextCodeArr);
} else {
setValue('', -1);
previousInput.focus();
}
}
};
return (
<InputBlockStyle
className={`${value.length > 0 ? 'focused' : ''} ${className ?? ''}`}
type="text"
ref={inputRef}
value={value}
onKeyDown={_onKeyDown}
onChange={_onChange}
onPaste={_onPaste}
isInputValue={value.length > 0}
inputMode="numeric"
/>
);
};
interface InputBlockStyleProps {
isInputValue: boolean;
}
export const InputBlockStyle = styled.input<InputBlockStyleProps>`
width: 40px;
height: 40px;
border-radius: 8px;
background-color: ${COLOR['GRAY6__#E7E7E7']};
border: none;
text-align: center;
font-size: 20px;
font-weight: 700;
line-height: 140%; /* 33.6px */
transition: 0.2s;
:focus {
outline: none;
}
&.focused {
background-color: ${COLOR['WHITE__#FFFFFF']};
border: 1px solid ${COLOR['BLACK__#000000']};
}
&.error {
border: 1px solid ${COLOR['ERROR__#EC1A26']};
}
`;
export default InputBlock;
import React, { useState } from 'react';
import InputBlock from './ModalBody.InputBlock';
(...)
export default function ModalBodyVerification() {
const [codeArr, setCodeArr] = useState<string[]>(['', '', '', '', '', '']);
(...)
const _onChangeCode = (code: string[]) => {
// setCodeArr값은 codeArr.length로 잘라준다. 안그럼 인풋창 개수가 마음대로 변경됨..
setCodeArr(code.slice(0, codeArr.length));
//show error text
//if (showCodeError === 'code' || showCodeError === 'invalidCode')
// setShowCodeError(null);
};
return (
<ModalBodyVerificationStyle>
(...)
<div className="box">
(...)
<div className="code__input">
{codeArr.map((item, index) => (
<InputBlock
key={index}
item={item}
index={index}
codeArr={codeArr}
onChange={_onChangeCode}
className={
showCodeError === 'code' || showCodeError === 'invalidCode'
? 'error'
: ''
}
/>
))}
</div>
</div>
(...)
</ModalBodyVerificationStyle>
);
}
// style
(...)
.code__input {
margin-top: 12px;
display: flex;
justify-content: center;
gap: 8px;
}
해당 인풋 박스를 만들고자했을때 별거 아니라고 생각했으나, 커서의 위치와 포커스 이동등 생각보다 고려할께 많았다. nextElementSibling, selectionStart 등 평소 사용할 기회가 없던 method를 사용해 볼 기회이기도해서 재밌었다. 정말 별에별 method가 다 있구나 싶다.