leoreisdias / react-currency-mask

React Input library to handle user entries applying currency masks - BRL currency supported!
MIT License
19 stars 5 forks source link

Currency Mask Focus Not Working With Shadcn Dialog + RHF #9

Open KevinGuedes opened 1 month ago

KevinGuedes commented 1 month ago

Description

By default, Shadcn's Dialog component will focus on the first focusable element once it is opened. When the Currency Mask input is the first focusable element of the dialog, the value and defaultValue are both ignored:

issue-currency-mask

Cause

When handleFocuns is called, the value of input.targer.value is 0

Demo Component

import { zodResolver } from '@hookform/resolvers/zod'
import { CurrencyInput } from 'react-currency-mask'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'

import { Input } from './components/ui/input'

const formSchema = z.object({
  value: z.number().min(0).positive(),
})

export function App() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      value: 30,
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <main>
      <Dialog>
        <DialogTrigger asChild>
          <Button>Open Form</Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Are you absolutely sure?</DialogTitle>
            <DialogDescription>
              This action cannot be undone. This will permanently delete your
              account and remove your data from our servers.
            </DialogDescription>
          </DialogHeader>
          <Form {...form}>
            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
              <FormField
                control={form.control}
                name="value"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Value</FormLabel>
                    <FormControl>
                      <CurrencyInput
                        value={field.value}
                        defaultValue={10}
                        onChangeValue={(_, value) => {
                          field.onChange(value)
                        }}
                        onFocus={(_, original, masked) =>
                          console.log({ original, masked })
                        }
                        InputElement={
                          <Input placeholder="R$ 0,00" inputMode="numeric" />
                        }
                      />
                    </FormControl>
                    <FormDescription>
                      This is your public display name.
                    </FormDescription>
                    <FormMessage />
                  </FormItem>
                )}
              />
              <Button type="submit">Submit</Button>
            </form>
          </Form>
        </DialogContent>
      </Dialog>
    </main>
  )
}

Solutions

After debugging a little bit, i came up with two solutions

A - Change handleFocus

Since the event.target.value inside this function is 0, maybe we could use the actual value prop sent to the component. That said, handleFocus could be something like:

const handleFocus = (event: FocusEvent<HTMLInputElement, Element>) => {
    if (autoSelect) {
      event.target.select()
    }

    const [originalValue, maskedValue] = updateValues(value ?? '0')

    if (maskedValue && onFocus) {
      onFocus(event, originalValue, maskedValue)
    }
}

B - Update the initial value for maskedValue

This solution consists in updating the initial maskedValue to something like:

const [maskedValue, setMaskedValue] = useState<number | string>(() => {
    if (!value) return '0'

    const [, calculatedMaskedValue] = maskValues(
      locale,
      value,
      currency,
      hideSymbol,
    )

    return calculatedMaskedValue
})

Let me know your thoughts here and maybe we can discuss more! Once we agree on a solution i can open a PR.

Comments

Greeting from Brazil Fortaleza-CE! Awesome package @leoreisdias! I've been looking for a currency input/mask, found many of them but honestly the best one is react-currency-mask, congrats for the project!

leoreisdias commented 2 weeks ago

@KevinGuedes Hello!

First, sorry for the delay in getting back to you. Thank you for reporting this issue and providing such detailed suggestions! The solutions you proposed are interesting and can work well to address the problem.

I particularly like Solution B because it initializes the maskedValue more robustly, ensuring the input value remains consistent with the value prop from the start. This approach reduces dependency on external props throughout the component's lifecycle, making the component more predictable and less prone to errors.

(I believe) this solution will help prevent future issues related to the component's initial state, especially in more complex scenarios. But what do think? Do you agree with this point?

Also, if you would like to open a PR with this change, I would be happy to review it!

Thanks again for your contribution and the kind words! 😊

Cheers from Minas Gerais! 😎