Skip to content

Commit 9059766

Browse files
committed
fix(webview-ui): robustly center edit area when editing long historical messages after topic switch by recentering within Virtuoso scroller and deferring scroll
1 parent 15932f9 commit 9059766

File tree

1 file changed

+62
-19
lines changed

1 file changed

+62
-19
lines changed

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,29 @@ export const ChatRowContent = ({
166166

167167
// Handle edit button click
168168
const handleEditClick = useCallback(() => {
169-
// Pre-scroll the bubble container into view so the textarea can mount fully
169+
// Pre-scroll the bubble container into view so the textarea can mount fully.
170+
// Use center to give Virtuoso more room to render around the target.
170171
try {
171-
editAreaRef.current?.scrollIntoView({
172-
behavior: "auto",
173-
block: "nearest",
174-
inline: "nearest",
175-
})
172+
const el = editAreaRef.current
173+
if (el) {
174+
el.scrollIntoView({
175+
behavior: "auto",
176+
block: "center",
177+
inline: "nearest",
178+
})
179+
// Safety re-center on the next frame in case virtualization/layout shifts after state updates.
180+
requestAnimationFrame(() => {
181+
try {
182+
el.scrollIntoView({
183+
behavior: "auto",
184+
block: "center",
185+
inline: "nearest",
186+
})
187+
} catch {
188+
// no-op
189+
}
190+
})
191+
}
176192
} catch {
177193
// no-op
178194
}
@@ -195,7 +211,7 @@ export const ChatRowContent = ({
195211
}, [isEditing, message.text, message.images, mode])
196212

197213
// Ensure the edit textarea is focused and scrolled into view when entering edit mode.
198-
// Uses a short delay and a few animation frames to allow virtualization reflow before scrolling.
214+
// Uses a short delay and repeated frames to allow virtualization reflow before scrolling.
199215
useEffect(() => {
200216
if (!isEditing) return
201217

@@ -205,8 +221,7 @@ export const ChatRowContent = ({
205221
const getScrollContainer = (el: HTMLElement | null): HTMLElement | null => {
206222
if (!el) return null
207223
// ChatView sets the Virtuoso scroller with class "scrollable"
208-
const scroller = el.closest(".scrollable") as HTMLElement | null
209-
return scroller
224+
return el.closest(".scrollable") as HTMLElement | null
210225
}
211226

212227
const isFullyVisible = (el: HTMLElement): boolean => {
@@ -220,6 +235,22 @@ export const ChatRowContent = ({
220235
return topVisible && bottomVisible
221236
}
222237

238+
const centerInScroller = (el: HTMLElement) => {
239+
const scroller = getScrollContainer(el)
240+
if (!scroller) {
241+
// Fallback to element's own scroll logic
242+
el.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" })
243+
return
244+
}
245+
const rect = el.getBoundingClientRect()
246+
const containerRect = scroller.getBoundingClientRect()
247+
const elCenter = rect.top + rect.height / 2
248+
const containerCenter = containerRect.top + containerRect.height / 2
249+
const delta = elCenter - containerCenter
250+
// Adjust scrollTop by the delta between element center and container center
251+
scroller.scrollTop += delta
252+
}
253+
223254
const focusTextarea = () => {
224255
if (editTextAreaRef.current) {
225256
try {
@@ -231,23 +262,35 @@ export const ChatRowContent = ({
231262
}
232263

233264
let attempts = 0
234-
const maxAttempts = 10
265+
const maxAttempts = 20 // a bit more robust across topic switches
235266

236267
const step = () => {
237268
if (cancelled) return
238269
attempts += 1
239270

240-
const targetEl = (editTextAreaRef.current as HTMLTextAreaElement | null) ?? editAreaRef.current
271+
const targetEl =
272+
(editTextAreaRef.current as HTMLTextAreaElement | null) ?? (editAreaRef.current as HTMLElement | null)
241273

242274
// Focus first so caret is visible; preventScroll avoids double-jump
243275
focusTextarea()
244276

245-
if (targetEl && !isFullyVisible(targetEl)) {
246-
targetEl.scrollIntoView({
247-
behavior: "smooth",
248-
block: "center",
249-
inline: "nearest",
250-
})
277+
if (targetEl) {
278+
if (!isFullyVisible(targetEl)) {
279+
// Prefer centering within the Virtuoso scroller; fallback to native scrollIntoView
280+
try {
281+
centerInScroller(targetEl)
282+
} catch {
283+
try {
284+
targetEl.scrollIntoView({
285+
behavior: "auto",
286+
block: "center",
287+
inline: "nearest",
288+
})
289+
} catch {
290+
// no-op
291+
}
292+
}
293+
}
251294
}
252295

253296
// Continue for a few frames to account for virtualization/layout reflows
@@ -256,12 +299,12 @@ export const ChatRowContent = ({
256299
}
257300
}
258301

259-
// Defer until after the textarea has mounted
302+
// Defer until after the textarea has mounted and Virtuoso has had a moment to lay out
260303
const timeoutId = window.setTimeout(() => {
261304
if (!cancelled) {
262305
step()
263306
}
264-
}, 50)
307+
}, 80)
265308

266309
return () => {
267310
cancelled = true

0 commit comments

Comments
 (0)