diff --git a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts index 13abe629d6f..2ad451dc732 100644 --- a/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts +++ b/packages/lexical/src/__tests__/unit/LexicalSelection.test.ts @@ -61,6 +61,53 @@ function mapLatest(nodes: T[]): T[] { describe('LexicalSelection tests', () => { initializeUnitTest(testEnv => { + 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(); + + // 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); + + // 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(); + if ($isRangeSelection(selection)) { + expect(selection.hasFormat('bold')).toBe(true); + expect(selection.hasFormat('italic')).toBe(false); + } + + // 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(); + 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; } diff --git a/packages/lexical/src/nodes/LexicalTextNode.ts b/packages/lexical/src/nodes/LexicalTextNode.ts index 6701990c605..165455c4807 100644 --- a/packages/lexical/src/nodes/LexicalTextNode.ts +++ b/packages/lexical/src/nodes/LexicalTextNode.ts @@ -897,6 +897,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 || @@ -905,6 +908,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; }