final-form / react-final-form

🏁 High performance subscription-based form state management for React
https://final-form.org/react
MIT License
7.39k stars 481 forks source link

Parse moves cursor to end of input field #317

Open joewestcott opened 6 years ago

joewestcott commented 6 years ago

See this example, in the username field: https://codesandbox.io/s/10rzowm323

Typing an uppercase character moves the cursor to the end of the line. This is only visible when the cursor has characters to it's right.

joewestcott commented 6 years ago

Related to #255? 🤔

renato commented 6 years ago

It also happens with the phone field in the same example.

joeljeske commented 6 years ago

I think its the typical react input cursor jump issue: facebook/react#955

emartini commented 5 years ago

any workaround on this issue? I'm can't figure out how to fix the issue in this example: https://codesandbox.io/s/no20p7z3l

emartini commented 5 years ago

Not using format and using react-text-mask worked for me:

Example: https://codesandbox.io/s/m4183xjk5j

taschetto commented 5 years ago

@emartini could you solve it without react-text-mask?

afrievalt commented 4 years ago

New to the code base but working on a solution, attempting to add a 3rd parameter to parse callback sending old value, if new value is shorter than the old value don't parse.

// todo: add oldValue to callback
const normalizePhone = (value, field, oldValue) => {
  if (!value) return value;
  const isDelete = oldValue.length > value.length
  if(isDelete) return value;
  const onlyNums = value.replace(/[^\d]/g, "");
  if (onlyNums.length <= 3) return onlyNums;
  if (onlyNums.length <= 7)
    return `(${onlyNums.slice(0, 3)}) ${onlyNums.slice(3, 7)}`;
  return `(${onlyNums.slice(0, 3)}) ${onlyNums.slice(3, 6)}-${onlyNums.slice(
    6,
    10
  )}`;
`};`
reinrl commented 4 years ago

@emartini could you solve it without react-text-mask?

@taschetto I am starting to believe that what solved this for Esteban was not necessarily the use of react-text-mask - and was more importantly the result of doing his own input masking (by way of rendering a <MaskedInput> component) within the render() prop of <Field ... />, instead of using the built-in parse attribute (which I am hypothesizing suffers the ill effects of some state mutation/rerender cycles within the bowels of react-final-form, as opposed to the outside handling of the masking/transformation of the user's input that react-text-mask in Esteban's example - admittedly, I haven't taken the time yet to tweak his refenced code sandbox sufficiently to further confirm).

@erikras - Does this seem like a plausible theory/something worth investigating further?

UPDATE: So I added a field to a fork of the codesandbox, and it appears to prove that this approach (using the Field's render prop, and effectively treating the render function like the Component prop through use of the value and onChange from the reference to input) suffers the same cursor jump issue...to a point.

The only time that I can get the cursor to not jump is when the input length is less than the allowed length (in this example, less than 10 digits entered) AND when the cursor is in the last "chunk" of the value (in this example, when the cursor is to the right of the last hyphen if any have already been added). After some more playing, this seems to be tied to whenever the value is mutated from the previous (e.g., for the phone number, I input a character to turn "123" into "1234" - and then my masking mutates it to "123-4") - meaning that input.onChange() is suffering the same ill effects of some state mutation/rerender cycles within the bowels of react-final-form? To make this more apparent, you can even attempt to manually set the cursor position with an example like this:

onChange={(event) => {
  const caretStart = event.target.selectionStart;
  const caretEnd = event.target.selectionEnd;
  onChange(
    formatString("999-999-9999", event.currentTarget.value)
  );
  event.target.setSelectionRange(caretStart, caretEnd);
}}

I am hypothesizing that the state update/rerender happening within RFF is happening after the setSelectionRange() call, so it has no visible effect. Circling back around to the react-text-mask example above (and why it works), notice that it doesn't actually set the form field value to the masked input form (instead, it has the value of the raw input - the second field is a great example of this) - so the efficacy of the solution seems to depend a lot on your requirements (Do you just need to display the mask, or are you trying to control what gets handed off to some sort of persistence mechanism?).