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

Fix issue with ReactEditor.focus + tests #5527

Merged
merged 5 commits into from
Nov 10, 2023

Conversation

skogsmaskin
Copy link
Collaborator

@skogsmaskin skogsmaskin commented Oct 19, 2023

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 supports window. This means we can test code that constructs DOM ranges against the actual window.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 tests

Context
See #5516

Checks

  • The new code matches the existing patterns and styles.
  • The tests pass with yarn test.
  • The linter passes with yarn lint. (Fix errors with yarn fix.)
  • The relevant examples still work. (Run examples with yarn start.)
  • You've added a changeset if changing functionality. (Add one with yarn changeset add.)

@changeset-bot
Copy link

changeset-bot bot commented Oct 19, 2023

🦋 Changeset detected

Latest commit: 0b01f5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
slate-react Minor

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()
Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

@skogsmaskin skogsmaskin Nov 7, 2023

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.

Copy link
Collaborator Author

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.
@skogsmaskin skogsmaskin changed the title Fix issue with slate-react static ReactEditor.focus method Fix issue with ReactEditor.focus method + tests Nov 7, 2023
@skogsmaskin skogsmaskin marked this pull request as ready for review November 7, 2023 22:47
@skogsmaskin
Copy link
Collaborator Author

skogsmaskin commented Nov 7, 2023

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 main to keep those commits separate from the actual bug fix.

@skogsmaskin skogsmaskin changed the title Fix issue with ReactEditor.focus method + tests Fix issue with ReactEditor.focus + tests Nov 7, 2023
focus: (editor, options = { retries: 5 }) => {
// Return if already focused
if (IS_FOCUSED.get(editor)) {
return
Copy link
Collaborator Author

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)
Copy link
Collaborator Author

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.

Comment on lines +445 to +449
// Create a new selection in the top of the document if missing
if (!editor.selection) {
Transforms.select(editor, Editor.start(editor, []))
editor.onChange()
}
Copy link
Collaborator Author

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.
Copy link
Collaborator Author

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"}

@dylans dylans merged commit fc08181 into ianstormtaylor:main Nov 10, 2023
11 checks passed
@github-actions github-actions bot mentioned this pull request Nov 10, 2023
skogsmaskin added a commit to sanity-io/sanity that referenced this pull request Nov 11, 2023
This was a workaround for missing functionality in slate-react which is now fixed upstream
ianstormtaylor/slate#5527
skogsmaskin added a commit to sanity-io/sanity that referenced this pull request Nov 13, 2023
This was a workaround for missing functionality in slate-react which is now fixed upstream
ianstormtaylor/slate#5527
github-merge-queue bot pushed a commit to sanity-io/sanity that referenced this pull request Nov 14, 2023
* 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
christianhg pushed a commit to portabletext/editor that referenced this pull request Jun 19, 2024
* 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
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.

3 participants