-
Notifications
You must be signed in to change notification settings - Fork 46.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Chrome number input backspace and invalid input issue #7359
Conversation
} else if (attributeName === 'value') { | ||
if (node.value != value) { | ||
node.value = '' + value; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ironically our code used to look a lot like this… https://github.com/facebook/react/blob/0.14-stable/src/renderers/dom/shared/DOMPropertyOperations.js#L187-L197
@jimfb - I think this is more fallout from your changes. Can you take a look and figure out a way forward?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack! I crossed the streams!
This PR was supposed to be solely for fixing defaultValue
, but it looks I got my branches mixed up and pushed the more general fix for both inputs. I actually just replied here:
Again, pushing to this branch was a total accident. A bunch of tests fail now. I'll work on getting those to pass in the mean time, but could definitely use some advice.
Cases enumerated here: |
Ah and it looks like |
@@ -158,6 +158,10 @@ var DOMPropertyOperations = { | |||
} else if (propertyInfo.hasBooleanValue || | |||
(propertyInfo.hasOverloadedBooleanValue && value === true)) { | |||
node.setAttribute(attributeName, ''); | |||
} else if (attributeName === 'value' && node.hasAttribute('value')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type coercion solved my problems, however it makes the initial case hard. Empty inputs are always equal to 0
.
I think this could be better. Any suggestions?
1e2a9ab
to
dcd31d3
Compare
Added ignore statements for ESLint rules and a test for |
// Use loose coercion to prevent replacement on comparisons like | ||
// '3e1' == 30 in Chrome (~52). | ||
if (node.value != value) { // eslint-disable-line | ||
node.setAttribute(attributeName, '' + value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing I'm not clear on yet: Why assign the value
attribute at all?.
ReactDOMInput
will assign the value
property in updateWrapper
. We could do that in mountWrapper
as well. Is it important that the attribute be assigned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, there's a difference between setting node.value
and calling node.setAttribute('value')
. node.value
tracks the value of the node in JS, and setAttribute
will actually set the attribute on the element in the DOM.
This change causes an issue where the DOM attribute is never updated, because node.value
is already updated prior to this check, so it never actually sets the attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. So actually: should the value attribute update for anything other than checkbox
and radio
?
Following browser behavior, editing the text inside of a text input doesn't update the value
attribute.
Possible my current code doesn't do this, but to me that's the correct functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React does deviate from browser behavior when it comes to controlled components (e.g., change events on keydown instead of on blur). Currently the value
attribute does update on controlled inputs, and it looks like we're explicitly setting it, so there may be a good reason I'm not aware of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an extra complication here... Assigning the value
attribute forces validation for number inputs. Check this out:
Notice how the decimal gets stripped? My fix focuses on avoiding touching the input as much as possible to avoid unexpected behavior like this. Fortunately, I believe this also conforms to the standard way inputs work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the current behavior of setting the attribute is intentional though I could be misremembering. I know we definitely need to set value
on the <progress>
element. You can look at the history of ReactDOMInput to see when jimfb last made changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dcd31d3
to
1b097ff
Compare
Anything this is waiting on? This is a pretty obnoxious bug. Thanks so much to @nhunzaker for putting this PR together. |
I'll try and get this reviewed when I get back from vacation next week, if no one else gets a chance to do it in the meantime. Overall it looks good to me though! |
@aweary No worries -- I'm on vacation too! Looking forward to getting this |
1b097ff
to
149dfc3
Compare
Rebased with master. I also added @iangilman's case for trailing dashes, and a test case for negative numbers: |
67ed49f
to
4a3eeca
Compare
I'm interested in why |
@jquense You know what, I think I have this wrong. The test case is when you select the entire number value and I'll update the test case. I also need to figure out what to do about Away I go. |
ah gotcha. If it helps btw please feel free to look at https://github.com/jquense/react-widgets/blob/v4/packages/react-widgets/src/NumberInput.js to see how i fumbled through on this logic |
Also run prettier
Cool. I just had to update the test case, everything works fine. If the number isn't valid, it parses to a blank value, which is also what the number input reports, so no change occurs. I've also run prettier on this branch and dealt with the associated eslint issue. |
@jquense This NumberInput is cool. How crazy would it be to bake something like this into React? Later... later.. I want to get this PR merged so badly, haha. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Later... later.. I want to get this PR merged so badly, haha.
Oh yeah? well, how about we go ahead and do that then 😄 I was able to test this out today using the DOM fixtures. I verified everything seems to be working correctly in:
- IE9
- IE10
- IE11
- Edge 14
- Chrome 57 (Windows)
- Chrome 57 (macOS)
- Chrome (Android, Galaxy S5)
- Firefox 52 (Windows)
- Firefox 52 (macOS)
- Opera 44 (Windows)
- Safari 10 (macOS)
- Safari (iOS 9)
- Safari (iOS 8)
Everything appears to be working great, and I see nothing immediately wrong with the changes made. So, LGTM!
* Only re-assign defaultValue if it is different * Do not set value if it is the same * Properly cover defaultValue * Use coercion to be smart about value assignment * Add explanation of loose type checks in value assignment. * Add test coverage for setAttribute update. * Only apply loose value check to text inputs * Fix case where empty switches to zero * Handle zero case in controlled input * Correct mistake with default value assignment after rebase * Do not assign bad input to number input * Only trigger number input value attribute updates on blur * Remove reference to LinkedValueUtils * Record new fiber tests * Add tests for blurred number input behavior * Replace onBlur wrapper with rule in ChangeEventPlugin * Sift down to only number inputs * Re-record fiber tests * Add test case for updating attribute on uncontrolled inputs. Make related correction * Handle uncontrolled inputs, integrate fiber * Reorder boolean to mitigate DOM checks * Only assign value if it is different * Add number input browser test fixtures During the course of the number input fix, we uncovered many edge cases. This commit adds browser test fixtures for each of those instances. * Address edge case preventing number precision lower than 1 place 0.0 coerces to 0, however they are not the same value when doing string comparision. This prevented controlled number inputs from inputing the characters `0.00`. Also adds test cases. * Accommodate lack of IE9 number input support IE9 does not support number inputs. Number inputs in IE9 fallback to traditional text inputs. This means that accessing `input.value` will report the raw text, rather than parsing a numeric value. This commit makes the ReactDOMInput wrapper check to see if the `type` prop has been configured to `"number"`. In those cases, it will perform a comparison based upon `parseFloat` instead of the raw input value. * Remove footnotes about IE exponent issues With the recent IE9 fix, IE properly inserts `e` when it produces an invalid number. * Address exception in IE9/10 ChangeEventPlugin blur event On blur, inputs have their values assigned. This is so that number inputs do not conduct unexpected behavior in Chrome/Safari. Unfortunately, there are cases where the target instance might be undefined in IE9/10, raising an exception. * Migrate over ReactDOMInput.js number input fixes to Fiber Also re-record tests * Update number fixtures to use latest components * Add number input test case for dashes and negative numbers * Replace trailing dash test case with replace with dash Also run prettier
// If controlled, assign the value attribute to the current value on blur | ||
let value = '' + node.value; | ||
if (node.getAttribute('value') !== value) { | ||
node.setAttribute('value', value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not caught up on the entire history here.
Why is this special case needed? extractEvents should not have side-effects in it.
Can this be handled by the normal controlledness flow in updateWrapper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be enough to flag this with enqueueStateRestore
so that the normal updateWrapper
kicks in at the end of the blur event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can verify. As for history:
React controlled inputs sync up the value attribute with the value property. This prevents state mismatches with form.reset() and browser plugins (though i never got a reply on which ones). Unfortunately, on number inputs in Chrome and Safari, this forces a check on the input, removing trailing decimal places, invalid characters, etc.
The only way i could get around this (i tried for months, off and on) was to defer setting the value attribute on blur.
I haven't done much with enqueueStateRestore. I'll give it a go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we should just not set the value
attribute. People writing manual DOM operations don't do that and it works fine for them, so not sure why we do.
Would just avoiding setting the attribute all together fix the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be lovely. The only edge case is form.reset(). Honestly, I'd be in favor of just adding a warning about this, or enqueing updateWrapper on the reset form event if it can be done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to be clear that form.reset
is totally fine when the form contains only uncontrolled inputs. If we did go the route of warning it should only warn if the form contains one or more controlled inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just .reset() and CSS input[value=foo]
selectors. TBH the latter was a pretty common feature request so I'd be sad to see it go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are other attributes that matter. Such as all of those that don't have corresponding properties (generally things outside the HTML spec). There are edge cases that need removing attributes (like progress
). However, for value
(or checked
) I don't believe there were any cases that needed it outside of intuition. E.g. the attribute shows up in DOM dev tools in Chrome etc.
However, there is nothing that relies on it being there because all those thousands of websites that do manual DOM management. Nobody manually calls setAttribute
in those cases. However, as we've seen, there are cases where setting it all the time is unusual such as in this case. So it leads to undiscovered browser deviations.
That's why I think our design should be to model what a user would write manually - even if that seems unintuitive of how people think React works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In most cases I can think of with manual manipulation it's actually undesirable to set the value attribute because you want to rely on form.reset. at least, this has always been the case for me. Being able to style text inputs based on a dynamic value attribute is a privilege react provides, one I would be comfortable with no longer having.
React doing Dom manipulation, the correct way, is the abstraction I've always had. I think this is a very strong metaphor that makes it easier to reason about what react is doing. We deviate from that with controlled inputs. I'm in favor of this change, which moves closer towards that idea.
So I agree, armchair theory aside 😂. I can start scaffolding out what that would look like, but I won't be able to pick it up until Monday, if someone else wants to give it a shot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof. I finally did it. Please see:
I've bumped my project to 15.5.4 and the issue is still there. |
@btoueg Provide a reproducing case please? |
Sorry for the delay. My report was a false negative. Works fine now, congrats on fixing the issue. |
In Google Chrome, backspacing an controlled and uncontrolled number inputs causes an unexpected behavior:
More cases (and what fixes look like) here: #7253 (comment)
Issues with uncontrolled inputs
This is because assigning
defaultValue
causes side-effects onvalue
. It seems to trigger validation onvalue
as if it were directly assigned itself.This Codepen provides a step by step reproduction case:
http://codepen.io/nhunzaker/pen/zBaokp
This PR wraps the
defaultValue
assignment in a conditional so that it will only ever be assigned if the defaultValue has changed.Issues with controlled inputs
It looks like the issue here is that value is being assigned using the standard React method of updating attributes when the props get passed in here:
https://github.com/facebook/react/blob/master/src/renderers/dom/shared/ReactDOMComponent.js#L926
There is some logic in ReactDOMInput to prevent duplicate values from being assigned here:
https://github.com/facebook/react/blob/master/src/renderers/dom/client/wrappers/ReactDOMInput.js#L201-L210
Unfortunately the new value has already been assigned.
This PR adds a check for the
value
prop, and only assigns it if is different. It never assigns the value attribute if it hasn't changed:I don't really know why this works. I didn't think node.setAttribute('value', value) was meaningful. What do you think?
Related to #7253