diff --git a/frontend/app/components/input-error.tsx b/frontend/app/components/input-error.tsx new file mode 100644 index 00000000..57943da2 --- /dev/null +++ b/frontend/app/components/input-error.tsx @@ -0,0 +1,22 @@ +import type { ComponentProps, ReactNode } from 'react'; + +import { cn } from '~/utils/tailwind-utils'; + +export interface InputErrorProps extends ComponentProps<'span'> { + children: ReactNode; + id: string; +} + +export function InputError(props: InputErrorProps) { + const { children, className, ...restProps } = props; + return ( + + {children} + + ); +} diff --git a/frontend/app/components/input-field.tsx b/frontend/app/components/input-field.tsx new file mode 100644 index 00000000..826d3d2d --- /dev/null +++ b/frontend/app/components/input-field.tsx @@ -0,0 +1,116 @@ +import { forwardRef } from 'react'; + +import { InputError } from './input-error'; +import { InputHelp } from './input-help'; + +import { InputLabel } from '~/components/input-label'; +import { cn } from '~/utils/tailwind-utils'; + +const inputBaseClassName = + 'block rounded-lg border-gray-500 focus:border-blue-500 focus:outline-none focus:ring focus:ring-blue-500'; +const inputDisabledClassName = + 'disabled:bg-gray-100 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70'; +const inputReadOnlyClassName = + 'read-only:bg-gray-100 read-only:pointer-events-none read-only:cursor-not-allowed read-only:opacity-70'; +const inputErrorClassName = 'border-red-500 focus:border-red-500 focus:ring-red-500'; + +export interface InputFieldProps { + ariaDescribedby?: string; + className?: string; + errorMessage?: string; + helpMessagePrimary?: React.ReactNode; + helpMessagePrimaryClassName?: string; + helpMessageSecondary?: React.ReactNode; + helpMessageSecondaryClassName?: string; + id: string; + label: string; + name: string; + required?: boolean; + type?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url'; +} + +const InputField = forwardRef((props, ref) => { + const { + ariaDescribedby, + errorMessage, + className, + helpMessagePrimary, + helpMessagePrimaryClassName, + helpMessageSecondary, + helpMessageSecondaryClassName, + id, + label, + required, + type = 'text', + ...restInputProps + } = props; + + const inputErrorId = `input-${id}-error`; + const inputHelpMessagePrimaryId = `input-${id}-help-primary`; + const inputHelpMessageSecondaryId = `input-${id}-help-secondary`; + const inputLabelId = `input-${id}-label`; + const inputWrapperId = `input-${id}`; + + function getAriaDescribedby() { + const describedby = []; + if (ariaDescribedby) describedby.push(ariaDescribedby); + if (helpMessagePrimary) describedby.push(inputHelpMessagePrimaryId); + if (helpMessageSecondary) describedby.push(inputHelpMessageSecondaryId); + return describedby.length > 0 ? describedby.join(' ') : undefined; + } + + return ( +
+ + {label} + + {errorMessage && ( +

+ {errorMessage} +

+ )} + {helpMessagePrimary && ( + + {helpMessagePrimary} + + )} + + {helpMessageSecondary && ( + + {helpMessageSecondary} + + )} +
+ ); +}); + +InputField.displayName = 'InputField'; + +export { InputField }; diff --git a/frontend/app/components/input-help.tsx b/frontend/app/components/input-help.tsx new file mode 100644 index 00000000..906b9c4d --- /dev/null +++ b/frontend/app/components/input-help.tsx @@ -0,0 +1,17 @@ +import type { ComponentProps, ReactNode } from 'react'; + +import { cn } from '~/utils/tailwind-utils'; + +export interface InputHelpProps extends ComponentProps<'span'> { + children: ReactNode; + id: string; +} + +export function InputHelp(props: InputHelpProps) { + const { children, className, ...restProps } = props; + return ( + + {children} + + ); +} diff --git a/frontend/app/components/input-label.tsx b/frontend/app/components/input-label.tsx new file mode 100644 index 00000000..572e8e74 --- /dev/null +++ b/frontend/app/components/input-label.tsx @@ -0,0 +1,18 @@ +import type { ComponentProps, ReactNode } from 'react'; + +import { cn } from '~/utils/tailwind-utils'; + +export interface InputLabelProps extends ComponentProps<'label'> { + children: ReactNode; + id: string; +} + +export function InputLabel(props: InputLabelProps) { + const { children, className, ...restProps } = props; + + return ( + + ); +}