-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Make PlainInput controllable #13795
Make PlainInput controllable #13795
Conversation
a6b4b16
to
bf6b917
Compare
…d update storyshots
bf6b917
to
309eaa3
Compare
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.
LGTM, I tried the storybook, but maybe get another thumb as well from someone who understands the significance better
/** | ||
* This can only be used when the input is controlled. Use `transformText` if | ||
* you want to do this on an uncontrolled input. Make sure the Selection is | ||
* valid against the `value` prop. Avoid changing `value` and calling this at |
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 now I'm confused by this API. We almost always want to change both the text and the selection at the same time...AIUI the existing use cases are inserting text (like emoji, mentions, etc.) at the cursor. So that's why I suggested having a selection
prop along with the value. Did that not 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 ran into issues early on with the parent actively managing the selection and value at the same time. AFAICT on desktop there's no way to listen to events that change the cursor position (without selecting anything). Without the parent knowing the selection beforehand, I think the semantics of the prop would be weird, and even then I'm not sure if there's a way to insert text at the cursor the way we want. I'm beginning to think having a transformText
that works with controlled inputs would be a good idea, though I'm not sure how to handle that along with updating props.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.
Ah, yes, I remember running into that when I tried. @chrisnojima did mention there was some way to listen to all DOM events and then filter out the selection ones on a given component, but I didn't know enough to pursue that.
...and then, yeah, you can try implementing transformText
on a controlled component, but then things get murky w.r.t. the source of truth...
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.
@akalin-keybase OK, rather than going down the murky source of truth route, I added a getSelection
method. I added an example of a controlled input setting both at the same time (getSelection
-> change props.value
-> setSelection
). Let me know what you think
@@ -46,6 +130,9 @@ const load = () => { | |||
.add('With placeholder color', () => ( | |||
<PlainInput {...commonProps} placeholder="I'm blue!" placeholderColor="blue" /> | |||
)) | |||
|
|||
// Sandbox for testing controlled input bugginess | |||
storiesOf('Common', module).add('Controlled input playground', () => <ControlledInputPlayground />) |
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.
Cool! The main thing I wanted to test, though, was something that would insert/replace text. See the Input story 'Empty (uncontrolled)' and 'Empty (multiline) (uncontrolled)' for what I mean. Can you add a story like that? (Then it'll probably make it clear what I mean about being confused by the API...)
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.
Added a replica of Empty (uncontrolled)
(but controlled)
CI will fail for now on prettier not liking this type arg
|
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.
few more comments, and still gonna play around on mobile, but I think this approach would work!
if (selection) { | ||
this.setState( | ||
s => { | ||
const value = s.value.substring(0, selection.start) + t + s.value.substring(selection.start) |
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.
mind changing the last selection.start
to selection.end
? (to match the behavior of the uncontrolled version)
const value = s.value.substring(0, selection.start) + t + s.value.substring(selection.start) | ||
return {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.
ah, interesting. I was gonna ask how this would work with redux, but it looks like most (all?) of our inputs end up setting the state in some higher component anyway, so this would 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.
Yeah I think having the value come directly from redux risks a bad ux, since there could be some latency involved and the keypress should be very responsive. So fortunately the ux and API interests align in this case
</Box2> | ||
) | ||
} | ||
} |
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.
mind also adding a multiline version of this playground? sadly, I had found that some bugs only show up with multiline inputs...
@@ -46,6 +180,9 @@ const load = () => { | |||
.add('With placeholder color', () => ( | |||
<PlainInput {...commonProps} placeholder="I'm blue!" placeholderColor="blue" /> | |||
)) | |||
|
|||
// Sandbox for testing controlled input bugginess | |||
storiesOf('Common', module).add('Controlled input playground', () => <ControlledInputPlayground />) |
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.
Also perhaps this belongs under Common/Plain input
in the storybook tree?
Okay @akalin-keybase, I pushed the changes. From my testing on mobile I noticed that this change doesn't seem to have an effect on the ios simulator (it always inserts 'foo' at the end). I think the |
@@ -73,6 +75,13 @@ class PlainInput extends React.PureComponent<InternalProps> { | |||
} | |||
|
|||
transformText = (fn: TextInfo => TextInfo, reflectChange?: boolean) => { | |||
const controlled = !!this.props.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.
shouldn't you check if it's null or undefined instead? since empty string can be a valid 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.
yup, thanks!
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.
lgtm! tested on android
I took a detour trying to use
HOCTimers
withPlainInput
, but that involved adding ref forwarding toHOCTimers
, which it looks like redux isn't ready for yet (reduxjs/react-redux#1000). On the bright side it led me to improve the typing onPlainInput
and things that 'implement' it.This:
value
toPlainInput
that passes through to the native input componentcastProps
to be used by components that usePropsWithInput
so they can pass props through without flow complaining about extra props (a-la castPlatformStyles)transformText
on the native component, setting the text and then delaying setting the selection to work around bad behavior on androidtransformText
will throw an error ifvalue
is provided as a prop. In that case, the input is considered controlled.PlainInput
exposessetSelection
that can be used to set a selection that's compatible with thevalue
prop. Conversely, if the input is uncontrolledsetSelection
will throw an error, astransformText
should be used instead.NewInput
toPlainInput
. (Flow doesn't know aboutReact.forwardRef
(see Missing React.forwardRef definition facebook/flow#6103) so there's a$FlowIssue
there)r? @keybase/react-hackers cc @akalin-keybase @cjb