Skip to content

Commit

Permalink
ADCIO-3115) feat: implement file input (#37)
Browse files Browse the repository at this point in the history
* feat: implement file input

* refactor: refactor file handler

* feat: modify to accept more mime types

* docs: modify storybook

* feat: add file name to input

* feat: export file input

* refactor: rollback input container style

---------

Co-authored-by: dev-redo <mis05041@naver.com>
  • Loading branch information
dev-redo and dev-redo authored Apr 8, 2024
1 parent 8a1b588 commit d11cff9
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 14 deletions.
102 changes: 102 additions & 0 deletions src/components/Input/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { ChangeEvent, useId } from 'react';

import styled from '@emotion/styled';

import Icon from '../Icon';
import { B3 } from '../Text';
import {
BaseInputType,
type InputBaseProps,
InputContainer,
baseInputStyles,
} from './InputContainer';

export type FileInputProps = Omit<InputBaseProps, 'children' | 'value'> & {
uploadType: 'audio' | 'image' | 'video' | 'csv' | 'txt' | 'pdf' | 'default';
file: File | null;
onUpload: (file: File) => void;
};

const getMimeType = (uploadType?: FileInputProps['uploadType']) => {
switch (uploadType) {
case 'audio':
case 'image':
case 'video':
return `${uploadType}/*`;
case 'csv':
return 'text/csv';
case 'txt':
return 'text/plain';
case 'pdf':
return '.pdf';
default:
return '*/*';
}
};

export function FileInput({
label,
placeholder,
description,
name,
file,
error,
disabled,
onUpload,
defaultValue,
required = false,
width,
tooltip,
onClick,
height,
uploadType = 'default',
...props
}: FileInputProps) {
const id = useId();
return (
<InputContainer
cursorPointer
onClick={onClick}
label={label}
description={description}
required={required}
error={error}
width={width}
tooltip={tooltip}
{...props}
leftSection={<Icon.Upload />}
>
<>
<FileUploadInput
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
onUpload(event.target.files[0]);
}
}}
type="file"
accept={getMimeType(uploadType)}
id={`file-input-${id}`}
/>
<UploadButton
isLeftSection
error={error}
height={height}
cursorPointer
htmlFor={`file-input-${id}`}
>
{file ? <B3>{file.name}</B3> : <B3 c="grey-50">{placeholder}</B3>}
</UploadButton>
</>
</InputContainer>
);
}

const FileUploadInput = styled.input`
display: none;
`;

export const UploadButton = styled.label<BaseInputType>`
${props => baseInputStyles(props)}
display: flex;
align-items: center;
`;
37 changes: 25 additions & 12 deletions src/components/Input/InputContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Icon from '../Icon';
import { B3, B5, B7 } from '../Text';
import { Tooltip, type TooltipProps } from '../Tooltip';
import { color, typography } from '../styles';
import { css } from '@emotion/react';

export type InputTooltipProps = Omit<TooltipProps, 'children'>;

Expand Down Expand Up @@ -121,18 +122,27 @@ export const ErrorContainer = styled.div`
gap: 4px;
`;

