Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Fix dead key deletion #666

Merged
merged 2 commits into from
Oct 17, 2016
Merged

Conversation

sophiebits
Copy link
Contributor

If you type Option-E, Backspace, then the in-progress accented character should be deleted. Similarly, in 2-Set Korean, if you press A, Backspace, the character should be deleted. Both of these don't work correctly yet, but this commit fixes it.

Currently, when you press Backspace, we are in composition mode and we receive a compositionend event then a keydown event for the backspace. Our composition handler doesn't exist composition mode immediately but rather waits 20ms to see if it gets a compositionstart event immediately (and if so, does not exit). It seems like if we receive a compositionend event then a keydown, we always want to exit composition mode immediately and re-interpret the keydown in edit mode. Here I do so.

This change is made trickier by the fact that each event handler reads from this.props.editorState, which doesn't get updated until the next rerender (that is, not synchronously). To combat this, we store the most current editor state in an instance variable and refer to it from all of the event handlers. (If we don't do this then the backspace isn't interpreted correctly by editOnKeyDown because the character data from compositionend hasn't been inserted into the editor state, so we'd end up effectively dropping the entire composition as well as deleting the preceding character.) In this commit, only editOnKeyDown needs the latest state but I changed the others for consistency. In cases where the parent always updates editorState in the same tick (as we recommend in the docs), there should be no behavior change.

Note: This also brings us closer to being able to handle cases where we don't always receive editorState synchronously -- instead we could store our own copy of the editorState locally but overwrite it with props when we receive new ones. It doesn't work yet though.

Test Plan: In plaintext example, type Option-E, Backspace (with US keyboard) or A, O, Backspace, Backspace (with 2-Set Korean keyboard). In both cases, the entire composition is deleted correctly.

cc @hellendag

@sophiebits
Copy link
Contributor Author

Note to self: need to at least fix the private props here because they're broken by the class transform internally.

@ghost ghost added the CLA Signed label Sep 19, 2016
@sophiebits
Copy link
Contributor Author

Looks like this also doesn't quite work in Firefox yet, though it seems no worse than the current code.

@flarnie flarnie self-assigned this Sep 20, 2016
@ghost ghost added the CLA Signed label Sep 20, 2016
@flarnie
Copy link
Contributor

flarnie commented Sep 20, 2016

About half done reviewing this, and wondering about your comment "Note to self: need to at least fix the private props here because they're broken by the class transform internally." What is that about, and is more work needed on this before it's ready to merge?

Notes so far:

  • Thanks for removing the 'renderGuard' dead code!
  • The switch to use this._latestEditorState makes sense; the only way that would cause issues is if users were somehow delaying passing in the updated 'editorState' after their 'onChange' callback was called, and relying on Draft.Editor using the old 'editorState' value. The docs say "For any edits or selection changes that occur in the editor DOM, your onChange handler will execute with the latest EditorState object based on those changes." Am I missing anything there?
  • The way the handlers are determined by the current 'mode' using 'buildHandler' in DraftEditor is neat, and I wonder if there is a name for that pattern. cc @hellendag

@sophiebits
Copy link
Contributor Author

About half done reviewing this, and wondering about your comment "Note to self: need to at least fix the private props here because they're broken by the class transform internally." What is that about, and is more work needed on this before it's ready to merge?

