[lexical-yjs] Bug Fix: refresh a remote cursor when its peer changes name or colour - #9013
Closed
LeSingh1 wants to merge 1 commit into
Closed
[lexical-yjs] Bug Fix: refresh a remote cursor when its peer changes name or colour#9013LeSingh1 wants to merge 1 commit into
LeSingh1 wants to merge 1 commit into
Conversation
…name or colour
## Description
`syncCursorPositions` is the awareness-change handler: it runs on every
awareness update and reconciles `binding.cursors` against the current states.
It reads three fields from each peer's state, but only ever applies `name` and
`color` on the pass that first creates the cursor:
```js
const {name, color, focusing} = awareness;
...
let cursor = cursors.get(clientID);
if (cursor === undefined) {
cursor = createCursor(name, color);
cursors.set(clientID, cursor);
}
```
Every later pass updates only `anchor` / `focus`. `cursor.name` and
`cursor.color` are baked into the DOM once, when `createCursorSelection` builds
the caret (`name.textContent = cursor.name`, the caret and label background
from `cursor.color`) and registers the `::highlight()` rule via
`addCursorHighlightRule`. So a peer that renames itself or changes colour keeps
its old label and colour on every other client until it disconnects, which is
the only thing that removes the cursor.
This is reachable through the supported API rather than a hypothetical:
`LexicalCollaborationPlugin` takes `username` and `cursorColor` props, and
`useYjsCollaboration`'s effect lists `name` and `color` in its dependencies and
re-publishes local awareness through `initLocalState` when they change (
`useYjsFocusTracking` does the same via `setLocalStateFocus`). The local peer
therefore does broadcast the change; remote peers just ignore it. A common
case is a session that starts anonymous and picks up a real display name after
sign-in.
When the name or colour actually changes, this destroys the stale selection
(which releases its caret DOM and its `::highlight()` rule) and clears
`cursor.selection`, so the existing code below rebuilds it from the new values
on the same pass. A peer whose name and colour are unchanged keeps its cursor
object untouched, so there is no rebuild churn on ordinary cursor movement.
## Test plan
New unit test `packages/lexical-yjs/src/__tests__/unit/SyncCursorsAwarenessRefresh.test.ts`,
driving `syncCursorPositions` through its `getAwarenessStates` option. The
third case asserts an unchanged peer keeps the *same* cursor object, pinning
that the rebuild only happens when the awareness fields really change.
### Before
```
$ npx vitest run packages/lexical-yjs/src/__tests__/unit/SyncCursorsAwarenessRefresh.test.ts
× a peer that renames itself updates its cursor name 9ms
× a peer that changes colour updates its cursor colour 1ms
AssertionError: expected 'Bob' to be 'Robert' // Object.is equality
AssertionError: expected '#ff0000' to be '#0000ff' // Object.is equality
Tests 2 failed | 1 passed (3)
```
### After
```
$ npx vitest run packages/lexical-yjs
Test Files 6 passed (6)
Tests 81 passed (81)
$ npx vitest run packages/lexical-react
Test Files 31 passed (31)
Tests 182 passed (182)
```
The DOM rebuild itself is not exercised by the unit test — jsdom has no layout,
so `updateCursor` returns before touching the caret. It was not verified in a
real browser.
LeSingh1
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
August 9, 2026 05:18
|
@LeSingh1 is attempting to deploy a commit to the Meta Open Source Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
syncCursorPositionsis the awareness-change handler: it runs on everyawareness update and reconciles
binding.cursorsagainst the current states.It reads three fields from each peer's state, but only ever applies
nameandcoloron the pass that first creates the cursor:Every later pass updates only
anchor/focus.cursor.nameandcursor.colorare baked into the DOM once, whencreateCursorSelectionbuildsthe caret (
name.textContent = cursor.name, the caret and label backgroundfrom
cursor.color) and registers the::highlight()rule viaaddCursorHighlightRule. So a peer that renames itself or changes colour keepsits old label and colour on every other client until it disconnects, which is
the only thing that removes the cursor.
This is reachable through the supported API rather than a hypothetical:
LexicalCollaborationPlugintakesusernameandcursorColorprops, anduseYjsCollaboration's effect listsnameandcolorin its dependencies andre-publishes local awareness through
initLocalStatewhen they change (useYjsFocusTrackingdoes the same viasetLocalStateFocus). The local peertherefore does broadcast the change; remote peers just ignore it. A common
case is a session that starts anonymous and picks up a real display name after
sign-in.
When the name or colour actually changes, this destroys the stale selection
(which releases its caret DOM and its
::highlight()rule) and clearscursor.selection, so the existing code below rebuilds it from the new valueson the same pass. A peer whose name and colour are unchanged keeps its cursor
object untouched, so there is no rebuild churn on ordinary cursor movement.
Test plan
New unit test
packages/lexical-yjs/src/__tests__/unit/SyncCursorsAwarenessRefresh.test.ts,driving
syncCursorPositionsthrough itsgetAwarenessStatesoption. Thethird case asserts an unchanged peer keeps the same cursor object, pinning
that the rebuild only happens when the awareness fields really change.
Before
After
The DOM rebuild itself is not exercised by the unit test — jsdom has no layout,
so
updateCursorreturns before touching the caret. It was not verified in areal browser.