From 287f8c71c8223c436c78586479ebed22b558428d Mon Sep 17 00:00:00 2001 From: 1012ayush Date: Fri, 10 Jul 2026 11:18:38 +0530 Subject: [PATCH 1/3] Fix: update selection format and style on programmatic select --- .../__tests__/unit/LexicalSelection.test.ts | 38 +++++++++++++++++++ .../lexical/src/nodes/LexicalElementNode.ts | 10 +++++ packages/lexical/src/nodes/LexicalTextNode.ts | 8 ++++ 3 files changed, 56 insertions(+) diff --git a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts index 3d8c0cb7448..9cf97c8f796 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts @@ -58,6 +58,44 @@ function mapLatest(nodes: T[]): T[] { describe('LexicalSelection tests', () => { initializeUnitTest(testEnv => { + test('programmatic selection movement clears stale formatting', async () => { + const {editor} = testEnv; + + await editor.update(() => { + const root = $getRoot(); + const paragraph = $createParagraphNode(); + const text = $createTextNode('Hello World'); + paragraph.append(text); + root.append(paragraph); + + // 1. Bold "Hello" (first 5 chars) + text.select(0, 5); + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.formatText('bold'); + } + }); + + await editor.update(() => { + const selection = $getSelection(); + // Verify we are indeed bold + if ($isRangeSelection(selection)) { + expect(selection.hasFormat('bold')).toBe(true); + } + + // 2. Jump to the end (the unformatted " World" text) + $getRoot().selectEnd(); + }); + + await editor.update(() => { + const selection = $getSelection(); + // 3. Verification: format cache must be flushed + if ($isRangeSelection(selection)) { + expect(selection.hasFormat('bold')).toBe(false); + } + }); + }); + describe('Inserting text either side of inline elements', () => { const setup = async ( mode: 'start-of-paragraph' | 'mid-paragraph' | 'end-of-paragraph', diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index bde78ba53fe..c4e60f54b46 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -606,8 +606,18 @@ export class ElementNode 'element', ); } else { + // Check if we are jumping to a new container + const isDifferentNode = + selection.anchor.key !== key || selection.focus.key !== key; + selection.anchor.set(key, anchorOffset, 'element'); selection.focus.set(key, focusOffset, 'element'); + + // ONLY flush the cache if we programmatically moved to a new element + if (isDifferentNode) { + selection.format = 0; + selection.style = ''; + } selection.dirty = true; } return selection; diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index c0844fb2c35..a4ad4bb5665 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -896,6 +896,9 @@ export class TextNode extends LexicalNode implements InlineFormattableNode { 'text', ); } else { + const isDifferentNode = + selection.anchor.key !== key || selection.focus.key !== key; + const compositionKey = $getCompositionKey(); if ( compositionKey === selection.anchor.key || @@ -904,6 +907,11 @@ export class TextNode extends LexicalNode implements InlineFormattableNode { $setCompositionKey(key); } selection.setTextNodeRange(this, anchorOffset, this, focusOffset); + + if (isDifferentNode) { + selection.format = this.getFormat(); + selection.style = this.getStyle(); + } } return selection; } From cf9e345f9357e3f91634b8d5e412958183bca329 Mon Sep 17 00:00:00 2001 From: 1012ayush Date: Fri, 10 Jul 2026 19:18:56 +0530 Subject: [PATCH 2/3] Fix: revert ElementNode format wipe to restore format inheritance --- packages/lexical/src/nodes/LexicalElementNode.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index c4e60f54b46..bde78ba53fe 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -606,18 +606,8 @@ export class ElementNode 'element', ); } else { - // Check if we are jumping to a new container - const isDifferentNode = - selection.anchor.key !== key || selection.focus.key !== key; - selection.anchor.set(key, anchorOffset, 'element'); selection.focus.set(key, focusOffset, 'element'); - - // ONLY flush the cache if we programmatically moved to a new element - if (isDifferentNode) { - selection.format = 0; - selection.style = ''; - } selection.dirty = true; } return selection; From f978d582c6aab5ed613c600f10fe062cdc84a4dd Mon Sep 17 00:00:00 2001 From: 1012ayush Date: Sat, 11 Jul 2026 23:05:24 +0530 Subject: [PATCH 3/3] Fix: update programmatic selection to inherit exact format and style --- .../__tests__/unit/LexicalSelection.test.ts | 27 ++++++++++++------- .../lexical/src/nodes/LexicalElementNode.ts | 8 ++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts index 4c0ba5ff26c..2ad451dc732 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts @@ -61,44 +61,53 @@ function mapLatest(nodes: T[]): T[] { describe('LexicalSelection tests', () => { initializeUnitTest(testEnv => { - test('programmatic selection movement clears stale formatting', async () => { + test('programmatic selection movement updates format and style to match target node', async () => { const {editor} = testEnv; + // 1. Setup two differently formatted nodes await editor.update(() => { const root = $getRoot(); const paragraph = $createParagraphNode(); - const text = $createTextNode('Hello World'); - paragraph.append(text); + + // We will make text1 bold via the selection below + const text1 = $createTextNode('Hello'); + const text2 = $createTextNode(' World') + .toggleFormat('italic') + .setStyle('color: red;'); + + paragraph.append(text1, text2); root.append(paragraph); - // 1. Bold "Hello" (first 5 chars) - text.select(0, 5); + // Select the first node and format the selection to bold + text1.select(0, 5); const selection = $getSelection(); if ($isRangeSelection(selection)) { selection.formatText('bold'); } }); + // 2. Verify initial selection is bold (This will pass now!) await editor.update(() => { const selection = $getSelection(); - // Verify we are indeed bold if ($isRangeSelection(selection)) { expect(selection.hasFormat('bold')).toBe(true); + expect(selection.hasFormat('italic')).toBe(false); } - // 2. Jump to the end (the unformatted " World" text) + // Programmatically jump to the end of the document (which lands exactly on the italic/red text2) $getRoot().selectEnd(); }); + // 3. Verification: format and style must exactly match text2 (italic, red, NOT bold) await editor.update(() => { const selection = $getSelection(); - // 3. Verification: format cache must be flushed if ($isRangeSelection(selection)) { expect(selection.hasFormat('bold')).toBe(false); + expect(selection.hasFormat('italic')).toBe(true); + expect(selection.style).toBe('color: red;'); } }); }); - describe('Inserting text either side of inline elements', () => { const setup = async ( mode: 'start-of-paragraph' | 'mid-paragraph' | 'end-of-paragraph', diff --git a/packages/lexical/src/nodes/LexicalElementNode.ts b/packages/lexical/src/nodes/LexicalElementNode.ts index bde78ba53fe..5134b4cd5ce 100644 --- a/packages/lexical/src/nodes/LexicalElementNode.ts +++ b/packages/lexical/src/nodes/LexicalElementNode.ts @@ -606,9 +606,17 @@ export class ElementNode 'element', ); } else { + const isDifferentNode = + selection.anchor.key !== key || selection.focus.key !== key; + selection.anchor.set(key, anchorOffset, 'element'); selection.focus.set(key, focusOffset, 'element'); selection.dirty = true; + + if (isDifferentNode) { + selection.format = this.getTextFormat(); + selection.style = this.getTextStyle(); + } } return selection; }