Skip to content

Commit 6b44cd4

Browse files
author
Eric Wheeler
committed
fix: improve code block scrolling behavior
Improve scrolling behavior for code blocks by: - Adding a consistent SCROLL_SNAP_TOLERANCE constant (20px) - Tracking outer container scroll position - Moving scrolling logic to happen immediately after Shiki highlighting completes - Ensuring both inner and outer containers scroll to bottom simultaneously when appropriate This creates a more responsive and consistent scrolling experience without relying on arbitrary timeouts. Signed-off-by: Eric Wheeler <[email protected]>
1 parent 3fcca27 commit 6b44cd4

File tree

1 file changed

+60
-17
lines changed

1 file changed

+60
-17
lines changed

webview-ui/src/components/common/CodeBlock.tsx

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export const WINDOW_SHADE_SETTINGS = {
1212
collapsedHeight: 500, // Default collapsed height in pixels
1313
}
1414

15+
// Tolerance in pixels for determining when a container is considered "at the bottom"
16+
export const SCROLL_SNAP_TOLERANCE = 20
17+
1518
/*
1619
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
1720
https://stackoverflow.com/questions/60778406/why-is-padding-right-clipped-with-overflowscroll/77292459#77292459
@@ -288,15 +291,18 @@ const CodeBlock = memo(
288291
// Ref to track if user was scrolled up *before* the source update potentially changes scrollHeight
289292
const wasScrolledUpRef = useRef(false)
290293

294+
// Ref to track if outer container was near bottom
295+
const outerContainerNearBottomRef = useRef(false)
296+
291297
// Effect to listen to scroll events and update the ref
292298
useEffect(() => {
293299
const preElement = preRef.current
294300
if (!preElement) return
295301

296302
const handleScroll = () => {
297-
const tolerance = 5 // Pixels tolerance for being "at the bottom"
298303
const isAtBottom =
299-
Math.abs(preElement.scrollHeight - preElement.scrollTop - preElement.clientHeight) < tolerance
304+
Math.abs(preElement.scrollHeight - preElement.scrollTop - preElement.clientHeight) <
305+
SCROLL_SNAP_TOLERANCE
300306
wasScrolledUpRef.current = !isAtBottom
301307
}
302308

@@ -309,23 +315,38 @@ const CodeBlock = memo(
309315
}
310316
}, []) // Empty dependency array: runs once on mount
311317

312-
// Scroll to bottom immediately when source changes, but only if user wasn't scrolled up *before* the update
318+
// Effect to track outer container scroll position
319+
useEffect(() => {
320+
const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]')
321+
if (!scrollContainer) return
322+
323+
const handleOuterScroll = () => {
324+
const isAtBottom =
325+
Math.abs(scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight) <
326+
SCROLL_SNAP_TOLERANCE
327+
outerContainerNearBottomRef.current = isAtBottom
328+
}
329+
330+
scrollContainer.addEventListener("scroll", handleOuterScroll, { passive: true })
331+
// Initial check
332+
handleOuterScroll()
333+
334+
return () => {
335+
scrollContainer.removeEventListener("scroll", handleOuterScroll)
336+
}
337+
}, []) // Empty dependency array: runs once on mount
338+
339+
// Store whether we should scroll after highlighting completes
340+
const shouldScrollAfterHighlightRef = useRef(false)
341+
342+
// Check if we should scroll when source changes
313343
useEffect(() => {
314-
// Check the ref *before* potentially scrolling
344+
// Only set the flag if we're at the bottom when source changes
315345
if (preRef.current && source && !wasScrolledUpRef.current) {
316-
// Use rAF to wait for DOM updates (like scrollHeight change)
317-
requestAnimationFrame(() => {
318-
// Use a small timeout to ensure rendering is complete, especially for complex highlights
319-
setTimeout(() => {
320-
if (preRef.current) {
321-
preRef.current.scrollTop = preRef.current.scrollHeight
322-
// After auto-scrolling, update the ref state as we are now at the bottom
323-
wasScrolledUpRef.current = false
324-
}
325-
}, 50) // Slightly increased delay
326-
})
346+
shouldScrollAfterHighlightRef.current = true
347+
} else {
348+
shouldScrollAfterHighlightRef.current = false
327349
}
328-
// We only need to react to source changes here. The ref check handles the scroll state.
329350
}, [source])
330351

331352
const updateCodeBlockButtonPosition = useCallback((forceHide = false) => {
@@ -412,10 +433,32 @@ const CodeBlock = memo(
412433
}
413434
}, [updateCodeBlockButtonPosition])
414435

415-
// Update button position when highlightedCode changes
436+
// Update button position and scroll when highlightedCode changes
416437
useEffect(() => {
417438
if (highlightedCode) {
439+
// Update button position
418440
setTimeout(updateCodeBlockButtonPosition, 0)
441+
442+
// Scroll to bottom if needed (immediately after Shiki updates)
443+
if (shouldScrollAfterHighlightRef.current) {
444+
// Scroll inner container
445+
if (preRef.current) {
446+
preRef.current.scrollTop = preRef.current.scrollHeight
447+
wasScrolledUpRef.current = false
448+
}
449+
450+
// Also scroll outer container if it was near bottom
451+
if (outerContainerNearBottomRef.current) {
452+
const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]')
453+
if (scrollContainer) {
454+
scrollContainer.scrollTop = scrollContainer.scrollHeight
455+
outerContainerNearBottomRef.current = true
456+
}
457+
}
458+
459+
// Reset the flag
460+
shouldScrollAfterHighlightRef.current = false
461+
}
419462
}
420463
}, [highlightedCode, updateCodeBlockButtonPosition])
421464

0 commit comments

Comments
 (0)