In the Facebook web codebase, we transform class Foo { ... this._bar ... } to class Foo { ... this.$Foo_bar ... } which prevents you from accessing private props from another file (specifically, from outside the class). In particular, both _latestEditorState and _onKeyDown need to be renamed here (or we can add @preventMunge to DraftEditor.react, but I was already nervous about using the private props cross-file so I'll just change it). A little more info at https://fburl.com/441040622 for FB employees.

if users were somehow delaying passing in the updated 'editorState' after their 'onChange' callback was called, and relying on Draft.Editor using the old 'editorState' value

We actually already don't support that: https://facebook.github.io/draft-js/docs/advanced-topics-issues-and-pitfalls.html#delayed-state-updates. This change probably brings us closer to being able to support that if we really wanted, actually.

The way the handlers are determined by the current 'mode' using 'buildHandler' in DraftEditor is neat, and I wonder if there is a name for that pattern.

Hmm, maybe related to delegation? https://en.wikipedia.org/wiki/Delegation_pattern

@ghost ghost added the CLA Signed label Sep 20, 2016
Copy link
Contributor Author

@sophiebits sophiebits left a comment

Choose a reason for hiding this comment

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

Most of these changes are just mechanical .props.editorState -> ._latestEditorState changes. Just adding ._latestEditorState in DraftEditor and the changes in the composition handler are the real changes here.

);
// Allow composition handler to interpret the compositionstart event
this._onCompositionStart(e);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was necessary so stillComposing in the composition handler gets set to true when first entering composition mode.

If you type Option-E, Backspace, then the in-progress accented character should be deleted. Similarly, in 2-Set Korean, if you press A, Backspace, the character should be deleted. Both of these don't work correctly yet, but this commit fixes it.

Currently, when you press Backspace, we are in composition mode and we receive a compositionend event then a keydown event for the backspace. Our composition handler doesn't exist composition mode immediately but rather waits 20ms to see if it gets a compositionstart event immediately (and if so, does not exit). It seems like if we receive a compositionend event then a keydown, we always want to exit composition mode immediately and re-interpret the keydown in edit mode. Here I do so.

This change is made trickier by the fact that each event handler reads from `this.props.editorState`, which doesn't get updated until the next rerender (that is, not synchronously). To combat this, we store the most current editor state in an instance variable and refer to it from all of the event handlers. (If we don't do this then the backspace isn't interpreted correctly by editOnKeyDown because the character data from compositionend hasn't been inserted into the editor state, so we'd end up effectively dropping the entire composition as well as deleting the preceding character.) In this commit, only editOnKeyDown needs the latest state but I changed the others for consistency. In cases where the parent always updates editorState in the same tick (as we recommend in the docs), there should be no behavior change.

Note: This also brings us closer to being able to handle cases where we don't always receive editorState synchronously -- instead we could store our own copy of the editorState locally but overwrite it with props when we receive new ones. It doesn't work yet though.

Test Plan: In plaintext example, type Option-E, Backspace (with US keyboard) or A, O, Backspace, Backspace (with 2-Set Korean keyboard). In both cases, the entire composition is deleted correctly.
@sophiebits
Copy link
Contributor Author

I decided to go with just adding @preventMunge because I don't want clients of DraftEditor thinking that it's okay to use these properties so I want to keep the underscore. Going to test in www.

@flarnie
Copy link
Contributor

flarnie commented Oct 13, 2016

Thanks - I'll try to read the rest of this later today.

@flarnie
Copy link
Contributor

flarnie commented Oct 14, 2016

Cool - so it's more like this:

It used to work like this:

                                 +--DraftEditor---------------------------+
                                 |                                        |
               event+----------->+ triggers '_update'                     |
                                 | ---> which calls 'this.props.onChange' |
                                 |      with the updated editorState      |
                                 |                                        |
                                 |                                        |
                                 |                                        |
                                 +----------------------------------------+

                                                      +
                                                      |
                                                      v

                                 +--DraftEditor---------------------------+
                                 |                                        |
           updates  <------------+ calls 'this.props.onChange'            |
           state.editorState     |                                        |
              +                  |                                        |
ANY DELAY     |                  |                                        |
HERE   XXXXXXX|                  |                                        |
CAUSES        v                  |                                        |
PROBLEMS   re-renders &          |                                        |
           passes in             |                                        |
           state.editorState +--------> We use updated                    |
           via props             |      this.props.editorState            |
                                 |                                        |
                                 |                                        |
                                 +----------------------------------------+


But now it works like this:

                                               +--DraftEditor---------------------------+
                                               |                                        |
                             event+----------->+ triggers '_update'                     |
                                               | ---> which calls 'this.props.onChange' |
                                               |      with the updated editorState      |
                                               |                                        |
                                               |                                        |
                                               |                                        |
                                               +----------------------------------------+

                                                                    +
                                                                    |
                                                                    v

                                               +--DraftEditor---------------------------+
