tremorlabs / tremor

React components to build charts and dashboards
https://tremor.so
Apache License 2.0
15.39k stars 446 forks source link

[Bug]: Error message is not showing properly with framer-motion #1023

Open galih56 opened 3 weeks ago

galih56 commented 3 weeks ago

Tremor Version

8.0.9

Link to minimal reproduction

https://codesandbox.io/p/devbox/zod-react-hook-formc-storybook-tremor-so-f6d8my

Steps to reproduce

I have zod, react-hook-form, and framer-motion installed

I'm using storybook for easier access of component implementation.

Can be seen below.

// Import necessary libraries
import React, { useState } from 'react';
import { StoryFn, Meta } from '@storybook/react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Select, SelectItem, TextInput } from '@tremor/react';
import { PhoneNumberInput } from '../elements/PhoneNumberInput';
import { motion } from 'framer-motion';
import './../../styles/index.css';

const regionOptions : string[] = [ "bpsum", "bpb", "bpjtg", "bpjtm", "bpkalsul","bpho" ];
const FormDataSchema = z.object({
    phoneNumber: z.string().min(1, 'Nomor Telephone harus diisi'),
    price: z.string().min(1, 'Harga harus diisi'),
    region: z.custom((value) => Boolean(regionOptions.filter(option => option === value).length), {
      message : 'Wilayah harus diisi'
    }),
  });

type Inputs = z.infer<typeof FormDataSchema>;

const steps = [
    {
      id: 'Step 1',
      name: 'Basic Information',
      fields: ['phoneNumber', 'price', 'region' ]
    },
    { id: 'Step 3', name: 'Complete' }
  ]
const MyForm = () => {
    const [previousStep, setPreviousStep] = useState(0);
    const [currentStep, setCurrentStep] = useState(0);
    const delta = currentStep - previousStep;

    // Initialize react-hook-form
    const  { register, handleSubmit, formState: { errors }, control, trigger, reset } = useForm<Inputs>({
        resolver: zodResolver(FormDataSchema)
    });

    const triggerFields = async () => {
        const fields = steps[currentStep].fields
        return await trigger(fields as FieldName[], { shouldFocus: true });
    }

    const processForm: SubmitHandler<Inputs> = async values => {
        console.log(values);
        // reset();
    }

    const next = async () => {
        const output = await triggerFields();

        if (!output) return;

        if (currentStep < steps.length - 1) {
          if (currentStep === steps.length - 2) {
            await handleSubmit(processForm)()
          }
          setPreviousStep(currentStep)
          setCurrentStep(step => step + 1)
        }
      }

      const prev = () => {
        if (currentStep > 0) {
          setPreviousStep(currentStep)
          setCurrentStep(step => step - 1)
        }
      }
    type FieldName = keyof Inputs
    // Function to handle form submission
    const onSubmit = (data: any) => {
        console.log(data); // Here you can do whatever you want with the form data
    };

    return (
        <div className="w-full">
            {/* steps */}
            <nav aria-label='Progress'>
                <ol role='list' className='space-y-4 md:flex md:space-x-8 md:space-y-0'>
                    {steps.map((step, index) => (
                    <li key={step.name} className='md:flex-1'>
                        {currentStep > index ? (
                        <div className='group flex w-full flex-col border-l-4 border-sky-600 py-2 pl-4 transition-colors md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4'>
                            <span className='text-sm font-medium text-sky-600 transition-colors '>
                            {step.id}
                            </span>
                            <span className='text-sm font-medium'>{step.name}</span>
                        </div>
                        ) : currentStep === index ? (
                        <div
                            className='flex w-full flex-col border-l-4 border-sky-600 py-2 pl-4 md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4'
                            aria-current='step'
                        >
                            <span className='text-sm font-medium text-sky-600'>
                            {step.id}
                            </span>
                            <span className='text-sm font-medium'>{step.name}</span>
                        </div>
                        ) : (
                        <div className='group flex w-full flex-col border-l-4 border-gray-200 py-2 pl-4 transition-colors md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4'>
                            <span className='text-sm font-medium text-gray-500 transition-colors'>
                            {step.id}
                            </span>
                            <span className='text-sm font-medium'>{step.name}</span>
                        </div>
                        )}
                    </li>
                    ))}
                </ol>
            </nav>

            <form onSubmit={handleSubmit(onSubmit)}>
            {currentStep === 0 && (
                <motion.div
                initial={{ x: delta >= 0 ? '50%' : '-50%', opacity: 0 }}
                animate={{ x: 0, opacity: 1 }}
                transition={{ duration: 0.3, ease: 'easeInOut' }}
                >
                <div className="mt-2">
                    <TextInput {...register('price', { required: 'Harga' })} error={!!errors.price} errorMessage={errors.price?.message}/>
                </div>
                <div className="mt-2">
                    <PhoneNumberInput control={control} {...register('phoneNumber', { required: 'Nomor Telephone' })} error={!!errors.phoneNumber} errorMessage={errors.phoneNumber?.message} />
                </div>
                <div className="mt-2">

                <label
                    htmlFor='region'
                    className='block text-sm font-medium leading-6 text-gray-900'
                    >
                    Wilayah
                    </label>
                    <Controller
                        name="region"
                        control={control}
                        render={({ field }) => (
                        <Select {...field} className='mt-2' id="region"
                            error={Boolean(errors.region?.message)}
                            errorMessage={errors.region?.message as string}
                            >
                            {regionOptions.map(item => (<SelectItem key={item} value={item}>{item.toUpperCase()}</SelectItem>))}
                        </Select>
                        )}
                    />
                </div>
                <div className="mt-2">
                    <Button type="submit">Submit</Button>
                </div>

                </motion.div>
            )}

                {currentStep === 1 && (
                    <>
                    <h2 className='text-base font-semibold leading-7 text-gray-900'>
                        Complete
                    </h2>
                    <p className='mt-1 text-sm leading-6 text-gray-600'>
                        Thank you for your submission.
                    </p>
                    </>
                )}
                {/* Navigation */}
                <div className='my-8 py-6'>
                    <div className='flex justify-between'>
                        <button type='button' onClick={prev}
                        className='rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50'>
                        <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth='1.5' stroke='currentColor' className='h-6 w-6'>
                            <path strokeLinecap='round' strokeLinejoin='round' d='M15.75 19.5L8.25 12l7.5-7.5'/>
                        </svg>
                        </button> 
                        <button type='button' onClick={next} disabled={currentStep === steps.length - 1} className='rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50'>
                        <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth='1.5' stroke='currentColor' className='h-6 w-6'>
                            <path strokeLinecap='round' strokeLinejoin='round' d='M8.25 4.5l7.5 7.5-7.5 7.5' />
                        </svg>
                        </button>
                    </div>
                </div>
            </form>
        </div>
    );
};

// Define your Storybook story
export default {
  title: 'Form',
  component: MyForm,
};

const Template: StoryFn = () => <MyForm />;

// Export the story
export const Default = Template.bind({});

What is expected?

Correct appearance On Focus image

On change image

What is actually happening?

When the errors are triggered, error messages are displayed properly. But when i change the inputs, They must hide the error messages immediately. But the error messages keeps showing instead.

first error trigger image

Input on focus image

Input on change image

As you can see, the text color and the border appers different when i use motion.div. I notice this issue when i tried to remove the motion.div temporary.

This issue occurs on all kind of tremor Input components

What browsers are you seeing the problem on?

Chrome, Microsoft Edge, Safari, Firefox, Brave

Any additional comments?

Thank you for the great UI Library :)