fix(CQ-WEB-9): Cache DOM measurement element in LinkAutocomplete
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

Replace per-keystroke DOM element creation/removal with a persistent
off-screen mirror element stored in useRef. The mirror and cursor span
are lazily created on first use and reused for all subsequent caret
position measurements, eliminating layout thrashing. Cleanup on
component unmount removes the element from the DOM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 18:32:50 -06:00
parent 214139f4d5
commit 952eeb7323

View File

@@ -53,6 +53,8 @@ export function LinkAutocomplete({
const dropdownRef = useRef<HTMLDivElement>(null);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const mirrorRef = useRef<HTMLDivElement | null>(null);
const cursorSpanRef = useRef<HTMLSpanElement | null>(null);
/**
* Search for knowledge entries matching the query.
@@ -132,15 +134,37 @@ export function LinkAutocomplete({
);
/**
* Calculate dropdown position relative to cursor
* Calculate dropdown position relative to cursor.
* Uses a persistent off-screen mirror element (via refs) to avoid
* creating and removing DOM nodes on every keystroke, which causes
* layout thrashing.
*/
const calculateDropdownPosition = useCallback(
(textarea: HTMLTextAreaElement, cursorIndex: number): { top: number; left: number } => {
// Create a mirror div to measure text position
const mirror = document.createElement("div");
const styles = window.getComputedStyle(textarea);
// Lazily create the mirror element once, then reuse it
if (!mirrorRef.current) {
const mirror = document.createElement("div");
mirror.style.position = "absolute";
mirror.style.visibility = "hidden";
mirror.style.height = "auto";
mirror.style.whiteSpace = "pre-wrap";
mirror.style.pointerEvents = "none";
document.body.appendChild(mirror);
mirrorRef.current = mirror;
// Copy relevant styles
const span = document.createElement("span");
span.textContent = "|";
cursorSpanRef.current = span;
}
const mirror = mirrorRef.current;
const cursorSpan = cursorSpanRef.current;
if (!cursorSpan) {
return { top: 0, left: 0 };
}
// Sync styles from the textarea so measurement is accurate
const styles = window.getComputedStyle(textarea);
const stylesToCopy = [
"fontFamily",
"fontSize",
@@ -161,31 +185,19 @@ export function LinkAutocomplete({
}
});
mirror.style.position = "absolute";
mirror.style.visibility = "hidden";
mirror.style.width = `${String(textarea.clientWidth)}px`;
mirror.style.height = "auto";
mirror.style.whiteSpace = "pre-wrap";
// Get text up to cursor
// Update content: text before cursor + cursor marker span
const textBeforeCursor = textarea.value.substring(0, cursorIndex);
mirror.textContent = textBeforeCursor;
// Create a span for the cursor position
const cursorSpan = document.createElement("span");
cursorSpan.textContent = "|";
mirror.appendChild(cursorSpan);
document.body.appendChild(mirror);
const textareaRect = textarea.getBoundingClientRect();
const cursorSpanRect = cursorSpan.getBoundingClientRect();
const top = cursorSpanRect.top - textareaRect.top + textarea.scrollTop + 20;
const left = cursorSpanRect.left - textareaRect.left + textarea.scrollLeft;
document.body.removeChild(mirror);
return { top, left };
},
[]
@@ -346,7 +358,8 @@ export function LinkAutocomplete({
}, [textareaRef, handleInput, handleKeyDown]);
/**
* Cleanup timeout and abort in-flight requests on unmount
* Cleanup timeout, abort in-flight requests, and remove the
* persistent mirror element on unmount
*/
useEffect(() => {
return (): void => {
@@ -356,6 +369,11 @@ export function LinkAutocomplete({
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
if (mirrorRef.current) {
document.body.removeChild(mirrorRef.current);
mirrorRef.current = null;
cursorSpanRef.current = null;
}
};
}, []);