fix(chat): rest the jump-to-latest button on the composer, not the padded scroller
The button was a sticky child of the scroll container, which reserves composer-sized bottom padding. Sticky boxes are clamped by their containing block's content box, so the 12px offset never applied and the button parked 160px above the composer, on top of the transcript. Anchor it to the composer wrapper's own top edge instead, so the offset no longer depends on the --composer-reserve constant. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -199,7 +199,7 @@ test("aria-busy marks the updating message list without swallowing the status re
|
||||
});
|
||||
|
||||
test("the jump-to-latest control stays keyboard reachable and labelled", () => {
|
||||
const jumpControl = sourceBetween(pageSource, "{jumpToLatestVisible && (", "</div>\n )}");
|
||||
const jumpControl = sourceBetween(pageSource, "{jumpToLatestVisible && (", "</button>");
|
||||
const buttonTag = sourceBetween(jumpControl, "<button", ">");
|
||||
|
||||
// Given: a real button in document order, never removed from the tab sequence.
|
||||
|
||||
@@ -8,6 +8,7 @@ import { nextAnchorState } from "../src/hooks/use-conversation-scroll-anchor.ts"
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const noticeSource = readFileSync(new URL("../src/lib/chat-notice.ts", import.meta.url), "utf8");
|
||||
const anchorSource = readFileSync(new URL("../src/hooks/use-conversation-scroll-anchor.ts", import.meta.url), "utf8");
|
||||
const globalsSource = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
|
||||
function sourceBetween(source: string, startMarker: string, endMarker: string) {
|
||||
const start = source.indexOf(startMarker);
|
||||
@@ -86,7 +87,7 @@ test("scrolls to the newest turn on intentional jumps", () => {
|
||||
});
|
||||
|
||||
test("offers an accessible jump-to-latest control while reading history", () => {
|
||||
const jumpControl = sourceBetween(pageSource, "{jumpToLatestVisible && (", "</div>\n )}");
|
||||
const jumpControl = sourceBetween(pageSource, "{jumpToLatestVisible && (", "</button>");
|
||||
|
||||
assert.match(pageSource, /const jumpToLatestVisible = !rectificationSurfaceOpen[\s\S]*?&& !conversationAnchor\.anchored/);
|
||||
assert.match(jumpControl, /type="button"/);
|
||||
@@ -97,6 +98,19 @@ test("offers an accessible jump-to-latest control while reading history", () =>
|
||||
assert.match(jumpControl, /onClick=\{conversationAnchor\.anchorToLatest\}/);
|
||||
});
|
||||
|
||||
test("rests the jump-to-latest control on the composer instead of the padded scroller", () => {
|
||||
// Given: the scroll container reserves composer-sized padding, which clamps any sticky child
|
||||
// of it that far above the composer, on top of the transcript the reader is still reading.
|
||||
assert.match(globalsSource, /\.conversation \{[^}]*padding-bottom: var\(--composer-reserve\)/);
|
||||
assert.match(globalsSource, /\.composer-wrap \{[^}]*position: sticky/);
|
||||
|
||||
// Then: the control hangs off the composer's own top edge, whatever that reserve is.
|
||||
const composerWrap = sourceBetween(pageSource, "className={`composer-wrap ", "<ChatComposer");
|
||||
const jumpControl = sourceBetween(composerWrap, "{jumpToLatestVisible && (", "</button>");
|
||||
assert.match(jumpControl, /absolute inset-x-0 bottom-full/);
|
||||
assert.doesNotMatch(jumpControl, /sticky/);
|
||||
});
|
||||
|
||||
test("keeps the scroll listener passive and reduced-motion aware", () => {
|
||||
assert.match(anchorSource, /addEventListener\("scroll", onScroll, \{ passive: true \}\)/);
|
||||
assert.match(anchorSource, /frame = window\.requestAnimationFrame\(measure\)/);
|
||||
|
||||
Reference in New Issue
Block a user