[lexical-yjs] Bug Fix: don't sync ElementNode __dir property#7330
[lexical-yjs] Bug Fix: don't sync ElementNode __dir property#7330etrepum merged 2 commits intofacebook:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
I don't personally have any domain expertise with bidi text so will have to wait for some feedback from other contributors to see what the ideal semantics are. I agree that it seems like it should primarily be a derived/computed property, but without full context I think we need to start with declaring what the DOM should be in the ideal scenario and then work backwards to come up with an implementation strategy that matches that expectation efficiently. I suspect that probably the complexity here is to make sure that if the user is entering rtl text in one paragraph and then creates another paragraph then it should probably start off as rtl even when empty so the selection is visually in the expected place? Some questions to answer:
@zurfyx - who is the best resource for this? |
|
I haven't looked deeply into this but we discussed it briefly on a call this morning. I would lean towards whatever the best long term solution is going to be, when updates to the DOM are made the reconciler is already processing essentially the whole tree top-down (skipping subtrees that are not dirty) so maybe reconciling the direction in a deterministic way could be done coming back up the tree at render time? The directional information could be cached on each DOM node as a cache. I think the real question is whether the ltr/rtl decisions need to be persisted outside of the DOM and if it makes sense to honor them as an override or have the reconciler update them in-place? Like before, I don't have much direct bidi experience to know what the correct UX and expectations are around this. |
That definitely makes sense. A natural follow-on question is what happens if the sibling paragraph then changes to LTR? What if you have N blank paragraphs? Thinking about the collab case, my gut feeling is that the blank paragraphs should stay as they were when they were created. Say User 1 has their cursor on the blank paragraph, and User 2 changes the first paragraph's text to LTR -- it would seem odd to have that move User 1's cursor to the other side of the page. I can also see it getting tricky when you have cases such as nested lists - if you're skipping non-dirty sub-trees, you could end up with parent elements that have one direction (because a sibling changed) and children with another. However...
I haven't looked into any of the rendering code yet -- maybe what you're suggesting would solve the nested list case.
Agree. That said, given that the current implementation derives direction from text content (i.e. ignores anything persisted), would you be open to a PR that implements the second suggestion above ("Mark NB: For context, this came up because we have a custom DecoratorNode on which we set a property based on some Redux state, where that property is in |
|
It probably makes sense to have There isn't really anywhere to associate versioned data (or cache computed data) with a LexicalNode without marking it as dirty, since they're copy on write and the connection to the children are indirect. The previous version has to be considered frozen (in |
a58cb56 to
ba8f280
Compare
|
PR updated to exclude |
|
(will wait for all extended collab tests to run before merging) |
Description
LexicalReconcilertries to determine whether text should be LTR or RTL based on a regex.If a given block has no directioned text, then the
activeEditorDirectionis used. This more-or-less-global variable is set tonullat the start of reconciliation but updated when a node with directioned text is encountered.That's fine when processing the whole tree: node get updated in reading order. However, if you process only some nodes in the tree, then you can end up changing the direction of a node in an unexpected way.
This is shown in the third test case where marking the first and third nodes as dirty changes the direction of the latter to RTL.Edit 2025-03-27: updated the PR to use the second approach below, i.e. exclude
__dirfor Yjs syncing.Potential fixes
Don't set node direction to LTR if no directioned text
This makes it so that the node direction is more in sync with the
dirproperty set in the DOM. Currently the reconciler clears thedirproperty if direction is null OR if it's LTR and the node has no directioned text: https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalReconciler.ts#L390-L395This approach is the one I've gone with in the current PR. The result is that the two editors have slightly different DOMs ('ltr' is removed in left editor but kept in right editor), however I don't think this is a huge issue. That said, a bunch of unit tests are failing.Mark
__diras an excluded property for Yjs syncingBased on what the reconciler is doing, figuring out direction from the text content, arguably
__diris a derived node property in the same way that__cachedTextis on the root node: https://github.com/facebook/lexical/blob/main/packages/lexical-yjs/src/Utils.ts#L50-L54I tried adding
__dirto the excluded list for element nodes and it was still only the Table test that needed updating.This feels like a reasonable approach to me, given
__diris derived state, however it would mean you could end up with two editors that display a given node with different directions.Take the example from the third test case - the two nodes were marked dirty but had no changes, so nothing would be synced to Lexical, so other editors would still havenullas the direction for the third paragraph.Backtrack through the tree to determine
activeEditorDirectionThis would be the most complete fix, however could be a significant change. Didn't want to go down this path without first discussing (and ruling out) the two options above.
The idea here would be to not use
activeEditorStatebut instead look at the previous block node in the tree (direct sibling, or sibling of a parent) and use its direction, assuming you were processing a node with no directioned text. That should give the same result as processing the whole tree in order.Process the whole tree when reconciling direction
This would give consistent results and would be a small code change, but is a bad idea for obvious performance reasons.