-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add `FormInput` * feat: add `FormItem`
- Loading branch information
Showing
19 changed files
with
5,048 additions
and
6,137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,7 @@ module.exports = { | |
unnamedComponents: 'arrow-function', | ||
}, | ||
], | ||
'react/prop-types': 'off', | ||
}, | ||
}, | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
app/components/common/FormInput/FormImageInput.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { action } from '@storybook/addon-actions'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
|
||
import FormInput from './FormInput'; | ||
|
||
const meta: Meta<typeof FormInput.Image> = { | ||
component: FormInput.Image, | ||
decorators: [ | ||
function Decorator(Story, ctx) { | ||
const [previewImageUrl, setPreviewImageUrl] = useState(''); | ||
|
||
return ( | ||
<Story | ||
args={{ | ||
...ctx.args, | ||
previewImageUrl, | ||
onImageChange: (imageUrl) => { | ||
action('onImageChange')(imageUrl); | ||
setPreviewImageUrl(imageUrl); | ||
}, | ||
}} | ||
/> | ||
); | ||
}, | ||
], | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof FormInput.Image>; | ||
|
||
export const Main: Story = { | ||
args: { | ||
isError: false, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
import { Breakpoint } from '@/constants/style'; | ||
import { textStyle } from '@/styles/text.css'; | ||
import { themeVars } from '@/styles/theme.css'; | ||
import { getMediaQuery } from '@/utils/style'; | ||
|
||
export const textInputContainer = style({ | ||
display: 'flex', | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
padding: '0 12px', | ||
gap: '10px', | ||
borderRadius: '8px', | ||
backgroundColor: themeVars.color.background.article.hex, | ||
borderColor: 'transparent', | ||
borderStyle: 'solid', | ||
borderWidth: '1px', | ||
height: '33px', | ||
'@media': { | ||
[getMediaQuery([Breakpoint.MOBILE2, Breakpoint.MOBILE1])]: { | ||
gap: '6px', | ||
height: '31px', | ||
}, | ||
}, | ||
}); | ||
|
||
export const blank = style({ | ||
color: themeVars.color.grayscale.gray5.hex, | ||
}); | ||
|
||
export const textInputIcon = style({ | ||
width: '16px', | ||
height: '16px', | ||
'@media': { | ||
[getMediaQuery([Breakpoint.MOBILE2, Breakpoint.MOBILE1])]: { | ||
width: '12px', | ||
height: '12px', | ||
}, | ||
}, | ||
}); | ||
|
||
export const textInput = style([ | ||
textStyle.body1R, | ||
{ | ||
flex: 1, | ||
color: themeVars.color.grayscale.black.hex, | ||
border: 0, | ||
backgroundColor: 'transparent', | ||
'::placeholder': { | ||
color: themeVars.color.grayscale.gray5.hex, | ||
}, | ||
}, | ||
]); | ||
|
||
export const imageInputContainer = style({ | ||
width: '100px', | ||
height: '100px', | ||
borderRadius: '50px', | ||
backgroundColor: themeVars.color.background.article.hex, | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
position: 'relative', | ||
appearance: 'none', | ||
padding: 0, | ||
border: 0, | ||
cursor: 'pointer', | ||
overflow: 'hidden', | ||
'@media': { | ||
[getMediaQuery([Breakpoint.MOBILE2, Breakpoint.MOBILE1])]: { | ||
width: '80px', | ||
height: '80px', | ||
borderRadius: '40px', | ||
}, | ||
}, | ||
}); | ||
|
||
export const imageInput = style({ | ||
display: 'none', | ||
position: 'absolute', | ||
}); | ||
|
||
export const imageInputPreview = style({ | ||
width: '100%', | ||
height: '100%', | ||
objectFit: 'cover', | ||
}); | ||
|
||
export const error = style({ | ||
borderColor: themeVars.color.system.caution.hex, | ||
borderStyle: 'solid', | ||
borderWidth: '1px', | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { | ||
useRef, | ||
type DetailedHTMLProps, | ||
type InputHTMLAttributes, | ||
} from 'react'; | ||
import * as styles from './FormInput.css'; | ||
import { getDataURLFromFiles } from '@/utils/form'; | ||
import { className } from '@/utils/style'; | ||
|
||
interface FormInputPropsBase | ||
extends DetailedHTMLProps< | ||
InputHTMLAttributes<HTMLInputElement>, | ||
HTMLInputElement | ||
> { | ||
isError?: boolean; | ||
} | ||
|
||
export interface FormTextInputProps extends FormInputPropsBase { | ||
Icon?: (props: { className?: string }) => JSX.Element; | ||
onTextChange?: (newText: string) => void; | ||
} | ||
|
||
const FormTextInput = ({ | ||
Icon, | ||
isError, | ||
onTextChange, | ||
...inputProps | ||
}: FormTextInputProps) => ( | ||
<div | ||
className={className( | ||
styles.textInputContainer, | ||
isError ? styles.error : undefined, | ||
!inputProps.value ? styles.blank : undefined, | ||
)} | ||
> | ||
{Icon && <Icon className={styles.textInputIcon} />} | ||
<input | ||
type="text" | ||
{...inputProps} | ||
className={className(styles.textInput, inputProps.className)} | ||
onChange={(e) => { | ||
inputProps.onChange?.(e); | ||
onTextChange?.(e.currentTarget.value); | ||
}} | ||
/> | ||
</div> | ||
); | ||
|
||
export interface FormImageInputProps extends FormInputPropsBase { | ||
multiple?: false; | ||
previewImageUrl?: string; | ||
onImageChange?: (imageUrl: string) => void; | ||
} | ||
|
||
const FormImageInput = ({ | ||
isError, | ||
multiple = false, | ||
previewImageUrl, | ||
onImageChange, | ||
...inputProps | ||
}: FormImageInputProps) => { | ||
const inputElement = useRef<HTMLInputElement>(null); | ||
|
||
return ( | ||
<button | ||
className={className( | ||
styles.imageInputContainer, | ||
isError ? styles.error : undefined, | ||
)} | ||
onClick={() => { | ||
inputElement.current?.click(); | ||
}} | ||
> | ||
{previewImageUrl?.length === 0 && ( | ||
<svg | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
fillRule="evenodd" | ||
clipRule="evenodd" | ||
d="M12.3829 9.12812C12.3829 7.75351 13.4973 6.63917 14.8719 6.63917C16.2465 6.63917 17.3608 7.75351 17.3608 9.12812C17.3608 10.5027 16.2465 11.6171 14.8719 11.6171C13.4973 11.6171 12.3829 10.5027 12.3829 9.12812ZM14.8719 7.78792C14.1317 7.78792 13.5317 8.38794 13.5317 9.12812C13.5317 9.86829 14.1317 10.4683 14.8719 10.4683C15.612 10.4683 16.2121 9.86829 16.2121 9.12812C16.2121 8.38794 15.612 7.78792 14.8719 7.78792ZM20.2595 16.1904C20.4241 15.7995 20.5574 15.3918 20.6563 14.9703C21.1146 13.0166 21.1146 10.9834 20.6563 9.02975C19.9945 6.20842 17.7916 4.00549 14.9703 3.3437C13.0166 2.88543 10.9834 2.88543 9.02975 3.3437C6.20842 4.00549 4.0055 6.20841 3.3437 9.02975C2.88543 10.9834 2.88543 13.0166 3.3437 14.9703C3.39229 15.1774 3.44919 15.3812 3.51403 15.5813L4.11782 14.9777C6.30157 12.7943 9.84209 12.7943 12.0258 14.9777L13.7165 16.668L14.1597 16.2249C15.8579 14.5271 18.5837 14.547 20.2595 16.1904ZM19.701 17.2801L19.6765 17.2506C18.4761 15.8104 16.2978 15.7116 14.9719 17.0373L14.5289 17.4802L16.9422 19.8931C18.0581 19.2711 18.9993 18.3786 19.6794 17.302C19.6868 17.2948 19.694 17.2875 19.701 17.2801ZM15.8264 20.4019L11.2136 15.79C9.47848 14.0552 6.66518 14.0552 4.93003 15.79L3.99195 16.7279C5.00173 18.6839 6.82731 20.1397 9.02975 20.6563C10.9834 21.1146 13.0166 21.1146 14.9703 20.6563C15.2626 20.5877 15.5484 20.5026 15.8264 20.4019Z" | ||
fill="#9E9E9E" | ||
/> | ||
</svg> | ||
)} | ||
<input | ||
ref={inputElement} | ||
type="file" | ||
accept="image/jpeg,image/jpg,image/png,image/gif" | ||
{...inputProps} | ||
className={className( | ||
styles.imageInput, | ||
|
||
inputProps.className, | ||
)} | ||
multiple={multiple} | ||
onChange={async (e) => { | ||
inputProps.onChange?.(e); | ||
|
||
e.preventDefault(); | ||
const files = e.target.files; | ||
|
||
if (files && files[0]) { | ||
const urls = await getDataURLFromFiles(files[0]); | ||
onImageChange?.(urls[0]); | ||
} | ||
|
||
e.currentTarget.value = ''; | ||
}} | ||
/> | ||
{previewImageUrl && previewImageUrl.length > 0 && ( | ||
<img | ||
src={previewImageUrl} | ||
alt="preview before upload" | ||
className={styles.imageInputPreview} | ||
/> | ||
)} | ||
</button> | ||
); | ||
}; | ||
|
||
const FormInput = { | ||
Text: FormTextInput, | ||
Image: FormImageInput, | ||
}; | ||
|
||
export default FormInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { action } from '@storybook/addon-actions'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
|
||
import FormInput from './FormInput'; | ||
|
||
const meta: Meta<typeof FormInput.Text> = { | ||
component: FormInput.Text, | ||
decorators: [ | ||
function Decorator(Story, ctx) { | ||
const [value, setValue] = useState(''); | ||
|
||
return ( | ||
<Story | ||
args={{ | ||
...ctx.args, | ||
value, | ||
onTextChange: (newText) => { | ||
action('onTextChange')(newText); | ||
setValue(newText); | ||
}, | ||
}} | ||
/> | ||
); | ||
}, | ||
], | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof FormInput.Text>; | ||
|
||
export const Main: Story = { | ||
args: { | ||
isError: false, | ||
placeholder: '텍스트를 입력해주세요.', | ||
}, | ||
}; | ||
|
||
export const WithIcon: Story = { | ||
args: { | ||
isError: false, | ||
placeholder: '텍스트를 입력해주세요.', | ||
Icon: ({ className }) => ( | ||
<svg | ||
width="16" | ||
height="17" | ||
viewBox="0 0 16 17" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
className={className} | ||
> | ||
<path | ||
d="M2.21049 9.68737L2.94067 9.51609L2.21049 9.68737ZM2.21049 6.04933L2.94067 6.22061L2.21049 6.04933ZM12.5262 6.04934L13.2564 5.87806L12.5262 6.04934ZM12.5262 9.68737L13.2564 9.85864L12.5262 9.68737ZM9.18737 13.0262L9.01609 12.296L9.18737 13.0262ZM5.54934 13.0262L5.37806 13.7564L5.54934 13.0262ZM5.54933 2.71049L5.37806 1.9803L5.54933 2.71049ZM9.18737 2.71048L9.35864 1.9803L9.18737 2.71048ZM13.4697 15.0303C13.7626 15.3232 14.2374 15.3232 14.5303 15.0303C14.8232 14.7374 14.8232 14.2626 14.5303 13.9697L13.4697 15.0303ZM2.94067 9.51609C2.68644 8.43231 2.68644 7.3044 2.94067 6.22061L1.4803 5.87805C1.17323 7.18715 1.17323 8.54955 1.4803 9.85865L2.94067 9.51609ZM11.796 6.22061C12.0503 7.3044 12.0503 8.43231 11.796 9.51609L13.2564 9.85864C13.5635 8.54955 13.5635 7.18715 13.2564 5.87806L11.796 6.22061ZM9.01609 12.296C7.93231 12.5503 6.8044 12.5503 5.72061 12.296L5.37806 13.7564C6.68715 14.0635 8.04955 14.0635 9.35864 13.7564L9.01609 12.296ZM5.72061 3.44067C6.8044 3.18644 7.93231 3.18644 9.01609 3.44067L9.35864 1.9803C8.04955 1.67323 6.68715 1.67323 5.37806 1.9803L5.72061 3.44067ZM5.72061 12.296C4.34124 11.9725 3.26422 10.8955 2.94067 9.51609L1.4803 9.85865C1.93396 11.7927 3.44405 13.3027 5.37806 13.7564L5.72061 12.296ZM9.35864 13.7564C11.2927 13.3027 12.8027 11.7927 13.2564 9.85864L11.796 9.51609C11.4725 10.8955 10.3955 11.9725 9.01609 12.296L9.35864 13.7564ZM9.01609 3.44067C10.3955 3.76422 11.4725 4.84124 11.796 6.22061L13.2564 5.87806C12.8027 3.94405 11.2927 2.43396 9.35864 1.9803L9.01609 3.44067ZM5.37806 1.9803C3.44405 2.43396 1.93396 3.94405 1.4803 5.87805L2.94067 6.22061C3.26422 4.84124 4.34124 3.76422 5.72061 3.44067L5.37806 1.9803ZM11.0264 12.5871L13.4697 15.0303L14.5303 13.9697L12.0871 11.5264L11.0264 12.5871Z" | ||
fill="currentColor" | ||
/> | ||
</svg> | ||
), | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default } from './FormInput'; | ||
|
||
export type { FormTextInputProps, FormImageInputProps } from './FormInput'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { action } from '@storybook/addon-actions'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
|
||
import FormItem from './FormItem'; | ||
|
||
const meta: Meta<typeof FormItem.Image> = { | ||
component: FormItem.Image, | ||
decorators: [ | ||
function Decorator(Story, ctx) { | ||
const [previewImageUrl, setPreviewImageUrl] = useState(''); | ||
|
||
return ( | ||
<Story | ||
args={{ | ||
...ctx.args, | ||
previewImageUrl, | ||
onImageChange: (imageUrl) => { | ||
action('onImageChange')(imageUrl); | ||
setPreviewImageUrl(imageUrl); | ||
}, | ||
}} | ||
/> | ||
); | ||
}, | ||
], | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof FormItem.Image>; | ||
|
||
export const Main: Story = { | ||
args: { | ||
label: '이름', | ||
optional: false, | ||
errorMessage: '', | ||
caption: '', | ||
}, | ||
}; |
Oops, something went wrong.