Skip to content

Commit

Permalink
Merge pull request #25 from Central-MakeUs/feat-18
Browse files Browse the repository at this point in the history
feat: Input TextField
  • Loading branch information
ptq124 authored Jan 19, 2025
2 parents 089b6b4 + 22f41b5 commit a1a5739
Show file tree
Hide file tree
Showing 18 changed files with 592 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Preview } from "@storybook/react";
import type { Preview } from '@storybook/react';
import '../src/reset.css.ts';

const preview: Preview = {
parameters: {
Expand Down
17 changes: 17 additions & 0 deletions src/ui/form/form-error-message.styles.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { style } from '@vanilla-extract/css';
import { globalVars } from '../theme.css.ts';
import { typography } from '../typography.css.ts';

export const formErrorMessageBase = style([
typography('body_5_12_r'),
{
color: globalVars.color.redDanger,
},
]);

export const formErrorMessageContainer = style({
display: 'flex',
gap: 4,
padding: '1.5px 0',
alignItems: 'center',
});
27 changes: 27 additions & 0 deletions src/ui/form/form-error-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type ComponentProps } from 'react';
import { ErrorCir } from '../../assets/index.ts';
import { cx } from '../util.ts';
import * as styles from './form-error-message.styles.css.ts';
import { useFormField } from './form-field.tsx';

export type FormErrorMessageProps = ComponentProps<'p'>;

export const FormErrorMessage = (props: FormErrorMessageProps) => {
const { children, className, ...restProps } = props;

const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;

if (!body) {
return null;
}

return (
<div id={formMessageId} className={styles.formErrorMessageContainer}>
<ErrorCir />
<p {...restProps} className={cx(styles.formErrorMessageBase, className)}>
{body}
</p>
</div>
);
};
66 changes: 66 additions & 0 deletions src/ui/form/form-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createContext, useContext } from 'react';
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form';

export const Form = FormProvider;

type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};

const FormFieldContext = createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);

export const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};

type FormItemContextValue = {
id: string;
};

export const FormItemContext = createContext<FormItemContextValue>(
{} as FormItemContextValue,
);

export const useFormField = () => {
const fieldContext = useContext(FormFieldContext);
const itemContext = useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();

const fieldState = getFieldState(fieldContext.name, formState);

if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}

const { id } = itemContext;

return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
176 changes: 176 additions & 0 deletions src/ui/form/form-item.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { zodResolver } from '@hookform/resolvers/zod';
import type { Meta, StoryObj } from '@storybook/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { DeleteCir } from '../../assets/index.ts';
import { InputContainer } from '../input/input-container.tsx';
import { InputRightElement } from '../input/input-element.tsx';
import { Input } from '../input/input.tsx';
import { FormErrorMessage } from './form-error-message.tsx';
import { Form, FormField } from './form-field.tsx';
import { FormItem } from './form-item.tsx';
import { FormLabel } from './form-label.tsx';

const formSchema = z.object({
username: z.string().min(2, {
message: 'Username must be at least 2 characters.',
}),
});

const meta: Meta<typeof FormField> = {
title: 'ui/FormField',
component: FormField,
parameters: {
layout: 'centered',
},
argTypes: {},
tags: ['autodocs'],
decorators: [
(Story, context) => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
},
});

function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<Story
args={{
...context.args,
control: form.control as any,
}}
/>
{/*
// error State는 이렇게 관리, storybook에선 보여주기 힘듬
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>폼 라밸</FormLabel>
<InputContainer>
{
<Input
{...field}
variant={
form.formState.errors.username ? 'error' : 'default'
}
/>
}
{field.value && (
<InputRightElement>
<DeleteCir />
</InputRightElement>
)}
</InputContainer>
<FormErrorMessage />
</FormItem>
)}
/> */}
<button hidden />
</form>
</Form>
);
},
],
} satisfies Meta<typeof FormField>;

export default meta;
type Story = StoryObj<typeof meta>;

