-
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.
Merge pull request #25 from Central-MakeUs/feat-18
feat: Input TextField
- Loading branch information
Showing
18 changed files
with
592 additions
and
1 deletion.
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
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,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', | ||
}); |
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,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> | ||
); | ||
}; |
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,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, | ||
}; | ||
}; |
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,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> | ||
)} | ||
/> | ||
), | ||
}; |
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,7 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const formItemBase = style({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 6, | ||
}); |
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,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> | ||
); | ||
}; |
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,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, | ||
}); |
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,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> | ||
); | ||
}; |
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,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'; |
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,9 @@ | ||
export { | ||
type InputContainerProps, | ||
InputContainer, | ||
} from './input-container.tsx'; | ||
export { Input, type InputProps } from './input.tsx'; | ||
export { | ||
InputRightElement, | ||
type InputRightElementProps, | ||
} from './input-element.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,5 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const inputContainerBase = style({ | ||
position: 'relative', | ||
}); |
Oops, something went wrong.