diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
index 1339b46196b8..43340bd558a3 100644
--- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
@@ -196,6 +196,7 @@ function getReviewPositionAnchor(position: PullRequestReviewPosition): {
* drafted rather than being posted as it is typed.
*/
function PullRequestCodeTab({
+ scrollerRef,
environmentId,
reference,
detail,
@@ -208,6 +209,7 @@ function PullRequestCodeTab({
onRefresh,
refreshToken = 0,
}: {
+ scrollerRef: (node: HTMLDivElement | null) => void;
environmentId: EnvironmentId;
reference: PullRequestRef;
detail: PullRequestDetailView;
@@ -1390,7 +1392,9 @@ function PullRequestCodeTab({
const withToolbar = (body: ReactNode) => (
{toolbar}
-
{body}
+
+ {body}
+
);
@@ -1570,6 +1574,7 @@ function PullRequestCodeTab({
// interaction, but its native host outline clips and competes with the focus
// indicators on its actual controls.
className="@container h-full overflow-auto [scrollbar-gutter:stable]"
+ containerRef={scrollerRef}
viewerRef={setViewer}
items={items}
selectedLines={selectedLines}
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
index daf9356003af..b7c92720bbde 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
@@ -107,43 +107,49 @@ vi.mock("./PullRequestThreadLinks", () => ({ PullRequestThreadLinks: () => null
vi.mock("./PullRequestSummaryTab", () => ({
PullRequestSummaryTab: ({
onFixFinding,
+ scrollerRef,
}: ComponentProps) => (
-
+
+
+
),
}));
vi.mock("./PullRequestCodeTab", () => ({
default: ({
onAddToAgentSelection,
+ scrollerRef,
}: ComponentProps) => (
-
+
+
+
),
}));
@@ -342,3 +348,82 @@ describe.each([
}
});
});
+
+it("changes the title only for the active tab's main scroller", async () => {
+ const scrollers = new Map();
+ await act(async () => {
+ renderer = create(
+ ({
+ terminalFocus: false,
+ terminalOpen: false,
+ previewFocus: false,
+ previewOpen: false,
+ isWeb: true,
+ isDesktop: false,
+ })}
+ />,
+ {
+ createNodeMock: (element) => {
+ const props = element.props as Record;
+ 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);
+});
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
index 7f6a4592a0b4..3aa31fc020d6 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
@@ -549,18 +549,44 @@ export function PullRequestDetailPanel({
setChromeCondensed(chromeStateByTab.current[tab] ?? false);
}, [tab]);
const condensed = chromeCondensed;
- const scrollerRef = useRef(null);
+ const scrollerByTab = useRef>>({});
+ 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(null);
const condensedRowRef = useRef(null);
// Refund after the fold commits so the content under the reader does not jump with its height.
- const compensationRef = useRef(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);
const resolveProjectDefaultMergeMethod = usePullRequestDefaultMergeMethodResolver(
@@ -2676,8 +2702,8 @@ export function PullRequestDetailPanel({
{
- 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;
@@ -2692,7 +2718,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;
@@ -2718,6 +2744,7 @@ export function PullRequestDetailPanel({
{mountedTabs.has("summary") ? (
) : (
}>
{
function render(value = detail) {
return (
{}}
environmentId={EnvironmentId.make("environment")}
threadRef={null}
reference={value}
diff --git a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
index 581c4d0741d7..218efe25859f 100644
--- a/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
@@ -455,6 +455,7 @@ function CommentGroup({
const COMMENT_PAGE = 10;
export function PullRequestSummaryTab({
+ scrollerRef,
environmentId,
threadRef,
reference,
@@ -469,6 +470,7 @@ export function PullRequestSummaryTab({
onRefresh,
onRefreshChecks = onRefresh,
}: {
+ scrollerRef: (node: HTMLDivElement | null) => void;
environmentId: EnvironmentId;
threadRef: ScopedThreadRef | null;
reference: PullRequestRef;
@@ -708,7 +710,7 @@ export function PullRequestSummaryTab({
};
return (
-
+
} label="Reviewers">
diff --git a/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx b/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx
index c6174b54f135..0cfc8e20cc40 100644
--- a/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx
@@ -538,6 +538,7 @@ function ReviewVerdictEvent({
}
export function PullRequestTimelineTab({
+ scrollerRef,
detail,
environmentId,
threadRef = null,
@@ -546,6 +547,7 @@ export function PullRequestTimelineTab({
onOpenCommit,
onRefresh,
}: {
+ scrollerRef: (node: HTMLDivElement | null) => void;
detail: PullRequestDetailView;
environmentId: EnvironmentId;
threadRef?: ScopedThreadRef | null;
@@ -577,7 +579,7 @@ export function PullRequestTimelineTab({
};
return (
-