Skip to content

Commit

Permalink
refactor: PinInput, FormatInput의 로직을 hook으로 관심사 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
bytrustu committed Apr 9, 2024
1 parent f53ba4e commit fcc138c
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 176 deletions.
17 changes: 17 additions & 0 deletions src/shared/components/Input/FormatInput.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext, RefObject, ReactElement } from 'react';
import type { UpdateValueProps, InputType } from './Input.type';

export type FormatInputContextValue = {
id: string;
values: string[];
inputElementCount: number;
updateValue: ({ index, value, inputRefs, maxLength, focus }: UpdateValueProps) => void;
inputRefs: RefObject<HTMLInputElement | null>[];
type: InputType;
mask: boolean;
separator: string | ReactElement;
showCompletedSeparator?: boolean;
error: boolean;
};

export const FormatInputContext = createContext<FormatInputContextValue | null>(null);
100 changes: 22 additions & 78 deletions src/shared/components/Input/FormatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,27 @@
import {
PropsWithChildren,
useContext,
createContext,
useMemo,
useRef,
RefObject,
ReactElement,
InputHTMLAttributes,
ChangeEvent,
FocusEvent,
forwardRef,
} from 'react';
import { useInputFieldsValues, useInputRefs } from './hooks';
import { FormatInputContext, FormatInputContextValue } from './FormatInput.context';
import { useFormatInputTextCounter, useInputFieldsValues, useInputRefs } from './hooks';
import { useFormatInputField } from './hooks/useFormatInputField';
import { INPUT_COLOR, INPUT_FONT_SIZE, INPUT_FONT_WEIGHT } from './Input.constant';
import type { UpdateValueProps, InputType } from './Input.type';
import { findComponentsInChildren, isValidateInputValueByType, isValidInputRef } from './utils';
import { findComponentsInChildren } from './utils';
import { StyleProps, styleToken, Box, HStack, Label, TextField, Typography } from '@/shared';

export type FormatInputContextValue = {
id: string;
values: string[];
inputElementCount: number;
updateValue: ({ index, value, inputRefs, maxLength, focus }: UpdateValueProps) => void;
inputRefs: RefObject<HTMLInputElement | null>[];
type: InputType;
mask: boolean;
separator: string | ReactElement;
showCompletedSeparator?: boolean;
error: boolean;
};

type FormatInputProps = Partial<FormatInputContextValue> & {
value: string[];
pattern?: RegExp;
onValueChange?: (payload: { values: string[] }) => void;
onValueComplete?: (payload: { values: string[] }) => void;
};

const FormatInputContext = createContext<FormatInputContextValue | null>(null);

