fix: restore the jump-to-bottom glide and keep user state, selections and scroll anchors in windowed lists
Pause scroll writes while the jump-to-bottom smooth scroll runs; keep rows with pending questions, approvals, recent interactions and selection ends mounted; cache Mermaid results; copy text spanning unmounted rows; anchor the sidebar on id changes; disable native overflow-anchor for windowed lists; write terminal output on the leading edge and before clear; forward remote events every 16 ms; flush streaming text before copy/export; add performance principle P10 (no UX regressions).
This commit is contained in:
@@ -607,7 +607,26 @@ try {
|
||||
}
|
||||
await sleep(300)
|
||||
const after = await js(scroller)
|
||||
return { fields: { scrollTopBefore: before, scrollTopAfter: after, scrolledPx: before === null || after === null ? null : before - after } }
|
||||
// UX: large jumps (scrollbar drag, Home/End, flick) must not paint a frame
|
||||
// whose viewport is not covered by mounted rows (a blank screen).
|
||||
const blankFrames = await js(`new Promise((resolve) => {
|
||||
const c = document.querySelector('.chat-history-pane[data-active=true] .chat');
|
||||
const covered = () => {
|
||||
const v = c.getBoundingClientRect(), mid = v.top + v.height / 2;
|
||||
return [...c.querySelectorAll('.message-window-row')].some((row) => { const r = row.getBoundingClientRect(); return r.top <= mid && r.bottom >= mid; });
|
||||
};
|
||||
const targets = [0.5, 0.1, 0.9, 0.3, 0.7, 0.05, 0.95, 0.2, 0.8, 0.4].map((f) => Math.round((c.scrollHeight - c.clientHeight) * f));
|
||||
let blank = 0, checked = 0;
|
||||
const step = () => {
|
||||
const target = targets.shift();
|
||||
if (target === undefined) { resolve({ blank, checked }); return; }
|
||||
c.scrollTop = target;
|
||||
// The frame the jump is painted in: rAF runs before paint, after rendering work queued by the scroll.
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => { checked += 1; if (!covered()) blank += 1; setTimeout(step, 50); }));
|
||||
};
|
||||
step();
|
||||
})`)
|
||||
return { fields: { scrollTopBefore: before, scrollTopAfter: after, scrolledPx: before === null || after === null ? null : before - after, jumpBlankFrames: blankFrames.blank, jumpChecks: blankFrames.checked } }
|
||||
}))
|
||||
|
||||
await screenshot('long-conversation')
|
||||
|
||||
@@ -163,8 +163,22 @@ app.whenReady().then(async () => {
|
||||
const clicks = await evaluate(`Number(document.querySelector('[data-clicks]').dataset.clicks)`)
|
||||
await click('.topbar button')
|
||||
await waitFor(`Number(document.querySelector('[data-clicks]').dataset.clicks) === ${clicks + 1}`)
|
||||
// The jump-to-bottom button glides (UX): sample scrollTop each frame after
|
||||
// the click; an instant jump would show no position between start and end.
|
||||
await evaluate(`(() => { const e = document.querySelector('.chat-history-pane[data-active="true"] .chat');
|
||||
const samples = window.__jumpSamples = [e.scrollTop];
|
||||
const sample = () => { samples.push(e.scrollTop); if (samples.length < 90) requestAnimationFrame(sample); };
|
||||
requestAnimationFrame(sample); })()`)
|
||||
await click('.chat-scroll-to-bottom')
|
||||
await waitFor(`(() => { const e = document.querySelector('.chat-history-pane[data-active="true"] .chat'); return e.scrollHeight - e.clientHeight - e.scrollTop < 1; })()`)
|
||||
await waitFor('window.__jumpSamples.length >= 90')
|
||||
const glide = await evaluate(`(() => { const e = document.querySelector('.chat-history-pane[data-active="true"] .chat');
|
||||
const end = e.scrollHeight - e.clientHeight, s = window.__jumpSamples, start = s[0];
|
||||
const between = new Set(s.filter(v => v > start + 1 && v < end - 1).map(Math.round));
|
||||
const reappeared = !!document.querySelector('.chat-scroll-to-bottom');
|
||||
return { start, end, between: between.size, reappeared, samples: s.slice(0, 12).map(Math.round) }; })()`)
|
||||
assert.ok(glide.between >= 3, `${theme}/${enabled}: jump-to-bottom animates ${JSON.stringify(glide)}`)
|
||||
assert.equal(glide.reappeared, false, `${theme}/${enabled}: button stays hidden after the glide`)
|
||||
const bottom = await evaluate(`document.querySelector('.chat-history-pane[data-active="true"] article:last-child').getBoundingClientRect().bottom`)
|
||||
assert.ok(bottom <= geometry.composer.top, 'last message is above composer')
|
||||
assert.equal(await evaluate(`document.querySelector('[aria-label="Composer"]').value`), 'Draft')
|
||||
|
||||
Reference in New Issue
Block a user