Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
56 changes: 40 additions & 16 deletions apps/docs/src/remix-hook-form/textarea.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Textarea } from '@lambdacurry/forms/remix-hook-form/textarea';
import { Button } from '@lambdacurry/forms/ui/button';
import type { Meta, StoryObj } from '@storybook/react-vite';
import type { Meta, StoryContext, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, within } from '@storybook/test';
import { type ActionFunctionArgs, useFetcher } from 'react-router';
import { RemixFormProvider, createFormData, getValidatedFormData, useRemixForm } from 'remix-hook-form';
Expand Down Expand Up @@ -49,6 +49,7 @@ const ControlledTextareaExample = () => {
<Button type="submit" className="mt-4">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
{fetcher.data?.submittedMessage && (
<div className="mt-4">
<p className="text-sm font-medium">Submitted message:</p>
Expand Down Expand Up @@ -92,6 +93,41 @@ const meta: Meta<typeof Textarea> = {
export default meta;
type Story = StoryObj<typeof meta>;

// Test scenarios
const testInvalidSubmission = async ({ canvas }: StoryContext) => {
const messageInput = canvas.getByLabelText('Your message');
const submitButton = canvas.getByRole('button', { name: 'Submit' });

// Clear the textarea and enter text that's too short
await userEvent.click(messageInput);
await userEvent.clear(messageInput);
await userEvent.type(messageInput, 'Short');
await userEvent.click(submitButton);

// Check for validation error
await expect(await canvas.findByText('Message must be at least 10 characters')).toBeInTheDocument();
};

const testValidSubmission = async ({ canvas }: StoryContext) => {
const messageInput = canvas.getByLabelText('Your message');
const submitButton = canvas.getByRole('button', { name: 'Submit' });

// Clear and enter valid text
await userEvent.click(messageInput);
await userEvent.clear(messageInput);
await userEvent.type(messageInput, 'This is a test message that is longer than 10 characters.');
await userEvent.click(submitButton);

// Check for success message
const successMessage = await canvas.findByText('Message submitted successfully');
expect(successMessage).toBeInTheDocument();

// Check if the submitted message is displayed
await expect(
await canvas.findByText('This is a test message that is longer than 10 characters.'),
).toBeInTheDocument();
};

export const Default: Story = {
parameters: {
docs: {
Expand All @@ -100,20 +136,8 @@ export const Default: Story = {
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Enter text
const messageInput = canvas.getByLabelText('Your message');
await userEvent.type(messageInput, 'This is a test message that is longer than 10 characters.');

// Submit the form
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);

// Check if the submitted message is displayed
await expect(
await canvas.findByText('This is a test message that is longer than 10 characters.'),
).toBeInTheDocument();
play: async (storyContext) => {
await testInvalidSubmission(storyContext);
await testValidSubmission(storyContext);
},
};
12 changes: 7 additions & 5 deletions packages/components/src/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import type { ButtonHTMLAttributes } from 'react';
import React, { type ButtonHTMLAttributes } from 'react';
import { cn } from './utils';

const buttonVariants = cva(
Expand Down Expand Up @@ -35,10 +35,12 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, Va
asChild?: boolean;
}

export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} data-slot="button" {...props} />;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp ref={ref} className={cn(buttonVariants({ variant, size, className }))} data-slot="button" {...props} />;
}
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Button.displayName = 'Button';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ type ControlFunctions = {
isPending: () => boolean;
};

export type DebouncedState<T extends (...args: any) => ReturnType<T>> = ((
export type DebouncedState<T extends (...args: any) => any> = ((
...args: Parameters<T>
) => ReturnType<T> | undefined) &
ControlFunctions;

export function useDebounceCallback<T extends (...args: any) => ReturnType<T>>(
export function useDebounceCallback<T extends (...args: any) => any>(
func: T,
delay = 500,
options?: DebounceOptions,
): DebouncedState<T> {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const debouncedFunc = useRef<ReturnType<typeof debounce>>(null);
const debouncedFunc = useRef<ReturnType<typeof debounce> | null>(null);

useUnmount(() => {
if (debouncedFunc.current) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
lastArgs = null;
lastThis = null;
lastInvokeTime = time;
result = func.apply(thisArg, args);
result = func.apply(thisArg, args) as ReturnType<T>;
return result;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/ui/debounced-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function DebouncedInput({
// Define the debounced function with useCallback
// biome-ignore lint/correctness/useExhaustiveDependencies: from Bazza UI
const debouncedOnChange = useCallback(
debounce((newValue: string | number) => {
debounce((...args: unknown[]) => {
const newValue = args[0] as string | number;
onChange(newValue);
Comment on lines +25 to 27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider a more type-safe approach to maintain flexibility without losing compile-time safety.

The change from a typed parameter to rest parameters with casting reduces type safety. While this makes the debounce utility more generic, the runtime cast args[0] as string | number could fail if unexpected argument types are passed.

Consider maintaining type safety while preserving flexibility:

-    debounce((...args: unknown[]) => {
-      const newValue = args[0] as string | number;
+    debounce((newValue: string | number) => {
       onChange(newValue);
     }, debounceMs),

If generic flexibility is required, consider using a proper generic constraint in the debounce utility instead of runtime casting.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
debounce((...args: unknown[]) => {
const newValue = args[0] as string | number;
onChange(newValue);
debounce((newValue: string | number) => {
onChange(newValue);
}, debounceMs),
🤖 Prompt for AI Agents
In packages/components/src/ui/debounced-input.tsx around lines 25 to 27, the
current use of rest parameters with a runtime cast to string | number reduces
type safety and risks runtime errors. To fix this, update the debounce function
to use a generic type parameter that enforces the expected argument type at
compile time. Replace the rest parameter and casting with a properly typed
single parameter matching the generic constraint, ensuring onChange receives a
correctly typed value without unsafe casting.

}, debounceMs), // Pass the wait time here
[debounceMs, onChange], // Dependencies
Expand Down
31 changes: 17 additions & 14 deletions packages/components/src/ui/textarea.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 { cn } from './utils';

export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
Expand All @@ -7,20 +7,23 @@ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextArea
>;
}

const Textarea = ({ className, CustomTextarea, ...props }: TextareaProps) => {
if (CustomTextarea) return <CustomTextarea className={className} {...props} />;
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, CustomTextarea, ...props }, ref) => {
if (CustomTextarea) return <CustomTextarea ref={ref} className={className} {...props} />;

return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm',
className,
)}
{...props}
data-slot="textarea"
/>
);
};
return (
<textarea
ref={ref}
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm',
className,
)}
{...props}
data-slot="textarea"
/>
);
}
);
Textarea.displayName = 'Textarea';

export { Textarea };
Loading