-
Notifications
You must be signed in to change notification settings - Fork 3.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
Fix issue with ReactEditor.focus + tests #5527
Conversation
🦋 Changeset detectedLatest commit: 0b01f5a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
editor, | ||
setTimeout(() => { | ||
// Flush changes before trying to focus again | ||
editor.onChange() |
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.
Not sure if we should actually call onChange here, or just retry until there are no operations pending.
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.
onChange should already have been called by this point, right? I can't think of any situations where manually flushing the changes would be beneficial, and it might result in some weird edge cases. If, somehow, there are still pending operations after the timeout, retrying is probably safest.
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.
Do we still need a weak map if we're not calling onChange? I can't think of any adverse side effects of running the timeout callback twice if a consumer happens to call ReactEditor.focus twice.
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 only other thing that occurs to me is what happens if something goes wrong and editor.operations never gets emptied? That might happen if we call ReactEditor.focus after the editor has been unmounted, for example.
Perhaps we should add an options: { retries?: number }
argument that defaults to 5 and is decremented on each attempt?
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.
@12joan - thank you for your comments. I've been looking a bit closer at this now and I'm unsure if this is actually the right fix, though it seems to help in some cases.
EDIT: I'm currently working on setting up some tests for this in the mono repo. Will come back with something later.
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.
onChange should already have been called by this point, right? I can't think of any situations where manually flushing the changes would be beneficial, and it might result in some weird edge cases. If, somehow, there are still pending operations after the timeout, retrying is probably safest.
Yes, I have refactored this now. It will now call onChange
, but only because it will try to set a selection in the top of the document if the editor currently isn't selected.
Do we still need a weak map if we're not calling onChange? I can't think of any adverse side effects of running the timeout callback twice if a consumer happens to call ReactEditor.focus twice.
I agree. I have removed it - also one less WeakMap :)
The only other thing that occurs to me is what happens if something goes wrong and editor.operations never gets emptied? That might happen if we call ReactEditor.focus after the editor has been unmounted, for example.
Perhaps we should add an options: { retries?: number } argument that defaults to 5 and is decremented on each attempt?
Yeah, good thinking. I have implemented a retry (default 5) and then throw an error if the editor is still stuck with operations.
This will make sure we don't try to focus the editor while it's in the midst of applying operations. If this is the case, retry setting focus in the next tick.
We need to be able to test against window features, like the DOM selection. @testing-library/react has a very similar API, but have also these features, which react-test-renderer is missing.
This will rewrite the existing tests for Editable and move them into a own file.
cd93734
to
649a36d
Compare
I have updated this PR quite a bit today. Please have another look. I can make a separate PR on the test upgrades if you'd prefer. But I prefer to have the tests together in this PR to illustrate the problem. If not pulled out, this PR should probably be rebased on top of |
focus: (editor, options = { retries: 5 }) => { | ||
// Return if already focused | ||
if (IS_FOCUSED.get(editor)) { | ||
return |
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.
This makes logical sense, but in real life maybe it's safer to just always run the code and re-set it to true?
el.focus({ preventScroll: true }) | ||
IS_FOCUSED.set(editor, 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.
Moved this to the end to ensure that we were able to set everything properly first.
// Create a new selection in the top of the document if missing | ||
if (!editor.selection) { | ||
Transforms.select(editor, Editor.start(editor, [])) | ||
editor.onChange() | ||
} |
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.
This seems like it's missing. Added it as the tests showed that the editor would be stuck with a null
selection after focus was set.
await act(async () => { | ||
Transforms.removeNodes(editor, { at: [0] }) | ||
Transforms.insertNodes(editor, propagatedValue) | ||
ReactEditor.focus(editor) // Note: calling focus in the middle of these transformations. |
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.
This focus call here will provoke that Error: Cannot resolve a DOM node from Slate node: {"text":"hello"}
This was a workaround for missing functionality in slate-react which is now fixed upstream ianstormtaylor/slate#5527
This was a workaround for missing functionality in slate-react which is now fixed upstream ianstormtaylor/slate#5527
* chore(portable-text-editor): upgrade slate and slate-react to latest * refactor(portable-text-editor): remove workaround This was a workaround for missing functionality in slate-react which is now fixed upstream ianstormtaylor/slate#5527 * fix(core/form/inputs): remove workaround This is not needed anymore as it's fixed upstream. * feat(portable-text-editor): create a selection on focus if it doesn't exist
* chore(portable-text-editor): upgrade slate and slate-react to latest * refactor(portable-text-editor): remove workaround This was a workaround for missing functionality in slate-react which is now fixed upstream ianstormtaylor/slate#5527 * fix(core/form/inputs): remove workaround This is not needed anymore as it's fixed upstream. * feat(portable-text-editor): create a selection on focus if it doesn't exist
Description
This will make sure we don't try to create a DOM range while the editor is still applying operations in the
ReactEditor.focus
function. And that it retries doing this until this the editor has flushed all it's changes.If a client requests
ReactEditor.focus
from the UI, while the editor is updating the DOM, this can result in unrecoverable errors like:Error: Cannot resolve a DOM node from Slate node: {"text":"hello"}
This has been reported by several people since #5516 was merged (see discussion there).
Have a look a these changes
I have made tests for this function, and in order to properly test it I replaced
react-test-renderer
with@testing-library/react
which has a very similar API, but fully supportswindow
. This means we can test code that constructs DOM ranges against the actualwindow.getSelection
API.This test will fail without my refactored code and provoke the above-mentioned error.
Issue
Fixes: see feedback on #5516
Example
See the new
react-editor.spec.tsx
testsContext
See #5516
Checks
yarn test
.yarn lint
. (Fix errors withyarn fix
.)yarn start
.)yarn changeset add
.)