export const BaseInput = styled.input<{
export type BaseInputType = {
error?: string;
isRightSection?: boolean;
isLeftSection?: boolean;
height?: string | number;
cursorPointer?: boolean;
}>`
};

export const baseInputStyles = ({
error,
isRightSection,
isLeftSection,
height,
cursorPointer,
}: BaseInputType) => css`
width: 100%;
padding: ${({ isRightSection, isLeftSection }) => {
if (isLeftSection) return '6px 12px 6px 34px';
return isRightSection ? '6px 36px 6px 12px' : '6px 12px';
}};
padding: ${isLeftSection
? '6px 12px 6px 34px'
: isRightSection
? '6px 36px 6px 12px'
: '6px 12px'};
outline: none;
border: 1px solid ${color['grey-50']};
font-style: normal;
Expand All @@ -141,7 +151,7 @@ export const BaseInput = styled.input<{
color: ${color['grey-80']};
background: ${color.white};
border-radius: 8px;
height: ${p => (typeof p.height === 'number' ? `${p.height}px` : p.height || '34px')};
height: ${typeof height === 'number' ? `${height}px` : height || '34px'};
&:disabled {
border: none;
Expand All @@ -155,19 +165,22 @@ export const BaseInput = styled.input<{
font-size: ${typography.size.xs}px;
}
${({ error }) =>
error
? `
${error
? `
border: 1px solid ${color['error-30']};
background: ${color['error-10']};
`
: `
: `
&:focus-visible,
&:focus {
border: 1px solid ${color['grey-80']};
}
`}
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'default')};
cursor: ${cursorPointer ? 'pointer' : 'default'};
`;

export const BaseInput = styled.input<BaseInputType>`
${props => baseInputStyles(props)}
`;

const QuestionIconWrapper = styled.i`
Expand Down
4 changes: 3 additions & 1 deletion src/components/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { NumberInput, NumberInputProps } from './NumberInput';
import { PasswordInput, PasswordInputProps } from './PasswordInput';
import { TextInput, TextInputProps } from './TextInput';
import { ColorPickerInput, ColorPickerInputProps } from './ColorPickerInput';
import { FileInput, FileInputProps } from './FileInput';

export { InputContainer, TextInput, PasswordInput, NumberInput, ColorPickerInput };
export { InputContainer, TextInput, PasswordInput, NumberInput, ColorPickerInput, FileInput };
export type {
InputBaseProps,
TextInputProps,
PasswordInputProps,
NumberInputProps,
ColorPickerInputProps,
FileInputProps,
};
95 changes: 94 additions & 1 deletion src/stories/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
import Icon from '../components/Icon';
import { BaseInput, type InputTooltipProps } from '../components/Input/InputContainer';
import { NumberInput } from '../components/Input/NumberInput';
import { useState } from 'react';
import { ChangeEvent, useState } from 'react';
import { FileInput, FileInputProps } from '../components/Input/FileInput';

export default {
title: 'Components/Input',
Expand Down Expand Up @@ -93,6 +94,98 @@ export function NumberInputDefault() {
);
}

export const FileInputDefault = () => {
const [files, setFiles] = useState<{
[key in FileInputProps['uploadType']]: File | null;
}>({
audio: null,
image: null,
video: null,
csv: null,
txt: null,
pdf: null,
default: null,
});

const handleFileChange = (type: FileInputProps['uploadType'], file: File | null) => {
setFiles(prevFiles => ({
...prevFiles,
[type]: file,
}));
};

const fileTypes: FileInputProps['uploadType'][] = [
'audio',
'image',
'video',
'csv',
'txt',
'pdf',
'default',
];

return (
<>
{fileTypes.map(type => (
<FileInput
key={type}
label={getFileInputExplain(type)}
placeholder={getFileInputExplain(type)}
description={getFileInputDescription(type)}
width={400}
onUpload={file => handleFileChange(type, file)}
file={files[type as FileInputProps['uploadType'] | 'default']}
uploadType={type as FileInputProps['uploadType']}
/>
))}
</>
);
};

const FILE_TYPE_CONFIGS: {
[key in FileInputProps['uploadType']]: {
explain: string;
description: string;
};
} = {
audio: {
explain: 'MP3, WAV, FLAC 파일 추가',
description: '오디오 파일만 입력 가능한 입력창입니다.',
},
image: {
explain: 'JPG, JPEG, PNG, SVG 파일 추가',
description: '이미지 파일만 입력 가능한 입력창입니다.',
},
video: {
explain: 'MP4, AVI, MOV 파일 추가',
description: '비디오 파일만 입력 가능한 입력창입니다.',
},
csv: {
explain: 'CSV 파일 추가',
description: 'CSV 파일만 입력 가능한 입력창입니다.',
},
txt: {
explain: 'TXT 파일 추가',
description: 'TXT 파일만 입력 가능한 입력창입니다.',
},
pdf: {
explain: 'PDF 파일 추가',
description: 'PDF 파일만 입력 가능한 입력창입니다.',
},
default: {
explain: '전체 유형의 파일 추가',
description: '전체 유형의 파일을 입력할 수 있는 입력창입니다.',
},
};

const getFileInputExplain = (uploadType?: FileInputProps['uploadType']) => {
return (FILE_TYPE_CONFIGS[uploadType || 'default'] || FILE_TYPE_CONFIGS.default).explain;
};

const getFileInputDescription = (uploadType?: FileInputProps['uploadType']) => {
return (FILE_TYPE_CONFIGS[uploadType || 'default'] || FILE_TYPE_CONFIGS.default).description;
};

export function WithIcon() {
return (
<>
Expand Down

0 comments on commit d11cff9

Please sign in to comment.