-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
[Breaking Change][lexical] Feature: New update tag: skip-dom-selection, $onUpdate now always called #6894
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
size-limit report 📦
|
currentAnchorNodeDOM, | ||
editor._config, | ||
))); | ||
($isTextNode(currentAnchorNode) && |
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.
These changes are just to allow TypeScript to infer that the type of currentAnchorNode and previousAnchorNode are both TextNode which is required to satisfy updateDOM. Strictly speaking it could still violate constraints if they have different TextNode-compatible constructors, e.g. !(previousAnchorNode instanceof currentAnchorNode.constructor)
but this is not something that TypeScript can detect and we already have a runtime invariant to ensure the constructor never changes for a node with a given key.
It looks like there's one failing flaky collab+firefox e2e test that now reliably fails in this PR. I'll investigate further before merge.
|
So what's currently happening here is:
|
Autocomplete is a necessary condition for this, so it seems that interaction between autocomplete and collab is the real problem here |
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 is passing all tests now, just needs a re-approval. Added some commentary on the new changes.
@@ -52,7 +52,12 @@ test.describe('Autocomplete', () => { | |||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | |||
dir="ltr"> | |||
<span data-lexical-text="true">Sort by alpha</span> | |||
<span data-lexical-text="true"></span> | |||
<span |
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 changed the autocomplete node to render more than an empty span in collab, but set its display to none. This makes it easier to see what's going on in the DOM. I think ultimately there should be some other solution for autocomplete in collab that doesn't get synchronized.
@@ -960,7 +970,10 @@ export class LexicalEditor { | |||
registeredNodes.push(registeredReplaceWithNode); | |||
} | |||
|
|||
markAllNodesAsDirty(this, klass.getType()); | |||
markNodesWithTypesAsDirty( |
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 renamed this internal function because now it actually does what we need it to do
@@ -607,7 +607,8 @@ export function $commitPendingUpdates( | |||
editor._editable && | |||
// domSelection will be null in headless | |||
domSelection !== null && | |||
(needsUpdate || pendingSelection === null || pendingSelection.dirty) | |||
(needsUpdate || pendingSelection === null || pendingSelection.dirty) && | |||
!tags.has('skip-dom-selection') |
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 is the logic that skips DOM selection with that tag
@@ -1005,6 +1006,7 @@ function $beginUpdate( | |||
|
|||
const shouldUpdate = | |||
editor._dirtyType !== NO_DIRTY_NODES || | |||
editor._deferred.length > 0 || |
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 is where we defer the update whenever there is an onUpdate, so you can attach the callback, do some mutations in another update issued that tick, and still get a callback
@@ -72,7 +72,6 @@ import { | |||
internalGetActiveEditorState, | |||
isCurrentlyReadOnlyMode, | |||
triggerCommandListeners, | |||
updateEditor, |
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 didn't see a reason to use this internal function here
// Mark all existing text nodes as dirty | ||
updateEditor( | ||
editor, | ||
export function markNodesWithTypesAsDirty( |
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.
Previously whenever a transform was registered we marked all nodes in the editor as dirty, which was surprisingly the biggest factor in the collab+autocomplete+selection issue. Now it only marks relevant nodes as dirty, which in this case is none. We can use the same internal facility developed for registerMutationListener initialization to find the matching nodes by type in the reconciled editor state to determine which ones should be marked as dirty.
node.markDirty(); | ||
for (const nodeMap of dirtyNodeMaps) { | ||
for (const node of nodeMap.values()) { | ||
node.markDirty(); |
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.
We don't need to check if the nodes are attached, worst case they just end up being re-inserted in the nodeMap and get GC'd on reconciliation.
@@ -1825,17 +1833,26 @@ export function getCachedTypeToNodeMap( | |||
); | |||
let typeToNodeMap = cachedNodeMaps.get(editorState); | |||
if (!typeToNodeMap) { | |||
typeToNodeMap = new Map(); | |||
typeToNodeMap = computeTypeToNodeMap(editorState); |
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 is just extracting some logic to a function, nothing was changed here, I thought I might need to do it with a live EditorState but then realized that the frozen one will work even better since all nodes in the live editorstate are either in the frozen one or are already dirty.
@@ -29,6 +29,7 @@ export type { | |||
SerializedEditor, | |||
Spread, | |||
Transform, | |||
UpdateListener, |
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 thought I was going to need to use tags in the updateListener in the Autocomplete plugin and noticed this type was not exported
…ts context Changing the focus behavior was more difficult than expected. Apparently when Lexical has a Selection then it will 'steal' the focus whenever changes to its content is made. There have been made changes to Lexical just recently that would allow us to avoid this (facebook/lexical#6894) but we haven't updated to this version yet and updating this closely before the EAP seems risky. I found a workaround in one of the discussions that suggests to set the selection to `null` when making a change. In order to do this I refactored some of the function to make them more composable. Instead of calling `editor.update` directly these functions are now supposed to be called from `editor.update`. Since I've added a helper for creating a promise-version of `editor.update` anyway, I also addressed one of the issues I've encountered in the last PR, namely that `onUpdate` is not called when the editor state doesn't change. This will btw also be fixed on the Lexical side by the above mentioned PR.
…ts context (#6385) Closes srch-1483 Changing the focus behavior was more difficult than expected. Apparently when Lexical has a Selection then it will 'steal' the focus whenever changes to its content is made. There have been made changes to Lexical just recently that would allow us to avoid this (facebook/lexical#6894) but we haven't updated to this version yet and updating this closely before the EAP seems risky. I found a workaround in one of the discussions that suggests to set the selection to `null` when making a change. In order to do this I refactored some of the function to make them more composable. Instead of calling `editor.update` directly these functions are now supposed to be called from `editor.update`. Since I've added a helper for promisifying `editor.update` anyway, I also addressed one of the issues I've encountered in the last PR, namely that `onUpdate` is not called when the editor state doesn't change. To prevent accidentally calling it in a way that would not resolve the promise, the callback has to return a boolean. This will btw also be fixed on the Lexical side by the above mentioned PR. ## Test plan Manual testing https://github.com/user-attachments/assets/296cca9e-b8e3-433e-a39a-befbb8fe2017
…ts context (#6385) Closes srch-1483 Changing the focus behavior was more difficult than expected. Apparently when Lexical has a Selection then it will 'steal' the focus whenever changes to its content is made. There have been made changes to Lexical just recently that would allow us to avoid this (facebook/lexical#6894) but we haven't updated to this version yet and updating this closely before the EAP seems risky. I found a workaround in one of the discussions that suggests to set the selection to `null` when making a change. In order to do this I refactored some of the function to make them more composable. Instead of calling `editor.update` directly these functions are now supposed to be called from `editor.update`. Since I've added a helper for promisifying `editor.update` anyway, I also addressed one of the issues I've encountered in the last PR, namely that `onUpdate` is not called when the editor state doesn't change. To prevent accidentally calling it in a way that would not resolve the promise, the callback has to return a boolean. This will btw also be fixed on the Lexical side by the above mentioned PR. ## Test plan Manual testing https://github.com/user-attachments/assets/296cca9e-b8e3-433e-a39a-befbb8fe2017
Description
'skip-dom-selection'
update tag. Both of these are relevant to a very common FAQ which is about how to avoid the editor taking over the selection with a programmatic update.updateDOM
to be more correct, theprevNode
argument should always bethis
(the same class, but a different instance). This fixes some subclassing issues.onUpdate
and$onUpdate
to always be called. Previously they would be skipped if the update containing them did not make any nodes dirty. It's often useful to use$onUpdate
to attach some logic in adispatchCommand
that does not need to update nodes or the selection but needs to do something with the reconciled DOM or editor afterwards.Closes #6514
Clsoes #4474
Upgrade Notes
If you have code that expects
onUpdate
or$onUpdate
to not be called if the update did not make any nodes or the selection dirty, then you will have to change your code accordingly.Now when registerNodeTransform is called, only relevant nodes (by type, including support for withKlass) are marked dirty. Previously, all nodes in the editor were marked as dirty when any transform was registered (except RootNode, which was a special case).
Test plan
Unit tests were added for the new behavior of
onUpdate
/$onUpdate
and the'skip-dom-selection'
tag.