diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
index 251f3cd49f95..726bcdfae780 100644
--- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
@@ -192,6 +192,7 @@ function getReviewPositionAnchor(position: PullRequestReviewPosition): {
* drafted rather than being posted as it is typed.
*/
function PullRequestCodeTab({
+ scrollerRef,
environmentId,
reference,
detail,
@@ -204,6 +205,7 @@ function PullRequestCodeTab({
onRefresh,
refreshToken = 0,
}: {
+ scrollerRef: (node: HTMLDivElement | null) => void;
environmentId: EnvironmentId;
reference: PullRequestRef;
detail: PullRequestDetailView;
@@ -1327,7 +1329,9 @@ function PullRequestCodeTab({
const withToolbar = (body: ReactNode) => (
{toolbar}
-
{body}
+
+ {body}
+
);
@@ -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}
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
index 884bef0bf0b8..a7940caaf7b1 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.test.tsx
@@ -101,43 +101,49 @@ vi.mock("./PullRequestThreadLinks", () => ({ PullRequestThreadLinks: () => null
vi.mock("./PullRequestSummaryTab", () => ({
PullRequestSummaryTab: ({
onFixFinding,
+ scrollerRef,
}: ComponentProps) => (
-
+
+
+
),
}));
vi.mock("./PullRequestCodeTab", () => ({
default: ({
onAddToAgentSelection,
+ scrollerRef,
}: ComponentProps) => (
-
+
+
+
),
}));
@@ -336,3 +342,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 1b42fdd4481a..2b34fdeda2a2 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
@@ -548,18 +548,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);
// Server-side and per project, like every other project setting. The
@@ -2686,8 +2712,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;
@@ -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;
@@ -2722,6 +2748,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 cd2eaf6de501..1b07b4dd574d 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 9435304f6d7e..30bddef576e8 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 (
-