Skip to content

Commit

Permalink
Feat/#4 InputField를 추가합니다.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zero-1016 authored Sep 14, 2024
1 parent 52dd4be commit c689218
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 1 deletion.
6 changes: 5 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"shadow-radius",
"shadow-opacity",
"shadow-blur",
"elevation"
"elevation",
"outline-color",
"outline-offset",
"outline-style",
"outline-width"
]
}
]
Expand Down
106 changes: 106 additions & 0 deletions src/components/common/input-field/InputField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Ionicons } from '@expo/vector-icons';
import type { Meta, StoryObj } from '@storybook/react';
import { useCallback, useState } from 'react';
import { View } from 'react-native';

import { color } from '@/styles/theme';

import InputField from './';

const InputFieldMeta: Meta<typeof InputField> = {
title: 'common/InputField',
component: InputField,
argTypes: {
multiline: {
control: {
type: 'boolean',
},
description: '여러 줄 입력 여부를 설정합니다.',
},
touched: {
control: {
type: 'boolean',
},
description: '터치 여부를 설정합니다.',
},
disabled: {
control: {
type: 'boolean',
},
description: '비활성화 여부를 설정합니다.',
},
placeholder: {
control: {
type: 'text',
},
description: '플레이스홀더를 설정합니다.',
},
error: {
control: {
type: 'text',
},
description: '에러 메시지를 설정합니다.',
},
},
};

export default InputFieldMeta;

export const Primary: StoryObj<typeof InputField> = {
args: {
placeholder: '프로젝트의 이름을 적어주세요',
},
render: (args) => {
const [touched, setTouched] = useState(false);
const [error, setError] = useState('');

const onChangeText = useCallback((text: string) => {
if (text.length > 10) {
setError('올바르지 않은 형식입니다');
} else {
setError('');
}
}, []);

const onFocus = useCallback(() => {
setTouched(true);
}, []);

return (
<View style={{ flex: 1, backgroundColor: color.Background.Alternative, padding: 20 }}>
<InputField
error={error}
touched={touched}
onChangeText={onChangeText}
onFocus={onFocus}
{...args}
/>
</View>
);
},
};

export const WithIcon: StoryObj<typeof InputField> = {
args: {
icon: (
<Ionicons
name='search'
size={24}
color={color.Label.Normal}
/>
),
},
};

export const MultiLine: StoryObj<typeof InputField> = {
args: {
multiline: true,
icon: (
<Ionicons
name='search'
size={24}
color={color.Label.Normal}
/>
),
},
};
71 changes: 71 additions & 0 deletions src/components/common/input-field/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ForwardedRef, ReactNode } from 'react';
import React, { forwardRef, useRef } from 'react';
import type { TextInput } from 'react-native';
import { type TextInputProps } from 'react-native';
import { Pressable } from 'react-native';

import { color } from '@/styles/theme';
import { mergeRefs } from '@/utils';

import * as S from './style';

interface InputFieldProps extends TextInputProps {
touched?: boolean;
disabled?: boolean;
error?: string;
icon?: ReactNode;
}

const InputField = forwardRef(
(
{ touched, disabled = false, error, icon = null, multiline, ...props }: InputFieldProps,
ref?: ForwardedRef<TextInput>
) => {
const innerRef = useRef<TextInput>(null);

const handlePressInput = () => {
innerRef.current?.focus();
};

return (
<Pressable onPress={handlePressInput}>
<S.Container
style={{
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.22,
shadowRadius: 1.22,

elevation: 2,
}}
$disabled={disabled}
$isError={Boolean(touched) && Boolean(error)}>
<S.InnerContainer $isIcon={!!icon}>
{icon}
<S.TextInput
// @ts-expect-error: outline is not a valid style property
style={{ outline: 'none' }}
ref={ref ? mergeRefs(innerRef, ref) : innerRef}
multiline={Boolean(multiline)}
numberOfLines={multiline ? 4 : 1}
editable={!disabled}
autoCapitalize='none'
spellCheck={false}
autoCorrect={false}
placeholderTextColor={color.Label.Alternative}
{...props}
/>
</S.InnerContainer>
{touched && !!error && <S.ErrorText>{error}</S.ErrorText>}
</S.Container>
</Pressable>
);
}
);

InputField.displayName = 'InputField';

export default InputField;
48 changes: 48 additions & 0 deletions src/components/common/input-field/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled, { css } from '@emotion/native';
import type { Theme } from '@emotion/react';

import { flexDirectionRow } from '@/styles/common';

const errorStyle = (theme: Theme) => css`
border-color: ${theme.color.Status.Error};
border-width: 1px;
`;

const disabledStyle = (theme: Theme) => css`
color: ${theme.color.Label.Alternative};
background-color: ${theme.color.Label.Alternative};
`;

const hasIconStyle = css`
${flexDirectionRow};
gap: 5px;
`;

export const Container = styled.View<{
$isError: boolean;
$disabled: boolean;
}>`
${({ $isError, theme }) => $isError && errorStyle(theme)};
${({ $disabled, theme }) => $disabled && disabledStyle(theme)};
padding: 18px 16px;
background-color: ${({ theme }) => theme.color.Background.Normal};
border-radius: 8px;
`;

export const InnerContainer = styled.View<{ $isIcon: boolean }>`
${({ $isIcon }) => $isIcon && hasIconStyle}
`;

export const TextInput = styled.TextInput`
flex-grow: 1;
padding: 0;
font-family: Pretendard, serif;
font-size: 15px;
color: ${(props) => props.theme.color.Label.Normal};
`;

export const ErrorText = styled.Text`
padding-top: 5px;
font-size: 12px;
color: ${(props) => props.theme.color.Status.Error};
`;
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './device';
export * from './getSize';
export * from './mergeRef';
export * from './render';
13 changes: 13 additions & 0 deletions src/utils/mergeRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ForwardedRef } from 'react';

export function mergeRefs<T>(...refs: ForwardedRef<T>[]) {
return (node: T) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node);
} else if (ref !== null) {
ref.current = node;
}
});
};
}

0 comments on commit c689218

Please sign in to comment.