Skip to content
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

Merged
merged 17 commits into from
Sep 26, 2018
Merged

Conversation

buoyad
Copy link
Contributor

@buoyad buoyad commented Sep 17, 2018

I took a detour trying to use HOCTimers with PlainInput, but that involved adding ref forwarding to HOCTimers, 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 on PlainInput and things that 'implement' it.

This:

  • Adds a prop value to PlainInput that passes through to the native input component
  • Adds a function castProps to be used by components that use PropsWithInput so they can pass props through without flow complaining about extra props (a-la castPlatformStyles)
  • Implements transformText on the native component, setting the text and then delaying setting the selection to work around bad behavior on android
    • transformText will throw an error if value is provided as a prop. In that case, the input is considered controlled. PlainInput exposes setSelection that can be used to set a selection that's compatible with the value prop. Conversely, if the input is uncontrolled setSelection will throw an error, as transformText should be used instead.
  • Adds a story Common/Controlled input playground with some examples and controls to test input behavior
  • Hooks up the amount value in the store to the send form field so requests from chat auto populate visually correctly
  • Forwards a ref from NewInput to PlainInput. (Flow doesn't know about React.forwardRef (see Missing React.forwardRef definition facebook/flow#6103) so there's a $FlowIssue there)

r? @keybase/react-hackers cc @akalin-keybase @cjb

@buoyad buoyad force-pushed the danny/DESKTOP-7572-new-controllable-inputs branch from a6b4b16 to bf6b917 Compare September 17, 2018 22:16
@buoyad buoyad force-pushed the danny/DESKTOP-7572-new-controllable-inputs branch from bf6b917 to 309eaa3 Compare September 18, 2018 14:16
Copy link
Contributor

@zanderz zanderz left a 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
Copy link
Contributor

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?

Copy link
Contributor Author

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...

Copy link
Contributor

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...

Copy link
Contributor Author

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 />)
Copy link
Contributor

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...)

Copy link
Contributor Author

@buoyad buoyad Sep 20, 2018

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)

@buoyad
Copy link
Contributor Author

buoyad commented Sep 20, 2018

CI will fail for now on prettier not liking this type arg

[error] shared/common-adapters/plain-input.stories.js: SyntaxError: Unexpected token (28:47)
[error]   27 |   // prettier-ignore
[error] > 28 |   _input = React.createRef<typeof PlainInput>()
[error]      |                                               ^

Copy link
Contributor

@akalin-keybase akalin-keybase left a 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)
Copy link
Contributor

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}
},
() => {
Copy link
Contributor

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!

Copy link
Contributor Author

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>
)
}
}
Copy link
Contributor

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 />)
Copy link
Contributor

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?

@buoyad
Copy link
Contributor Author

buoyad commented Sep 21, 2018

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 onSelectionChange events don't fire as reliably on the simulator.. I'll test it on device too.

@@ -73,6 +75,13 @@ class PlainInput extends React.PureComponent<InternalProps> {
}

transformText = (fn: TextInfo => TextInfo, reflectChange?: boolean) => {
const controlled = !!this.props.value
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, thanks!

Copy link
Contributor

@akalin-keybase akalin-keybase left a 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

@buoyad buoyad merged commit b2be704 into master Sep 26, 2018
@buoyad buoyad deleted the danny/DESKTOP-7572-new-controllable-inputs branch September 26, 2018 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants