Skip to content

Commit 0757d05

Browse files
committed
Fix sticky scroll
1 parent e1635cb commit 0757d05

File tree

1 file changed

+62
-17
lines changed

1 file changed

+62
-17
lines changed

webviews/editorWebview/overview.tsx

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,92 @@ const useMediaQuery = (query: string) => {
3232
export const Overview = (pr: PullRequest) => {
3333
const isSingleColumnLayout = useMediaQuery('(max-width: 768px)');
3434
const titleRef = React.useRef<HTMLDivElement>(null);
35+
const stickyHeightRef = React.useRef(0);
36+
const collapseDeltaRef = React.useRef(0);
3537

3638
React.useEffect(() => {
3739
const title = titleRef.current;
38-
40+
3941
if (!title) {
4042
return;
4143
}
4244

43-
// Initially ensure title is not stuck
44-
title.classList.remove('stuck');
45-
4645
// Small threshold to account for sub-pixel rendering
4746
const STICKY_THRESHOLD = 1;
4847

48+
const measureStickyMetrics = () => {
49+
const wasStuck = title.classList.contains('stuck');
50+
if (!wasStuck) {
51+
title.classList.remove('stuck');
52+
}
53+
54+
const unstuckHeight = title.getBoundingClientRect().height;
55+
title.classList.add('stuck');
56+
const stuckHeight = title.getBoundingClientRect().height;
57+
stickyHeightRef.current = stuckHeight;
58+
collapseDeltaRef.current = Math.max(0, unstuckHeight - stuckHeight);
59+
60+
if (!wasStuck) {
61+
title.classList.remove('stuck');
62+
}
63+
};
64+
65+
const hasEnoughScroll = () => {
66+
const doc = document.documentElement;
67+
const body = document.body;
68+
const scrollHeight = Math.max(doc.scrollHeight, body.scrollHeight);
69+
const availableScroll = scrollHeight - window.innerHeight;
70+
const adjustment = title.classList.contains('stuck') ? collapseDeltaRef.current : 0;
71+
return availableScroll + adjustment >= stickyHeightRef.current;
72+
};
73+
4974
// Use scroll event with requestAnimationFrame to detect when title becomes sticky
5075
// Check if the title's top position is at the viewport top (sticky position)
5176
let ticking = false;
5277
const handleScroll = () => {
53-
if (!ticking) {
54-
window.requestAnimationFrame(() => {
55-
const rect = title.getBoundingClientRect();
56-
// Title is stuck when its top is at position 0 (sticky top: 0)
57-
if (rect.top <= STICKY_THRESHOLD) {
58-
title.classList.add('stuck');
59-
} else {
60-
title.classList.remove('stuck');
61-
}
62-
ticking = false;
63-
});
64-
ticking = true;
78+
if (ticking) {
79+
return;
6580
}
81+
82+
ticking = true;
83+
window.requestAnimationFrame(() => {
84+
if (!hasEnoughScroll()) {
85+
title.classList.remove('stuck');
86+
ticking = false;
87+
return;
88+
}
89+
90+
const rect = title.getBoundingClientRect();
91+
// Title is stuck when its top is at position 0 (sticky top: 0)
92+
if (rect.top <= STICKY_THRESHOLD) {
93+
title.classList.add('stuck');
94+
} else {
95+
title.classList.remove('stuck');
96+
}
97+
ticking = false;
98+
});
99+
};
100+
101+
const handleResize = () => {
102+
measureStickyMetrics();
103+
handleScroll();
66104
};
67105

106+
measureStickyMetrics();
107+
68108
// Check initial state after a brief delay to ensure layout is settled
69-
const timeoutId = setTimeout(handleScroll, 100);
109+
const timeoutId = setTimeout(() => {
110+
measureStickyMetrics();
111+
handleScroll();
112+
}, 100);
70113

71114
window.addEventListener('scroll', handleScroll, { passive: true });
115+
window.addEventListener('resize', handleResize);
72116

73117
return () => {
74118
clearTimeout(timeoutId);
75119
window.removeEventListener('scroll', handleScroll);
120+
window.removeEventListener('resize', handleResize);
76121
};
77122
}, []);
78123

0 commit comments

Comments
 (0)