-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
[Numeric Input] Add allowNumericCharactersOnly prop #676
Conversation
- Mimics input[type="number"]'s limited char set now
- When true and when false - Add e.key null check in numericInput.tsx (for unit tests)
- Also fire this.props.onPaste via safeInvoke - Add unit test validating on-paste sanitization
- Mimics input[type="number"]'s limited char set now
- When true and when false - Add e.key null check in numericInput.tsx (for unit tests)
- Also fire this.props.onPaste via safeInvoke - Add unit test validating on-paste sanitization
How about simply |
+1 for reducing the character length of this prop name 😅 |
@giladgray that's good with me (and that's what @llorca suggested originally). Just want to make sure it's clear that the input will support more than just |
…antir/blueprint into cl/numeric-input-numeric-digits-only
Changed the prop name to |
@@ -141,6 +141,7 @@ export class NumericInputBasicExample extends BaseExample<INumericInputBasicExam | |||
return ( | |||
<div> | |||
<NumericInput | |||
allowFloatingPointNumberCharactersOnly={false} |
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.
missed prop name change
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.
Fixed 👍
it("sets allowNumericCharactersOnly to true by default", () => { | ||
const component = mount(<NumericInput />); | ||
const value = component.props().allowNumericCharactersOnly; | ||
expect(value).to.be.true; |
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.
you can probably remove this test =p
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.
The test isn't here to test that React's defaultProps
functionality works as expected; it's here to enforce the contract the component claims to uphold. During a refactor, if I accidentally delete or change a default value, then that would affect consumers, and I'd want to know about it from a failing test.
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.
can the same not be said for all defaultProps
?
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 my perfect world, yes. :) But even my coverage isn't complete here; you're right. Still, I'd prefer to strive toward a world of testing (and therefore ensuring) defaults, even though setting a default is a trivial operation.
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 agree with @leebyp here--this is not an important feature to test. it's a trivial test that can't catch bugs in our code because there's no logic being exercised here. it also doesn't increase code coverage.
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've come to appreciate unit tests as a way to both verify and communicate every reasonable assumption a consumer can make about a component. Catching bugs and increasing code coverage both play into that, and those are of course the most tangible benefits.
Expanding on the additional piece of my philosophy a bit, if I approach work on an unfamiliar component (e.g.) and see unit tests for Big Behavior X and Big Behavior Y but not Little Behavior Z, then (1) I can't tell if Little Behavior Z is an important part of the contract, (2) I can't always determine how to get the component in that state to verify how Little Behavior Z is supposed to work, and (3) I don't have a safeguard in place to tell me if I break Little Behavior Z.
For me, testing defaults fits into (3) and also fits into (1) if the default is impactful—but definitely does not fit into (2) (2 is rarer, but I still encounter it occasionally with subtle error-checking code).
Anyway, it's the communication piece that leads me to opt for testing defaults. Happy to go with the majority in this case though; will remove.
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 the case of <NumbericInput />
, to me, it seems more logical to have a test where you test the functionality of the component when you don't pass anything in - that's actually the output you care about, regardless of what the internals do.
In the examples, I think the problem is actually that behaviour z should be tested in the first place. No one checks the tests to see what the component contracts are (or at least I don't). If I break something and the tests don't catch it - we need more tests.
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.
test the functionality of the component when you don't pass anything in
This sounds like a good approach to me.
If I break something and the tests don't catch it - we need more tests.
Agree.
@@ -212,6 +218,410 @@ describe("<NumericInput>", () => { | |||
}); | |||
}); | |||
|
|||
describe("Keyboard text entry in input field", () => { | |||
|
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.
ALL THE TESTS!!!!
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 you can pull a bunch of the repeating constants to the top, especially the keys
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.
It's a little tricky, since certain keys hop from "invalid" lists into "valid" lists depending on the prop flag. I'll see what I can do though.
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.
Done 👍 This feels way better; thanks!
@giladgray @leebyp - ready for re-review! |
LGTM 👍 would be sweet if we could add that prop to the modifier switches in the example, so we can play with it |
@llorca done! |
Awesome! Hey I just realized this feature is broken on Safari... any idea why? |
let nextValue: string; | ||
|
||
if (this.props.allowNumericCharactersOnly && this.didPasteEventJustOccur) { | ||
this.didPasteEventJustOccur = false; |
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.
what happens if we run through this block everytime regardless of whether paste event happened or not - is this a perf thing or are there other effects?
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.
It's a little extra work, yes—with my interviewer hat on, O(n) for each keystroke—but that won't be an issue in real-world scenarios where numbers are only going to be a few characters long. I'd still prefer not to do that extra work, if only because it feels unclean and less clear that it's helpful specifically for the paste scenario. Maybe a comment would suffice though.
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.
Yeah it's pretty obvious what's going on, i was only curious because you mentioned it's one of the inelegant place. A comment sounds good.
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.
Comment + leave as is, or comment + removing this.didPasteEventJustOccur
?
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.
comment + leave as is
rest looks good except the safari issue - curious about the |
@llorca @leebyp RE: Safari issue: looks like Safari doesn't support |
If the Safari fix is nontrivial, would prefer addressing that in a follow-up; it's low priority |
@llorca @leebyp @adidahiya - Moving the logic to EDIT: Also verified that it works on IE 11. |
Fixes aspects of #618
Checklist
Numeric characters only
toggle toNumericInput
basic exampleChanges proposed in this pull request:
Motivation
Now that we have both a basic example and an extended example for
NumericInput
(PR #670), the basic/out-of-the-box example doesn't feel restrictive enough with its character validation (e.g. it allows entry of alpha characters that have no place in a numeric format). The component was designed to mimic the powerful numeric inputs in the Sketch app, but it also makes sense for the component to optionally behave exactly like nativeinput[type="number"]
elements.Change
Thus, the
allowNumericCharactersOnly
prop was born in this PR. It defaults totrue
, in which case the component mimics nativeinput[type="number"]
s; when this prop isfalse
, the component behaves as it currently behaves, allowing any character to be typed.Why not avoid the prop and always require numeric characters only?
Because then it would be impossible to add arbitrary additional functionality to the component, such as mathematical expressions, which would need support for characters like
*
and/
, or number abbreviations, which would need support for characters likek
,m
,b
, and so on.Earlier implementations
I initially hoped I could just use a real
input[type="number"]
element and defer all validation logic to it (only ifallowNumericCharactersOnly
wastrue
). I tried this in two ways, both of which proved unworkable:Replace the
input[type="text"]
with aninput[type="number"]
, and let the user type into it directly.Keep using
input[type="text"]
for the visible text field, but pass events down to a hiddeninput[type="number"]
to get sanitization behavior for free whenever the input text changes.Option 1 was bad for two reasons: (1)
setSelectionRange
doesn't work ininput[type="number"]
(link), which makes impossible our currentselectAllOnFocus
and "select all on increment" behavior; and (2) styling the native stepper, while mostly doable, is a huge pain that requires different approaches for each browser (link).Option 2 was bad because you can't "forward" keypresses to an input field programmatically (as if a user had typed them) and expect appropriate events to fire.
Final implementation
So, the next-best thing was to read the W3 spec for
input[type="number"]
(link) and implement our own validation logic to replicate that. Fortunately, this wasn't too difficult. Nativeinput[type="number"]
s simply accept any ordering of floating-point number characters as described elsewhere in the W3 spec (link).There were two input cases to consider:
0-9
,e
,E
,+
,-
, or.
.There are a plethora of unit-tests to verify correctness of this behavior, so I'm confident it works.
Remaining issues
input[type="number"]
is that after pasting (possibly in the middle of existing text), this implementation always moves your cursor to the end of the text. Would be nice to fix, but it seems uncommon and therefore low-priority.Reviewers should focus on:
didPasteEventJustHappen
flag). If you have other (or just better) implementation ideas, let me know.The prop name is long but precise. Happy to considerSo changedallowNumericCharactersOnly
or something shorter though.