export const FormatInput = ({
children,
id = '',
Expand Down Expand Up @@ -129,54 +112,21 @@ const FormatField = forwardRef<HTMLInputElement, FormatFieldProps & StyleProps>(
}: FormatFieldProps & StyleProps,
ref,
) => {
const context = useContext(FormatInputContext);
if (context === null) {
throw new Error('FormatInput.Input 컴포넌트는 FormatInput.Root 하위에서 사용되어야 합니다.');
}

const { id, inputElementCount, values, updateValue, inputRefs, type, separator, showCompletedSeparator } = context;
const inputRef = ref || inputRefs[index];

const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;

if (pattern && !pattern.test(inputValue)) {
console.warn('입력 형식이 올바르지 않습니다.');
return;
}

if (validateInput && !validateInput(inputValue)) {
console.warn('입력 값이 유효하지 않습니다.');
return;
}

if (!isValidateInputValueByType(type, inputValue)) {
return;
}

updateValue({
index,
value: inputValue,
inputRefs,
maxLength,
focus: !readOnly,
});
};

const inputType = mask ? 'password' : 'text';
const inputValue = values[index];

const validSeparator = index < inputElementCount - 1 && separator && index <= inputElementCount - 1;
const showSeparator = !showCompletedSeparator || (showCompletedSeparator && maxLength === inputValue?.length);
const { separator, validSeparator, showSeparator, ...restFormatInputField } = useFormatInputField({
ref,
index,
readOnly,
mask,
maxLength,
pattern,
validateInput,
});

return (
<>
<TextField
id={`formatted-input-${id}-${index}`}
type={inputType}
variant="unstyled"
maxLength={maxLength}
value={inputValue}
readOnly={readOnly}
width={width}
color={color}
Expand All @@ -186,8 +136,7 @@ const FormatField = forwardRef<HTMLInputElement, FormatFieldProps & StyleProps>(
_placeholder={{
color: styleToken.color.gray400,
}}
onChange={onChange}
{...(isValidInputRef(inputRef) && { ref: inputRef })}
{...restFormatInputField}
{...props}
/>
{validSeparator && (
Expand Down Expand Up @@ -218,22 +167,17 @@ const FormatInputTextCounter = ({
index,
inputRef: propInputRef,
...props
}: PropsWithChildren<{ index: number; inputRef?: RefObject<HTMLInputElement | null> } & StyleProps>) => {
const context = useContext(FormatInputContext);
if (context === null) {
throw new Error('FormatInput.Input 컴포넌트는 FormatInput.Root 하위에서 사용되어야 합니다.');
}
const { inputRefs } = context;
const inputRef = propInputRef || inputRefs[index];

const currentLength = inputRef.current?.value.length ?? 0;
const maxLength = inputRef.current?.maxLength ?? 0;

const counterText = `${currentLength} / ${maxLength}`;
}: PropsWithChildren<
{
index: number;
inputRef?: RefObject<HTMLInputElement | null>;
} & StyleProps
>) => {
const inputTextCounter = useFormatInputTextCounter({ index, inputRef: propInputRef });

return (
<Typography variant="caption" color={styleToken.color.gray400} {...props}>
{counterText}
{inputTextCounter.value}
</Typography>
);
};
Expand Down
16 changes: 16 additions & 0 deletions src/shared/components/Input/PinInput.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createContext, RefObject } from 'react';
import { InputType, UpdateValueProps } from '@/shared';

type PinInputContextValue = {
values: string[];
inputElementCount: number;
updateValue: ({ index, value, inputRefs, maxLength, focus }: UpdateValueProps) => void;
inputRefs: RefObject<HTMLInputElement | null>[];
type: InputType;
mask: boolean;
id?: string;
placeholder?: string;
enableVirtualKeyboard?: boolean;
};

export const PinInputContext = createContext<PinInputContextValue | null>(null);
104 changes: 6 additions & 98 deletions src/shared/components/Input/PinInput.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import {
PropsWithChildren,
useContext,
createContext,
FormEvent,
useMemo,
RefObject,
forwardRef,
useState,
FocusEvent,
} from 'react';
import { PropsWithChildren, useContext, useMemo, forwardRef, FocusEvent } from 'react';
import { useInputFieldsValues, useInputRefs } from './hooks';
import { findComponentsInChildren, isValidateInputValueByType, isValidInputRef } from './utils';
import { CARD_PASSWORD_NUMBERS } from '@/card';
import { usePinInputField } from './hooks/usePinInputField';
import { PinInputContext } from './PinInput.context';
import { findComponentsInChildren } from './utils';
import {
StyleProps,
INPUT_COLOR,
INPUT_FONT_SIZE,
INPUT_FONT_WEIGHT,
InputType,
UpdateValueProps,
styleToken,
Box,
Label,
TextField,
useModal,
VirtualKeyboardBottomSheet,
} from '@/shared';

type PinInputProps = PropsWithChildren<{
Expand All @@ -42,17 +30,6 @@ type PinInputProps = PropsWithChildren<{
onValueComplete?: (details: { values: string[] }) => void;
}>;

type PinInputContextValue = {
values: string[];
inputElementCount: number;
updateValue: ({ index, value, inputRefs, maxLength, focus }: UpdateValueProps) => void;
inputRefs: RefObject<HTMLInputElement | null>[];
type: InputType;
mask: boolean;
} & Pick<PinInputProps, 'id' | 'placeholder' | 'enableVirtualKeyboard'>;

const PinInputContext = createContext<PinInputContextValue | null>(null);

export const PinInput = ({
id = '',
type = 'numeric',
Expand Down Expand Up @@ -136,89 +113,20 @@ const PinInputField = forwardRef<
},
ref,
) => {
const context = useContext(PinInputContext);
if (context === null) {
throw new Error('PinInput.Input 컴포넌트는 PinInput.Root 하위에서 사용되어야 합니다.');
}

const showModal = useModal();

const { id, inputElementCount, placeholder, values, updateValue, inputRefs, type, mask, enableVirtualKeyboard } =
context;
const inputRef = ref || inputRefs[index];
const [error, setError] = useState(false);

const handleChange = (e: FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
if (enableVirtualKeyboard) {
return;
}
if (!isValidateInputValueByType(type, inputValue)) {
return;
}
updateValue({
index,
value: inputValue,
inputRefs,
maxLength: 1,
focus: true,
});
setError(false);
};

const isLastInput = index === inputElementCount - 1;
const inputType = mask ? 'password' : 'text';
const inputValue = index < inputElementCount ? values[index] : placeholder;
const marginRight = isLastInput ? '0' : '10px';

const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
onBlur?.(e);
setError(isValidInputRef(inputRef) && inputRef.current?.value.length === 0);
};

const handleFocus = async (e: FocusEvent<HTMLInputElement>) => {
if (enableVirtualKeyboard) {
const virtualKeyboardValues = CARD_PASSWORD_NUMBERS.map(String);
const virtualKeyboardValue = await showModal<string>(
<VirtualKeyboardBottomSheet values={virtualKeyboardValues} shuffle />,
{
closeOverlayClick: true,
placement: 'bottom',
},
);
if (virtualKeyboardValue) {
updateValue({
index,
value: virtualKeyboardValue,
inputRefs,
maxLength: 1,
focus: true,
});
}
}
onFocus?.(e);
setError(false);
};
const { error, ...restPinInputField } = usePinInputField({ ref, index, onBlur, onFocus });

return (
<TextField
id={`pin-input-${id}-${index}`}
type={inputType}
variant="filled"
maxLength={1}
value={inputValue}
readOnly={readOnly}
width="43px"
color={color}
fontSize={fontSize}
fontWeight={fontWeight}
textAlign="center"
marginRight={marginRight}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
{...(error && { outline: `2px solid ${styleToken.color.rose}` })}
{...(isValidInputRef(inputRef) && { ref: inputRef })}
{...restPinInputField}
{...props}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions src/shared/components/Input/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './useInputFieldsValues';
export * from './useInputRefs';
export * from './useInputValues';
export * from './useFormatInputField';
export * from './useFormatInputTextCounter';
export * from './usePinInputField';
Loading

0 comments on commit fcc138c

Please sign in to comment.