Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ function getReviewPositionAnchor(position: PullRequestReviewPosition): {
* drafted rather than being posted as it is typed.
*/
function PullRequestCodeTab({
scrollerRef,
environmentId,
reference,
detail,
Expand All @@ -204,6 +205,7 @@ function PullRequestCodeTab({
onRefresh,
refreshToken = 0,
}: {
scrollerRef: (node: HTMLDivElement | null) => void;
environmentId: EnvironmentId;
reference: PullRequestRef;
detail: PullRequestDetailView;
Expand Down Expand Up @@ -1327,7 +1329,9 @@ function PullRequestCodeTab({
const withToolbar = (body: ReactNode) => (
<div className="flex h-full min-h-0 flex-col">
{toolbar}
<div className="min-h-0 flex-1 overflow-auto">{body}</div>
<div ref={scrollerRef} className="min-h-0 flex-1 overflow-auto">
{body}
</div>
</div>
);

Expand Down Expand Up @@ -1507,6 +1511,7 @@ function PullRequestCodeTab({
// interaction, but its native host outline clips and competes with the focus
// indicators on its actual controls.
className="h-full overflow-auto [scrollbar-gutter:stable]"
containerRef={scrollerRef}
viewerRef={setViewer}
items={items}
selectedLines={selectedLines}
Expand Down
145 changes: 115 additions & 30 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,43 +101,49 @@ vi.mock("./PullRequestThreadLinks", () => ({ PullRequestThreadLinks: () => null
vi.mock("./PullRequestSummaryTab", () => ({
PullRequestSummaryTab: ({
onFixFinding,
scrollerRef,
}: ComponentProps<typeof import("./PullRequestSummaryTab").PullRequestSummaryTab>) => (
<button
onClick={() =>
onFixFinding?.({
kind: "check",
check: { name: "Unit tests", status: "failure", description: "Test failed", url: null },
})
}
>
Fix check
</button>
<div ref={scrollerRef} data-test-scroll="summary">
<button
onClick={() =>
onFixFinding?.({
kind: "check",
check: { name: "Unit tests", status: "failure", description: "Test failed", url: null },
})
}
>
Fix check
</button>
</div>
),
}));
vi.mock("./PullRequestCodeTab", () => ({
default: ({
onAddToAgentSelection,
scrollerRef,
}: ComponentProps<typeof import("./PullRequestCodeTab").default>) => (
<button
onClick={() =>
onAddToAgentSelection?.({
request: "Fix this line",
comment: {
id: "note-1",
sectionId: "file:a.ts",
sectionTitle: "a.ts",
filePath: "a.ts",
startIndex: 0,
endIndex: 0,
rangeLabel: "L1",
text: "Please fix",
diff: "+broken()",
},
})
}
>
Add to agent
</button>
<div ref={scrollerRef} data-test-scroll="code">
<button
onClick={() =>
onAddToAgentSelection?.({
request: "Fix this line",
comment: {
id: "note-1",
sectionId: "file:a.ts",
sectionTitle: "a.ts",
filePath: "a.ts",
startIndex: 0,
endIndex: 0,
rangeLabel: "L1",
text: "Please fix",
diff: "+broken()",
},
})
}
>
Add to agent
</button>
</div>
),
}));

Expand Down Expand Up @@ -336,3 +342,82 @@ describe.each([
}
});
});

it("changes the title only for the active tab's main scroller", async () => {
const scrollers = new Map<string, { scrollTop: number; scrollHeight: number }>();
await act(async () => {
renderer = create(
<PullRequestDetailPanel
environmentId={threadRef.environmentId}
reference={detail}
context="page"
shortcutsEnabled={false}
getShortcutContext={() => ({
terminalFocus: false,
terminalOpen: false,
previewFocus: false,
previewOpen: false,
isWeb: true,
isDesktop: false,
})}
/>,
{
createNodeMock: (element) => {
const props = element.props as Record<string, unknown>;
const name = props["data-test-scroll"] as string | undefined;
if (name) {
const scroller = { scrollTop: 0, scrollHeight: 300 };
scrollers.set(name, scroller);
return scroller;
}
return { scrollHeight: props.inert === false ? 100 : 20 };
},
},
);
});

const mainScroll = renderer.root.findByProps({
className: "relative flex min-h-0 flex-1 flex-col overflow-hidden",
});
const titleFold = () => {
let node = renderer.root.findByType("h1");
while (typeof node.props.inert !== "boolean") node = node.parent!;
return node;
};
const summary = scrollers.get("summary")!;
const nested = { scrollTop: 200 };
await act(async () => mainScroll.props.onScrollCapture({ target: nested }));
expect(titleFold().props.inert).toBe(false);
summary.scrollTop = 180;
nested.scrollTop = 0;
await act(async () => {
mainScroll.props.onScrollCapture({ target: summary });
mainScroll.props.onScrollCapture({ target: nested });
});
expect(titleFold().props.inert).toBe(true);
expect(summary.scrollTop).toBe(100);
expect(nested.scrollTop).toBe(0);

await act(async () => mainScroll.props.onScrollCapture({ target: { scrollTop: 0 } }));
expect(titleFold().props.inert).toBe(true);
summary.scrollTop = 0;
await act(async () => mainScroll.props.onScrollCapture({ target: summary }));
expect(titleFold().props.inert).toBe(false);

await click("Code");
const code = scrollers.get("code")!;
summary.scrollTop = 180;
await act(async () => mainScroll.props.onScrollCapture({ target: summary }));
expect(titleFold().props.inert).toBe(false);
code.scrollTop = 180;
await act(async () => mainScroll.props.onScrollCapture({ target: code }));
expect(titleFold().props.inert).toBe(true);
expect(code.scrollTop).toBe(100);
await click("Summary");
expect(titleFold().props.inert).toBe(false);
await click("Code");
expect(titleFold().props.inert).toBe(true);
code.scrollTop = 0;
await act(async () => mainScroll.props.onScrollCapture({ target: code }));
expect(titleFold().props.inert).toBe(false);
});
49 changes: 39 additions & 10 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,18 +548,44 @@ export function PullRequestDetailPanel({
setChromeCondensed(chromeStateByTab.current[tab] ?? false);
}, [tab]);
const condensed = chromeCondensed;
const scrollerRef = useRef<HTMLElement | null>(null);
const scrollerByTab = useRef<Partial<Record<DetailTab, HTMLDivElement | null>>>({});
const scrollerRefs = useMemo(
() => ({
summary: (node: HTMLDivElement | null) => {
scrollerByTab.current.summary = node;
},
timeline: (node: HTMLDivElement | null) => {
scrollerByTab.current.timeline = node;
},
code: (node: HTMLDivElement | null) => {
scrollerByTab.current.code = node;
},
}),
[],
);
const foldRef = useRef<HTMLDivElement | null>(null);
const condensedRowRef = useRef<HTMLDivElement | null>(null);
// Refund after the fold commits so the content under the reader does not jump with its height.
const compensationRef = useRef<number | null>(null);
const compensationRef = useRef<{
tab: DetailTab;
scroller: HTMLDivElement;
delta: number;
} | null>(null);
useLayoutEffect(() => {
if (compensationRef.current === null) return;
const scroller = scrollerRef.current;
const delta = compensationRef.current;
const compensation = compensationRef.current;
if (compensation === null) return;
compensationRef.current = null;
if (scroller) scroller.scrollTop = Math.max(0, scroller.scrollTop + delta);
}, [condensed]);
if (
!condensed ||
compensation.tab !== tab ||
scrollerByTab.current[tab] !== compensation.scroller
)
return;
compensation.scroller.scrollTop = Math.max(
0,
compensation.scroller.scrollTop + compensation.delta,
);
}, [condensed, tab]);
const lastSelectedMergeMethod = useUiStateStore((state) => state.pullRequestMergeMethod);
const setLastSelectedMergeMethod = useUiStateStore((state) => state.setPullRequestMergeMethod);
// Server-side and per project, like every other project setting. The
Expand Down Expand Up @@ -2686,8 +2712,8 @@ export function PullRequestDetailPanel({
<div
className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
onScrollCapture={(event) => {
const scroller = event.target as HTMLElement;
scrollerRef.current = scroller;
const scroller = scrollerByTab.current[tab];
if (event.target !== scroller || scroller === null || scroller === undefined) return;
const top = scroller.scrollTop;
setChromeCondensed((previous) => {
let next = previous;
Expand All @@ -2702,7 +2728,7 @@ export function PullRequestDetailPanel({
next = false;
}
} else if (foldHeight > 0 && top > foldHeight + 32) {
compensationRef.current = -chromeDelta;
compensationRef.current = { tab, scroller, delta: -chromeDelta };
next = true;
}
chromeStateByTab.current[tab] = next;
Expand All @@ -2722,6 +2748,7 @@ export function PullRequestDetailPanel({
{mountedTabs.has("summary") ? (
<div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>
<PullRequestSummaryTab
scrollerRef={scrollerRefs.summary}
environmentId={environmentId}
threadRef={threadRef}
reference={reference}
Expand Down Expand Up @@ -2749,6 +2776,7 @@ export function PullRequestDetailPanel({
/>
) : (
<PullRequestTimelineTab
scrollerRef={scrollerRefs.timeline}
detail={detail}
environmentId={environmentId}
threadRef={threadRef}
Expand All @@ -2764,6 +2792,7 @@ export function PullRequestDetailPanel({
<div className={cn("absolute inset-0", tab !== "code" && "invisible")}>
<Suspense fallback={<DiffPanelLoadingState label="Loading pull request diff..." />}>
<PullRequestCodeTab
scrollerRef={scrollerRefs.code}
onAddToAgentSelection={addSelectionToAgent}
environmentId={environmentId}
reference={reference}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ afterEach(() => {
function render(value = detail) {
return (
<PullRequestSummaryTab
scrollerRef={() => {}}
environmentId={EnvironmentId.make("environment")}
threadRef={null}
reference={value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ function CommentGroup({
const COMMENT_PAGE = 10;

export function PullRequestSummaryTab({
scrollerRef,
environmentId,
threadRef,
reference,
Expand All @@ -469,6 +470,7 @@ export function PullRequestSummaryTab({
onRefresh,
onRefreshChecks = onRefresh,
}: {
scrollerRef: (node: HTMLDivElement | null) => void;
environmentId: EnvironmentId;
threadRef: ScopedThreadRef | null;
reference: PullRequestRef;
Expand Down Expand Up @@ -708,7 +710,7 @@ export function PullRequestSummaryTab({
};

return (
<div className="h-full overflow-y-auto" data-pull-request-summary-scroll>
<div ref={scrollerRef} className="h-full overflow-y-auto" data-pull-request-summary-scroll>
<section className="px-4 pt-2.5 pb-1">
<div className="space-y-2">
<MetaRow icon={<UsersIcon className="size-3.5" />} label="Reviewers">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ function ReviewVerdictEvent({
}

export function PullRequestTimelineTab({
scrollerRef,
detail,
environmentId,
threadRef = null,
Expand All @@ -546,6 +547,7 @@ export function PullRequestTimelineTab({
onOpenCommit,
onRefresh,
}: {
scrollerRef: (node: HTMLDivElement | null) => void;
detail: PullRequestDetailView;
environmentId: EnvironmentId;
threadRef?: ScopedThreadRef | null;
Expand Down Expand Up @@ -577,7 +579,7 @@ export function PullRequestTimelineTab({
};

return (
<div className="h-full overflow-y-auto px-4 py-5">
<div ref={scrollerRef} className="h-full overflow-y-auto px-4 py-5">
<div className="mx-auto max-w-3xl">
<div className="relative">
<span aria-hidden className="absolute bottom-5 left-[15px] top-1 w-px bg-border/45" />
Expand Down
Loading