Delays here could                              |                                        |
be OK because            updates  <------------+ calls 'this.props.onChange'            |
DraftEditor is           state.editorState     |    AND also updates                    |
*not* using                 +                  |    this.latestEditorState              |
props.editorState           |                  |                                        |
and it has      +---------> |                  |                                        |
already updated             v                  |                                        |
this.latestEditorState   re-renders &          |                                        |
                         passes in             |                                        |
                         state.editorState +--------> We can use                        |
                         via props             |      this.latestEditorState            |
                                               |      and not worry                     |
                                               |                                        |
                                               +----------------------------------------+

Overall I think this makes sense, and when I manually tested it (briefly) it works.

I'm working on the internal sync of 0.10.0, which includes a big API breaking change. I can either add this to 0.10.0 or do a 0.9.2 release. Either way I think it would be great to get this bug fix out soon. Thanks for working on it!

@sophiebits
Copy link
Contributor Author

Yes, that's basically right! It's that we basically wanted two successive events with nothing in between and before we weren't getting the new editorState when processing the second event, because state updates are batched and we hadn't rerendered.

@sophiebits sophiebits merged commit 07b1eb8 into facebookarchive:master Oct 17, 2016
sophiebits added a commit to sophiebits/draft-js that referenced this pull request Oct 19, 2016
I can't describe entirely why the DraftEditor.react change here is necessary, but this is closer to what we had before facebookarchive#666.

The editOnSelect change fixes holding down Backspace in Firefox -- before this, the cursor would jump to the start of the input.
sophiebits added a commit that referenced this pull request Oct 19, 2016
I can't describe entirely why the DraftEditor.react change here is necessary, but this is closer to what we had before #666.

The editOnSelect change fixes holding down Backspace in Firefox -- before this, the cursor would jump to the start of the input.
@doortts
Copy link

doortts commented Oct 23, 2016

@spicyj Thanks you for your PR and I'm expecting merged version will be released soon (Because I'm using Korean.) And I wonder another similar problem is solved or not with this PR. The problem is Left and Right arrow key doesn't work properly with Korean IME because of following codes at 0.9.1 https://github.com/facebook/draft-js/blob/master/src/component/handlers/composition/DraftEditorCompositionHandler.js#L96-L98

When I press arrow key, normally I expect to move cursor but it doesn't move and just change editor mode to composition end. Unlike Japanese or Chinese, Korean IME doesn't use arrow key to select or finish composition. In Korean (same as English), arrow keys are used as arrow key whether IME is in composition or not.

@spicyj Cloud you test your PR also solve this problem or not?

@sophiebits
Copy link
Contributor Author

Yes, it should. This Wednesday you should be able to confirm that typing works correctly on facebook.com. If not, please file a new issue and tag me in it describing exactly which sequence of keys behaves incorrectly, with your OS and browser version.

@TylerShin
Copy link

is it applied to npm?

if it is applied, I think 2-set korean removing still has problem. 😢

@sophiebits
Copy link
Contributor Author

No, it is not yet.

@TylerShin
Copy link

too bad. But I really appreciate your work. I'll wait for the update.

@KendoJaaa
Copy link

KendoJaaa commented Jan 4, 2017

Sorry @spicyj I'm new to Coding. Does this solve the Enter key problem I have to Enter twice to go to the new line? the first time to trigger compositionend. thx in advance

@sophiebits
Copy link
Contributor Author

Yes, it should.

@KendoJaaa
Copy link

KendoJaaa commented Jan 4, 2017

thank you I'll test it 👍 you response so fast

ouchxp pushed a commit to ouchxp/draft-js that referenced this pull request Apr 7, 2017
* Remove unused render guard logic

* Fix dead key deletion

