Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/docs/src/remix-hook-form/text-field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { useRef } from 'react';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { expect, userEvent } from 'storybook/test';
Expand Down Expand Up @@ -38,6 +39,8 @@ const ControlledTextFieldExample = () => {
},
});

const ref = useRef<HTMLInputElement>(null);

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
Expand All @@ -58,6 +61,12 @@ const ControlledTextFieldExample = () => {
suffix="cm"
/>

<div className="flex gap-2 items-end">
<TextField name="refExample" label="Ref Example" placeholder="Enter something, this has a ref" ref={ref} />

<Button onClick={() => ref.current?.focus()}>Focus the input</Button>
</div>

<Button type="submit" className="mt-4">
Submit
</Button>
Expand Down
11 changes: 7 additions & 4 deletions packages/components/src/remix-hook-form/text-field.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import { TextField as BaseTextField, type TextInputProps as BaseTextFieldProps } from '../ui/text-field';
import { FormControl, FormDescription, FormLabel, FormMessage } from './form';

import { useRemixFormContext } from 'remix-hook-form';

export type TextFieldProps = Omit<BaseTextFieldProps, 'control'>;

export function TextField(props: TextFieldProps) {
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>((props, ref) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of using forwardRef let's use ref for React 19 patterns

const { control } = useRemixFormContext();

// Merge the provided components with the default form components
Expand All @@ -15,11 +16,13 @@ export function TextField(props: TextFieldProps) {
FormDescription,
FormMessage,
};

const components = {
...defaultComponents,
...props.components,
};

return <BaseTextField control={control} components={components} {...props} />;
}
return <BaseTextField ref={ref} control={control} components={components} {...props} />;
});

TextField.displayName = 'RemixTextField';
99 changes: 46 additions & 53 deletions packages/components/src/ui/text-field.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as React from 'react';
import * as React from 'react';
import type { Control, FieldPath, FieldValues } from 'react-hook-form';
import {
type FieldComponents,
Expand Down Expand Up @@ -57,64 +57,57 @@ export interface TextInputProps extends Omit<InputProps, 'prefix' | 'suffix'> {
label?: string;
description?: string;
components?: Partial<FieldComponents> & {
Input?: React.ComponentType<InputProps>;
Input?: React.ComponentType<InputProps & React.RefAttributes<HTMLInputElement>>;
};
prefix?: React.ReactNode;
suffix?: React.ReactNode;
className?: string;
}

export const TextField = ({
control,
name,
label,
description,
className,
components,
prefix,
suffix,
...props
}: TextInputProps) => {
// Use the custom Input component if provided, otherwise use the default TextInput
const InputComponent = components?.Input || TextInput;
export const TextField = React.forwardRef<HTMLInputElement, TextInputProps>(
({ control, name, label, description, className, components, prefix, suffix, ...props }, ref) => {
// Use the custom Input component if provided, otherwise use the default TextInput

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding forward ref, can we pass in ref as a prop for React 19 patterns?

const InputComponent = components?.Input || TextInput;

return (
<FormField
control={control}
name={name}
render={({ field, fieldState }) => {
return (
<FormItem className={className}>
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
<div
className={cn('flex group transition-all duration-200 rounded-md', {
'field__input--with-prefix': prefix,
'field__input--with-suffix': suffix,
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background': true,
})}
>
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
<FormControl Component={components?.FormControl}>
<InputComponent
{...field}
{...props}
className={cn('focus-visible:ring-0 focus-visible:ring-offset-0 border-input', {
'rounded-l-none border-l-0': prefix,
'rounded-r-none border-r-0': suffix,
})}
/>
</FormControl>
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
</div>
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
{fieldState.error && (
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
)}
</FormItem>
);
}}
/>
);
};
return (
<FormField
control={control}
name={name}
render={({ field, fieldState }) => {
return (
<FormItem className={className}>
{label && <FormLabel Component={components?.FormLabel}>{label}</FormLabel>}
<div
className={cn('flex group transition-all duration-200 rounded-md', {
'field__input--with-prefix': prefix,
'field__input--with-suffix': suffix,
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background': true,
})}
>
{prefix && <FieldPrefix>{prefix}</FieldPrefix>}
<FormControl Component={components?.FormControl}>
<InputComponent
{...field}
{...props}
ref={ref}
className={cn('focus-visible:ring-0 focus-visible:ring-offset-0 border-input', {
'rounded-l-none border-l-0': prefix,
'rounded-r-none border-r-0': suffix,
})}
/>
</FormControl>
{suffix && <FieldSuffix>{suffix}</FieldSuffix>}
</div>
{description && <FormDescription Component={components?.FormDescription}>{description}</FormDescription>}
{fieldState.error && (
<FormMessage Component={components?.FormMessage}>{fieldState.error.message}</FormMessage>
)}
</FormItem>
);
}}
/>
);
},
);

TextField.displayName = 'TextField';
8 changes: 5 additions & 3 deletions packages/components/src/ui/text-input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type * as React from 'react';
import * as React from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line could still be "type" right?

import { cn } from './utils';

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

function TextInput({ className, type, ...props }: InputProps) {
const TextInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lcmohsen instead of using forward Ref, can we follow React 19's patterns for passing in refs?

return (
<input
ref={ref}
type={type}
className={cn(
'flex h-10 w-full text-base sm:text-sm rounded-md border border-input bg-background px-3 py-2 ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
Expand All @@ -15,6 +16,7 @@ function TextInput({ className, type, ...props }: InputProps) {
{...props}
/>
);
}
});
TextInput.displayName = 'TextInput';

export { TextInput };