From 57705f6b567deb4a5b3e7ff9915108efa1d1fe17 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 11 Jun 2026 19:01:43 +0000 Subject: [PATCH] Preserve markdown selection during re-renders Co-Authored-By: Paperclip --- .../components/MarkdownBody.rerender.test.tsx | 27 +++++++++++++++++++ ui/src/components/MarkdownBody.tsx | 6 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/ui/src/components/MarkdownBody.rerender.test.tsx b/ui/src/components/MarkdownBody.rerender.test.tsx index 3843768d247..cb655537236 100644 --- a/ui/src/components/MarkdownBody.rerender.test.tsx +++ b/ui/src/components/MarkdownBody.rerender.test.tsx @@ -84,4 +84,31 @@ describe("MarkdownBody re-render stability (PAP-10767)", () => { expect(codeAfter).toBe(codeBefore); expect(anchorAfter).toBe(anchorBefore); }); + + it("preserves text selection across a parent re-render with unchanged props", () => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + flushSync(() => root?.render(tree(SAMPLE, queryClient))); + + const paragraph = container.querySelector("p"); + const textNode = paragraph?.firstChild; + expect(textNode?.nodeType).toBe(Node.TEXT_NODE); + + const range = document.createRange(); + range.setStart(textNode!, 0); + range.setEnd(textNode!, "Some text".length); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + expect(selection?.toString()).toBe("Some text"); + + flushSync(() => root?.render(tree(SAMPLE, queryClient))); + + expect(window.getSelection()?.toString()).toBe("Some text"); + }); }); diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 7afed5b2bae..dbea43c1097 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -1,4 +1,4 @@ -import { isValidElement, useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; +import { isValidElement, memo, useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { Check, Copy, ExternalLink, Github, WrapText } from "lucide-react"; import Markdown, { defaultUrlTransform, type Components, type Options } from "react-markdown"; @@ -563,7 +563,7 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b ); } -export function MarkdownBody({ +function MarkdownBodyImpl({ children, className, style, @@ -778,3 +778,5 @@ export function MarkdownBody({ ); } + +export const MarkdownBody = memo(MarkdownBodyImpl);