Skip to content

Commit

Permalink
feat: add FormItem
Browse files Browse the repository at this point in the history
  • Loading branch information
aube-dev committed Sep 27, 2024
1 parent 8917f71 commit 2d9cc30
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 147 deletions.
23 changes: 20 additions & 3 deletions app/components/common/FormInput/FormImageInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
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;
Expand All @@ -14,7 +33,5 @@ type Story = StoryObj<typeof FormInput.Image>;
export const Main: Story = {
args: {
isError: false,
multiple: false,
onImageChange: fn(),
},
};
13 changes: 6 additions & 7 deletions app/components/common/FormInput/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
useRef,
useState,
type DetailedHTMLProps,
type InputHTMLAttributes,
} from 'react';
Expand All @@ -16,7 +15,7 @@ interface FormInputPropsBase
isError?: boolean;
}

interface FormTextInputProps extends FormInputPropsBase {
export interface FormTextInputProps extends FormInputPropsBase {
Icon?: (props: { className?: string }) => JSX.Element;
onTextChange?: (newText: string) => void;
}
Expand Down Expand Up @@ -47,19 +46,20 @@ const FormTextInput = ({
</div>
);

interface FormImageInputProps extends FormInputPropsBase {
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);
const [previewImageUrl, setPreviewImageUrl] = useState('');

return (
<button
Expand All @@ -71,7 +71,7 @@ const FormImageInput = ({
inputElement.current?.click();
}}
>
{previewImageUrl.length === 0 && (
{previewImageUrl?.length === 0 && (
<svg
width="24"
height="24"
Expand Down Expand Up @@ -107,13 +107,12 @@ const FormImageInput = ({
if (files && files[0]) {
const urls = await getDataURLFromFiles(files[0]);
onImageChange?.(urls[0]);
setPreviewImageUrl(urls[0]);
}

e.currentTarget.value = '';
}}
/>
{previewImageUrl.length > 0 && (
{previewImageUrl && previewImageUrl.length > 0 && (
<img
src={previewImageUrl}
alt="preview before upload"
Expand Down
4 changes: 3 additions & 1 deletion app/components/common/FormInput/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default as FormInput } from './FormInput';
export { default } from './FormInput';

export type { FormTextInputProps, FormImageInputProps } from './FormInput';
40 changes: 40 additions & 0 deletions app/components/common/FormItem/FormImageItem.stories.tsx
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: '',
},
};
48 changes: 48 additions & 0 deletions app/components/common/FormItem/FormItem.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { style } from '@vanilla-extract/css';
import { Breakpoint, textStyleInfo } from '@/constants/style';
import { textStyle } from '@/styles/text.css';
import { themeVars } from '@/styles/theme.css';
import { getMediaQuery } from '@/utils/style';

export const container = style({
display: 'flex',
flexDirection: 'column',
gap: '12px',
});

export const titleArea = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '4px',
});

export const optionalText = style([
textStyle.body2SB,
{
color: themeVars.color.grayscale.gray5.hex,
},
]);

export const inputArea = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
});

export const caption = style([
textStyle.body2R,
{
display: 'block',
minHeight: `${textStyleInfo.body2R.pc.lineHeight}px`,
'@media': {
[getMediaQuery([Breakpoint.MOBILE2, Breakpoint.MOBILE1])]: {
minHeight: `${textStyleInfo.body2R.mobile.lineHeight}px`,
},
},
},
]);

export const errorText = style({
color: themeVars.color.system.caution.hex,
});
88 changes: 88 additions & 0 deletions app/components/common/FormItem/FormItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { type ReactNode } from 'react';
import * as styles from './FormItem.css';
import FormInput, {
type FormImageInputProps,
type FormTextInputProps,
} from '@/components/common/FormInput';
import { textStyle } from '@/styles/text.css';
import { className } from '@/utils/style';

interface FormItemPropsBase {
label: string;
optional?: boolean;
caption?: string;
errorMessage?: string;
}

interface FormTextItemProps
extends FormItemPropsBase,
Omit<FormTextInputProps, 'isError'> {}

interface FormImageItemProps
extends FormItemPropsBase,
Omit<FormImageInputProps, 'isError'> {}

const FormItemLayout = ({
label,
optional,
caption,
errorMessage,
children,
}: FormItemPropsBase & { children: ReactNode }) => (
<div className={styles.container}>
<div className={styles.titleArea}>
<span className={textStyle.subtitle2SB}>{label}</span>
{optional && <span className={styles.optionalText}>(선택)</span>}
</div>
{children}
<span
className={className(
styles.caption,
errorMessage ? styles.errorText : undefined,
)}
>
{errorMessage ? errorMessage : caption}
</span>
</div>
);

const FormTextItem = ({
label,
optional,
caption,
errorMessage,
...inputProps
}: FormTextItemProps) => (
<FormItemLayout
label={label}
optional={optional}
caption={caption}
errorMessage={errorMessage}
>
<FormInput.Text {...inputProps} isError={!!errorMessage} />
</FormItemLayout>
);

const FormImageItem = ({
label,
optional,
caption,
errorMessage,
...inputProps
}: FormImageItemProps) => (
<FormItemLayout
label={label}
optional={optional}
caption={caption}
errorMessage={errorMessage}
>
<FormInput.Image {...inputProps} isError={!!errorMessage} />
</FormItemLayout>
);

const FormItem = {
Text: FormTextItem,
Image: FormImageItem,
};

export default FormItem;
41 changes: 41 additions & 0 deletions app/components/common/FormItem/FormTextItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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.Text> = {
component: FormItem.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 FormItem.Text>;

export const Main: Story = {
args: {
label: '이름',
optional: false,
placeholder: '텍스트를 입력해주세요.',
errorMessage: '',
caption: '',
},
};
1 change: 1 addition & 0 deletions app/components/common/FormItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './FormItem';
Loading

0 comments on commit 2d9cc30

Please sign in to comment.