export const First: Story = {
render: (args) => (
<FormField
control={args.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>폼 라밸</FormLabel>
<InputContainer>
<Input {...field} />
{field.value && (
<InputRightElement>
<DeleteCir />
</InputRightElement>
)}
</InputContainer>
<FormErrorMessage />
</FormItem>
)}
/>
),
};

export const required: Story = {
render: (args) => (
<FormField
control={args.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel required>폼 라밸</FormLabel>
<InputContainer>
<Input {...field} />
{field.value && (
<InputRightElement>
<DeleteCir />
</InputRightElement>
)}
</InputContainer>
<FormErrorMessage />
</FormItem>
)}
/>
),
};

export const WithoutLabel: Story = {
render: (args) => (
<FormField
control={args.control}
name="username"
render={({ field }) => (
<FormItem>
<InputContainer>
<Input {...field} />
{field.value && (
<InputRightElement>
<DeleteCir />
</InputRightElement>
)}
</InputContainer>
<FormErrorMessage />
</FormItem>
)}
/>
),
};

export const disabledField: Story = {
render: (args) => (
<FormField
control={args.control}
name="username"
render={({ field }) => (
<FormItem>
<InputContainer>
<Input {...field} disabled />
{field.value && (
<InputRightElement>
<DeleteCir />
</InputRightElement>
)}
</InputContainer>
<FormErrorMessage />
</FormItem>
)}
/>
),
};
7 changes: 7 additions & 0 deletions src/ui/form/form-item.styles.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { style } from '@vanilla-extract/css';

export const formItemBase = style({
display: 'flex',
flexDirection: 'column',
gap: 6,
});
20 changes: 20 additions & 0 deletions src/ui/form/form-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type ComponentProps, useId } from 'react';
import { cx } from '../util.ts';
import { FormItemContext } from './form-field.tsx';
import * as styles from './form-item.styles.css.ts';

export type FormItemProps = ComponentProps<'div'>;

export const FormItem = (props: FormItemProps) => {
const { children, className, ...restProps } = props;

const id = useId();

return (
<FormItemContext.Provider value={{ id }}>
<div {...restProps} className={cx(styles.formItemBase, className)}>
{children}
</div>
</FormItemContext.Provider>
);
};
16 changes: 16 additions & 0 deletions src/ui/form/form-label.styles.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { style } from '@vanilla-extract/css';
import { globalVars } from '../theme.css.ts';
import { typography } from '../typography.css.ts';

export const formLabelBase = style([
typography('body_2_14_sb'),
{
color: globalVars.color.grey800,
display: 'flex',
gap: 4,
},
]);

export const requiredIcon = style({
color: globalVars.color.mainblue500,
});
24 changes: 24 additions & 0 deletions src/ui/form/form-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type ComponentProps } from 'react';
import { cx } from '../util.ts';
import { useFormField } from './form-field.tsx';
import * as styles from './form-label.styles.css.ts';

export type FormLabelProps = ComponentProps<'label'> & {
required?: boolean;
};

export const FormLabel = (props: FormLabelProps) => {
const { children, className, required, ...restProps } = props;

const { formItemId } = useFormField();
return (
<label
{...restProps}
htmlFor={formItemId}
className={cx(styles.formLabelBase, className)}
>
{children}
{required && <span className={styles.requiredIcon}>*</span>}
</label>
);
};
15 changes: 15 additions & 0 deletions src/ui/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export {
Form,
FormField,
FormItemContext,
useFormField,
} from './form-field.tsx';

export { FormLabel, type FormLabelProps } from './form-label.tsx';

export {
FormErrorMessage,
type FormErrorMessageProps,
} from './form-error-message.tsx';

export { FormItem, type FormItemProps } from './form-item.tsx';
9 changes: 9 additions & 0 deletions src/ui/input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
type InputContainerProps,
InputContainer,
} from './input-container.tsx';
export { Input, type InputProps } from './input.tsx';
export {
InputRightElement,
type InputRightElementProps,
} from './input-element.tsx';
5 changes: 5 additions & 0 deletions src/ui/input/input-container.styles.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { style } from '@vanilla-extract/css';

export const inputContainerBase = style({
position: 'relative',
});
Loading

0 comments on commit a1a5739

Please sign in to comment.