Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"storybook": "8.5.3",
"typescript": "5.7.3",
"typescript-eslint": "8.23.0",
"vite": "6.1.0",
"vite": "6.0.11",
"vitest": "3.0.5"
}
}
2 changes: 1 addition & 1 deletion src/common/api/__tests__/useGetUserTokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('useGetTokens', () => {
getItemSpy.mockReturnValue(null);
// use a specific wrapper to avoid test side effects from "AuthProvider"
const { result } = renderHook(() => useGetUserTokens(), { wrapper: WithQueryClientProvider });
await waitFor(() => expect(result.current.isError).toBe(true));
await waitFor(() => expect(result.current.isError).toBe(true), { timeout: 2000 });

// ASSERT
expect(result.current.error).toBeInstanceOf(Error);
Expand Down
29 changes: 14 additions & 15 deletions src/common/components/Form/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InputHTMLAttributes } from 'react';
import { useFormContext } from 'react-hook-form';
import { Control, FieldValues, Path, useController } from 'react-hook-form';

import { cn } from 'common/utils/css';
import { PropsWithTestId } from 'common/utils/types';
Expand All @@ -13,9 +13,12 @@ import { PropsWithTestId } from 'common/utils/types';
* @see {@link PropsWithTestId}
* @see {@link InputHTMLAttributes}
*/
export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, PropsWithTestId {
export interface InputProps<T extends FieldValues>
extends InputHTMLAttributes<HTMLInputElement>,
PropsWithTestId {
control: Control<T>;
label?: string;
name: string;
name: Path<T>;
supportingText?: string;
}

Expand All @@ -25,20 +28,16 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement>, Props
* @param {InputProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const Input = ({
const Input = <T extends FieldValues>({
className,
control,
label,
name,
supportingText,
testId = 'input',
...props
}: InputProps): JSX.Element => {
const {
formState: { errors },
register,
} = useFormContext();
const hasError = !!errors[name];
const errorMessage: string = errors[name]?.message as string;
}: InputProps<T>): JSX.Element => {
const { field, fieldState } = useController({ control, name });
const isDisabled = props.disabled || props.readOnly;

return (
Expand All @@ -55,21 +54,21 @@ const Input = ({
<input
id={props.id || name}
{...props}
{...register(name)}
{...field}
className={cn(
'mb-1 block w-full border-b border-neutral-500/50 bg-transparent py-0.5 focus:border-blue-600 focus-visible:outline-none',
{
'!border-red-600': hasError,
'!border-red-600': fieldState.error,
},
{
'opacity-50': isDisabled,
},
)}
data-testid={`${testId}-input`}
/>
{hasError && (
{fieldState.error && (
<div className="me-1 inline text-sm text-red-600" data-testid={`${testId}-error`}>
{errorMessage}
{fieldState.error.message}
</div>
)}
{!!supportingText && (
Expand Down
29 changes: 14 additions & 15 deletions src/common/components/Form/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { InputHTMLAttributes, PropsWithChildren } from 'react';
import { useFormContext } from 'react-hook-form';
import { Control, FieldValues, Path, useController } from 'react-hook-form';

import { cn } from 'common/utils/css';
import { PropsWithTestId } from 'common/utils/types';

/**
* Properties for the `Select` component.
* @param {Control} control - Object containing methods for registering components
* into React Hook Form.
* @param {string} [label] - Optional. The text to display. If omitted, the
* `value` is displayed.
* @param {string} name - Name of the form control.
Expand All @@ -14,12 +16,13 @@ import { PropsWithTestId } from 'common/utils/types';
* @see {@link PropsWithChildren}
* @see {@link InputHTMLAttributes}
*/
interface SelectProps
export interface SelectProps<T extends FieldValues>
extends PropsWithTestId,
PropsWithChildren,
InputHTMLAttributes<HTMLSelectElement> {
control: Control<T>;
label?: string;
name: string;
name: Path<T>;
supportingText?: string;
}

Expand All @@ -31,21 +34,17 @@ interface SelectProps
* @param {SelectProps} props - Component properties.
* @returns JSX
*/
const Select = ({
const Select = <T extends FieldValues>({
children,
className,
control,
label,
name,
supportingText,
testId = 'select',
...props
}: SelectProps): JSX.Element => {
const {
formState: { errors },
register,
} = useFormContext();
const hasError = !!errors[name];
const errorMessage: string = errors[name]?.message as string;
}: SelectProps<T>): JSX.Element => {
const { field, fieldState } = useController({ control, name });
const isDisabled = props.disabled || props.readOnly;

return (
Expand All @@ -62,11 +61,11 @@ const Select = ({
<select
id={props.id || name}
{...props}
{...register(name)}
{...field}
className={cn(
'mb-1 block w-full border-b border-neutral-500/50 bg-transparent py-0.5 focus:border-blue-600',
{
'!border-red-600': hasError,
'!border-red-600': fieldState.error,
},
{
'opacity-50': isDisabled,
Expand All @@ -76,9 +75,9 @@ const Select = ({
>
{children}
</select>
{hasError && (
{fieldState.error && (
<div className="me-1 inline text-sm text-red-600" data-testid={`${testId}-error`}>
{errorMessage}
{fieldState.error.message}
</div>
)}
{!!supportingText && (
Expand Down
38 changes: 23 additions & 15 deletions src/common/components/Form/__stories__/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { FieldValues, useForm } from 'react-hook-form';

import Input from '../Input';
import { default as MyInput } from '../Input';
import { InputProps } from '../Input';

/**
* A wrapper for the `Input` component. Provides the RHF form `control`
* to the `Input` component.
*/
const Input = (props: Omit<InputProps<FieldValues>, 'control'>) => {
const form = useForm();

const onSubmit = () => {};

return (
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)}>
<MyInput control={form.control} {...props} />
</form>
);
};

const meta = {
title: 'Common/Form/Input',
component: Input,
decorators: [
(Story) => {
const formMethods = useForm({ defaultValues: { color: '' } });
return (
<FormProvider {...formMethods}>
<form className="w-80">
<Story />
</form>
</FormProvider>
);
},
],
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
className: { description: 'Additional CSS classes.' },
control: {
description: 'Object containing methods for registering components into React Hook Form.',
},
label: { description: 'The field label.' },
name: { description: 'The form field name.' },
supportingText: { description: 'Additional field instructions.' },
testId: { description: 'The test identifier.' },
},
args: {},
} satisfies Meta<typeof Input>;
} satisfies Meta<typeof MyInput>;

export default meta;

Expand Down
42 changes: 25 additions & 17 deletions src/common/components/Form/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { FieldValues, useForm } from 'react-hook-form';

import Select from '../Select';
import { default as MySelect } from '../Select';
import { SelectProps } from '../Select';

/**
* A wrapper for the `Select` component. Provides the RHF form `control`
* to the `Select` component.
*/
const Select = (props: Omit<SelectProps<FieldValues>, 'control'>) => {
const form = useForm();

const onSubmit = () => {};

return (
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)}>
<MySelect control={form.control} {...props}></MySelect>
</form>
);
};

const meta = {
title: 'Common/Form/Select',
component: Select,
decorators: [
(Story) => {
const formMethods = useForm({ defaultValues: { color: 'blue' } });
return (
<FormProvider {...formMethods}>
<form className="w-80">
<Story />
</form>
</FormProvider>
);
},
],
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
className: { description: 'Additional CSS classes.' },
control: {
description: 'Object containing methods for registering components into React Hook Form.',
},
label: { description: 'The field label.' },
name: { description: 'The form field name.' },
supportingText: { description: 'Additional field instructions.' },
testId: { description: 'The test identifier.' },
},
args: {},
} satisfies Meta<typeof Select>;
} satisfies Meta<typeof MySelect>;

export default meta;

Expand All @@ -56,7 +64,7 @@ export const WithSupportingText: Story = {
export const WithLabel: Story = {
args: {
children: options,
name: 'myField',
label: 'Fruit',
name: 'color',
label: 'Color',
},
};
Loading