Fix highlight bug
What does this MR do and why?
Fixes #565353 (closed) — browser Ctrl+F (⌘+F) highlight disappears immediately after the browser scrolls to a match in the blob viewer.
Root cause
The blob viewer renders file content in 70-line chunks. Each chunk initially renders as plain {{ rawContent }} text so that browser find can locate text across the whole file. When a chunk becomes visible — including when the browser scrolls to it as a result of a find match — GlIntersectionObserver fires handleChunkAppear, setting hasAppeared = true. This causes shouldHighlight to become true, which triggers a v-if swap from {{ rawContent }} to v-safe-html="highlightedContent". The v-safe-html directive calls el.textContent = '' then el.appendChild(sanitize(...)), destroying the text node the browser's find was anchored to. The browser loses its match.
There is no JavaScript API to detect that the browser scrolled to a chunk because of a find match rather than user scrolling — window.getSelection() is empty during native find in all tested browsers. Any timing-based deferral approach either fires too early (bug persists) or introduces a visible rendering delay on every chunk scroll.
Solution — two-layer chunk rendering
Each chunk now renders two layers inside the <pre>:
Raw text layer (on top, z-index: 1):
- Rendered with
v-once— created once, never mutated by Vue color: transparent— invisible to the user, but the text node exists in the DOM- Browser find anchors here permanently — the anchor can never be broken
- Pointer events on this layer are forwarded to the highlighted layer below via
forwardEventToHighlight
Syntax-highlighted layer (underneath, position: absolute):
- Still gated by
v-if="shouldHighlight"— the lazy rendering performance model is fully preserved, off-screen chunks remain lightweight - Marked
inert— excluded from browser find entirely, preventing duplicate matches - May be freely added, removed, and re-rendered without affecting the find anchor
The inert attribute is the load-bearing piece: without it both layers would carry the same findable text and find would show 2× duplicate matches per search term. aria-hidden was considered but does not affect browser find — only display: none, visibility: hidden, and inert exclude content from find, and only inert keeps the layer visible to the user.
Code navigation
Because the highlighted layer is inert, pointer events on hljs spans (used for code navigation hover popovers and click handlers) would be dead. A forwarding bridge in forwardEventToHighlight handles this:
- Temporarily removes
inertfrom the highlighted layer - Uses
document.elementsFromPointto find the hljs span at the exact pointer coordinates - Dispatches a synthetic
MouseEventon that span — it bubbles normally so document-level delegation still fires - Re-adds
inertimmediately
This is wired to click, mouseover, and mouseout on the raw layer.
Screen recording
| Before | After |
|---|---|
Related issues
Closes #565353 (closed)
How to set up and validate locally
- Open a large file in the blob viewer, e.g.
https://gitlab.com/gitlab-org/gitlab-services/design.gitlab.com/-/blob/main/packages/gitlab-ui/src/tokens/build/css/tokens.css - Scroll to the middle of the page, copy a value from a line beyond the first 70 lines
- Scroll back to the top and refresh the page
- Press Ctrl+F (⌘+F), paste the value
- Observe that the browser scrolls to the match and the highlight persists
- Verify that code navigation (hover popovers, click-to-definition) still works on syntax-highlighted spans
MR acceptance checklist
- Evaluated against the MR acceptance checklist
Related to #565353 (closed)