fix(chat): sticky user headers float mid-list in the virtualized timeline
The tanstack rows sit in a wrapper offset with transform: translateY(), and a transformed ancestor becomes the sticky containing block — turn headers stuck to the wrapper's overscan-dependent top edge instead of the scroll container, floating over the previous turn. Offset the wrapper with padding-top instead: identical geometry, sticky computes against the scroll container again, and the padding only changes when the virtual window shifts, not per scroll frame.
This commit is contained in:
@@ -1193,12 +1193,17 @@ const StaticHistoryList = React.memo(({ entries, engine, contentRef, scrollRef,
|
|||||||
if (engine === 'tanstack') {
|
if (engine === 'tanstack') {
|
||||||
const virtualItems = tanstackVirtualizer.getVirtualItems();
|
const virtualItems = tanstackVirtualizer.getVirtualItems();
|
||||||
const startOffset = virtualItems[0]?.start ?? 0;
|
const startOffset = virtualItems[0]?.start ?? 0;
|
||||||
// Rendered rows stay in normal flow inside a single translated wrapper
|
// Rendered rows stay in normal flow inside a single offset wrapper (not
|
||||||
// (not per-row absolute positioning) so per-turn sticky user headers
|
// per-row absolute positioning) so per-turn sticky user headers keep
|
||||||
// keep working against the scroll container.
|
// working against the scroll container. The offset MUST be padding, not
|
||||||
|
// transform: a transformed ancestor becomes the sticky containing block,
|
||||||
|
// so headers would stick to the wrapper's (arbitrary, overscan-dependent)
|
||||||
|
// top edge mid-list and float over the previous turn. Padding only
|
||||||
|
// changes when the virtual window shifts — not per scroll frame — so the
|
||||||
|
// layout cost is negligible.
|
||||||
return (
|
return (
|
||||||
<div ref={sizeContainerRef} className="relative w-full" style={{ height: tanstackVirtualizer.getTotalSize() }}>
|
<div ref={sizeContainerRef} className="relative w-full" style={{ height: tanstackVirtualizer.getTotalSize() }}>
|
||||||
<div style={{ transform: `translateY(${startOffset}px)` }}>
|
<div style={{ paddingTop: `${startOffset}px` }}>
|
||||||
{virtualItems.map((item) => {
|
{virtualItems.map((item) => {
|
||||||
const entry = renderEntries[item.index];
|
const entry = renderEntries[item.index];
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user