From 334039668d32d3e44b3538779adbc74ccca0965e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 25 Mar 2026 10:01:26 +0100 Subject: [PATCH] :bug: Fix wrong typography font size in sidebar when selecting text in Firefox (editor v2) --- .../editor/content/dom/TextNodeIterator.js | 7 ++++++ .../content/dom/TextNodeIterator.test.js | 25 +++++++++++++++++++ .../editor/controllers/SelectionController.js | 7 +++++- .../controllers/SelectionController.test.js | 24 ++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/text-editor/src/editor/content/dom/TextNodeIterator.js b/frontend/text-editor/src/editor/content/dom/TextNodeIterator.js index 4ef7ea69db..d855387603 100644 --- a/frontend/text-editor/src/editor/content/dom/TextNodeIterator.js +++ b/frontend/text-editor/src/editor/content/dom/TextNodeIterator.js @@ -291,6 +291,13 @@ export class TextNodeIterator { break; } } + // The loop exits when currentNode === endNode without yielding endNode. + // Callers (e.g. selection style merge) must visit every text/BR node in the + // range, including the last one, or the final span is omitted (e.g. empty + // paragraph with only
) and the sidebar shows "mixed" incorrectly. + if (this.#currentNode === endNode) { + yield this.#currentNode; + } } } diff --git a/frontend/text-editor/src/editor/content/dom/TextNodeIterator.test.js b/frontend/text-editor/src/editor/content/dom/TextNodeIterator.test.js index 66bbb27d30..a0a783973d 100644 --- a/frontend/text-editor/src/editor/content/dom/TextNodeIterator.test.js +++ b/frontend/text-editor/src/editor/content/dom/TextNodeIterator.test.js @@ -70,4 +70,29 @@ describe("TextNodeIterator", () => { textNodeIterator.nextNode(); expect(textNodeIterator.currentNode.nodeValue).toBe("Whatever"); }); + + test("collectFrom includes the end node (iterateFrom must yield end inclusive)", () => { + const rootNode = createRoot([ + createParagraph([createTextSpan(new Text("Hello"))]), + createParagraph([createTextSpan(createLineBreak())]), + ]); + const firstText = rootNode.firstChild.firstChild.firstChild; + const br = rootNode.lastChild.firstChild.firstChild; + const textNodeIterator = new TextNodeIterator(rootNode); + const nodes = textNodeIterator.collectFrom(firstText, br); + expect(nodes.length).toBe(2); + expect(nodes[0]).toBe(firstText); + expect(nodes[1]).toBe(br); + }); + + test("collectFrom with identical start and end returns one node", () => { + const rootNode = createRoot([ + createParagraph([createTextSpan(new Text("Hi"))]), + ]); + const text = rootNode.firstChild.firstChild.firstChild; + const textNodeIterator = new TextNodeIterator(rootNode); + const nodes = textNodeIterator.collectFrom(text, text); + expect(nodes.length).toBe(1); + expect(nodes[0]).toBe(text); + }); }); diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.js b/frontend/text-editor/src/editor/controllers/SelectionController.js index ba06969893..8486a021ed 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.js @@ -403,7 +403,12 @@ export class SelectionController extends EventTarget { this.#updateCurrentStyle(textSpan); } else { // SELECTION. - this.#updateCurrentStyleFrom(this.#anchorNode, this.#focusNode); + // Use range boundaries normalized to text nodes, not anchor/focus. + // Firefox may set anchorNode on the paragraph element and focusNode on a + // text node for word selection; passing those to #updateCurrentStyleFrom + // breaks TextNodeIterator and yields wrong styles (e.g. default 14px). + const { startNode, endNode } = this.getRanges(); + this.#updateCurrentStyleFrom(startNode, endNode); } this.dispatchEvent( new CustomEvent("stylechange", { diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.test.js b/frontend/text-editor/src/editor/controllers/SelectionController.test.js index ff7e372c9d..3fd51ae16f 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.test.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.test.js @@ -1666,6 +1666,30 @@ describe("SelectionController", () => { expect(selectionController.focusAtEnd).toBeTruthy(); }) + test("`currentStyle` uses text span font-size when anchor is paragraph (Firefox-style word selection)", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ + createParagraph([ + createTextSpan(new Text("Hello World"), { "font-size": "36" }), + ]), + ]); + const root = textEditorMock.root; + const paragraph = root.firstChild; + const textNode = paragraph.firstChild.firstChild; + const selection = document.getSelection(); + const selectionController = new SelectionController( + textEditorMock, + selection, + ); + textEditorMock.element.focus(); + // Anchor on the paragraph (child offset 0) and focus in the text node — matches + // Firefox when double-click selects a word; anchor/focus are not both text nodes. + selection.setBaseAndExtent(paragraph, 0, textNode, 5); + document.dispatchEvent(new Event("selectionchange")); + expect(selectionController.currentStyle.getPropertyValue("font-size")).toBe( + "36px", + ); + }); + test("`dispose` should release every held reference", () => { const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ createParagraphWith(["Hello, "], {