@@ -32,47 +32,92 @@ const useMediaQuery = (query: string) => {
3232export 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