If you type Option-E, Backspace, then the in-progress accented character should be deleted. Similarly, in 2-Set Korean, if you press A, Backspace, the character should be deleted. Both of these don't work correctly yet, but this commit fixes it.

Currently, when you press Backspace, we are in composition mode and we receive a compositionend event then a keydown event for the backspace. Our composition handler doesn't exist composition mode immediately but rather waits 20ms to see if it gets a compositionstart event immediately (and if so, does not exit). It seems like if we receive a compositionend event then a keydown, we always want to exit composition mode immediately and re-interpret the keydown in edit mode. Here I do so.

This change is made trickier by the fact that each event handler reads from `this.props.editorState`, which doesn't get updated until the next rerender (that is, not synchronously). To combat this, we store the most current editor state in an instance variable and refer to it from all of the event handlers. (If we don't do this then the backspace isn't interpreted correctly by editOnKeyDown because the character data from compositionend hasn't been inserted into the editor state, so we'd end up effectively dropping the entire composition as well as deleting the preceding character.) In this commit, only editOnKeyDown needs the latest state but I changed the others for consistency. In cases where the parent always updates editorState in the same tick (as we recommend in the docs), there should be no behavior change.

Note: This also brings us closer to being able to handle cases where we don't always receive editorState synchronously -- instead we could store our own copy of the editorState locally but overwrite it with props when we receive new ones. It doesn't work yet though.

Test Plan: In plaintext example, type Option-E, Backspace (with US keyboard) or A, O, Backspace, Backspace (with 2-Set Korean keyboard). In both cases, the entire composition is deleted correctly.
ouchxp pushed a commit to ouchxp/draft-js that referenced this pull request Apr 7, 2017
I can't describe entirely why the DraftEditor.react change here is necessary, but this is closer to what we had before facebookarchive#666.

The editOnSelect change fixes holding down Backspace in Firefox -- before this, the cursor would jump to the start of the input.
facebook-github-bot pushed a commit that referenced this pull request Feb 5, 2018
Summary:
The last thing we do inside 'componentWillUpdate' in DraftEditor is update the
'_latestEditorState'.

This was introduced in #666 and that
PR has a great explanation in the comments.

**BUT** when manually testing this, the bug with dead key deletion and korean
IME deletion not working was still happening, even with the current state of
things. So that was interesting. It doesn't seem to fix the thing we originally
intended, unless I'm missing something.

In a later change (https://our.intern.facebook.com/intern/diff/D4778712/)
passes latestEditorState to handlers, so that they can use the latest editor
state instead of stale editor state. This fixed many bugs in our own uses of
Draft.js.

And since there are many untested IME cases, it could be that some situations
are still improved by storing and tracking the '_latestEditorState'.

We already set '_latestEditorState' inside 'update' and that is always called
before 'componentWillUpdate'. My first approach is to just rely on that and
remove the updating in 'componentWillUpdate'.

Updating '_latestEditorState' in 'componentWillUpdate' does potentially give
us important info though; if the 'onChange' handler mutated the editorState
then we will need to use that, and we get it in 'componentWillUpdate'.

If we want to maintain this, then I propose the following;
- Move '_latestEditorState' into state in 'DraftEditor' and create a public
  getter method to access it.
- Update it in the new static method 'getDerivedStateFromProps' and then use
  the polyfill wrapper to make Draft.js backwards compatible.
I'd rather skip that if it turns out we don't rely on '_latestEditorState'
being updated after 'onChange' is called. With so many uses and so few tests,
the only way to know is to try it. :)

Follow-up action items:
- Add a unit test for latestEditorState being used and passed to handlers.
- Open the GK to 70% employees and let that sit for a couple of days, to verify
  that this doesn't cause major problems. Continue roll-out and remove GK.
- Add inline comments explaining why we track and use '_latestEditorState'.

Depends on D6866079

Reviewed By: sophiebits

Differential Revision: D6873303

fbshipit-source-id: 5df00598ea20d826f96d70eddcfe7043cf18a18b
fabiomcosta added a commit to fabiomcosta/draft-js that referenced this pull request Mar 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants