1- import React , { useCallback , useEffect , useLayoutEffect , useMemo , useState } from 'react'
1+ import React , { useCallback , useEffect , useLayoutEffect , useMemo , useState , useRef } from 'react'
22import { InView } from 'react-intersection-observer'
3+ import { getViewPortScrollingState } from './viewPort'
34
45interface IElementMeasurements {
56 width : string | number
@@ -11,12 +12,14 @@ interface IElementMeasurements {
1112 id : string | undefined
1213}
1314
14- const OPTIMIZE_PERIOD = 5000
1515const IDLE_CALLBACK_TIMEOUT = 100
16+ // Increased delay for Safari, as Safari doesn't have scroll time when using 'smooth' scroll
17+ const SAFARI_VISIBILITY_DELAY = / ^ ( (? ! c h r o m e | a n d r o i d ) .) * s a f a r i / i. test ( navigator . userAgent ) ? 100 : 0
1618
1719/**
1820 * This is a component that allows optimizing the amount of elements present in the DOM through replacing them
1921 * with placeholders when they aren't visible in the viewport.
22+ * Scroll timing issues, should be handled in viewPort.tsx where the scrolling state is tracked.
2023 *
2124 * @export
2225 * @param {(React.PropsWithChildren<{
@@ -65,6 +68,9 @@ export function VirtualElement({
6568 const [ ref , setRef ] = useState < HTMLDivElement | null > ( null )
6669 const [ childRef , setChildRef ] = useState < HTMLElement | null > ( null )
6770
71+ // Track the last visibility change to debounce
72+ const lastVisibilityChangeRef = useRef < number > ( 0 )
73+
6874 const isMeasured = ! ! measurements
6975
7076 const styleObj = useMemo < React . CSSProperties > (
@@ -75,13 +81,43 @@ export function VirtualElement({
7581 marginLeft : measurements ?. marginLeft ,
7682 marginRight : measurements ?. marginRight ,
7783 marginBottom : measurements ?. marginBottom ,
84+ // These properties are used to ensure that if a prior element is changed from
85+ // placeHolder to element, the position of visible elements are not affected.
86+ contentVisibility : 'auto' ,
87+ containIntrinsicSize : `0 ${ measurements ?. clientHeight ?? placeholderHeight ?? '0' } px` ,
88+ contain : 'size layout' ,
7889 } ) ,
7990 [ width , measurements , placeholderHeight ]
8091 )
8192
82- const onVisibleChanged = useCallback ( ( visible : boolean ) => {
83- setInView ( visible )
84- } , [ ] )
93+ const onVisibleChanged = useCallback (
94+ ( visible : boolean ) => {
95+ const now = Date . now ( )
96+
97+ // Debounce visibility changes in Safari to prevent unnecessary recaconditions
98+ if ( SAFARI_VISIBILITY_DELAY > 0 && now - lastVisibilityChangeRef . current < SAFARI_VISIBILITY_DELAY ) {
99+ return
100+ }
101+
102+ lastVisibilityChangeRef . current = now
103+
104+ setInView ( visible )
105+ } ,
106+ [ inView ]
107+ )
108+
109+ const isScrolling = ( ) : boolean => {
110+ // Don't do updates while scrolling:
111+ if ( getViewPortScrollingState ( ) . isProgrammaticScrollInProgress ) {
112+ return true
113+ }
114+ // And wait if a programmatic scroll was done recently:
115+ const timeSinceLastProgrammaticScroll = Date . now ( ) - getViewPortScrollingState ( ) . lastProgrammaticScrollTime
116+ if ( timeSinceLastProgrammaticScroll < 100 ) {
117+ return true
118+ }
119+ return false
120+ }
85121
86122 useEffect ( ( ) => {
87123 if ( inView === true ) {
@@ -90,7 +126,20 @@ export function VirtualElement({
90126 }
91127
92128 let idleCallback : number | undefined
93- const optimizeTimeout = window . setTimeout ( ( ) => {
129+ let optimizeTimeout : number | undefined
130+
131+ const scheduleOptimization = ( ) => {
132+ if ( optimizeTimeout ) {
133+ window . clearTimeout ( optimizeTimeout )
134+ }
135+ // Don't proceed if we're scrolling
136+ if ( isScrolling ( ) ) {
137+ // Reschedule for after the scroll should be complete
138+ const scrollDelay = 400
139+ window . clearTimeout ( optimizeTimeout )
140+ optimizeTimeout = window . setTimeout ( scheduleOptimization , scrollDelay )
141+ return
142+ }
94143 idleCallback = window . requestIdleCallback (
95144 ( ) => {
96145 if ( childRef ) {
@@ -102,16 +151,20 @@ export function VirtualElement({
102151 timeout : IDLE_CALLBACK_TIMEOUT ,
103152 }
104153 )
105- } , OPTIMIZE_PERIOD )
154+ }
155+
156+ // Schedule the optimization:
157+ scheduleOptimization ( )
106158
107159 return ( ) => {
108160 if ( idleCallback ) {
109161 window . cancelIdleCallback ( idleCallback )
110162 }
111-
112- window . clearTimeout ( optimizeTimeout )
163+ if ( optimizeTimeout ) {
164+ window . clearTimeout ( optimizeTimeout )
165+ }
113166 }
114- } , [ childRef , inView ] )
167+ } , [ childRef , inView , measurements ] )
115168
116169 const showPlaceholder = ! isShowingChildren && ( ! initialShow || isMeasured )
117170
@@ -127,7 +180,13 @@ export function VirtualElement({
127180 const refreshSizingTimeout = window . setTimeout ( ( ) => {
128181 idleCallback = window . requestIdleCallback (
129182 ( ) => {
130- setMeasurements ( measureElement ( el ) )
183+ const newMeasurements = measureElement ( el )
184+ setMeasurements ( newMeasurements )
185+
186+ // Set CSS variable for expected height on parent element
187+ if ( ref && newMeasurements ) {
188+ ref . style . setProperty ( '--expected-height' , `${ newMeasurements . clientHeight } px` )
189+ }
131190 } ,
132191 {
133192 timeout : IDLE_CALLBACK_TIMEOUT ,
@@ -141,7 +200,7 @@ export function VirtualElement({
141200 }
142201 window . clearTimeout ( refreshSizingTimeout )
143202 }
144- } , [ ref , showPlaceholder ] )
203+ } , [ ref , showPlaceholder , measurements ] )
145204
146205 return (
147206 < InView
@@ -151,7 +210,17 @@ export function VirtualElement({
151210 className = { className }
152211 as = "div"
153212 >
154- < div ref = { setRef } >
213+ < div
214+ ref = { setRef }
215+ style = {
216+ // We need to set undefined if the height is not known, to allow the parent to calculate the height
217+ measurements
218+ ? {
219+ height : measurements . clientHeight + 'px' ,
220+ }
221+ : undefined
222+ }
223+ >
155224 { showPlaceholder ? (
156225 < div
157226 id = { measurements ?. id ?? id }
@@ -168,11 +237,26 @@ export function VirtualElement({
168237
169238function measureElement ( el : HTMLElement ) : IElementMeasurements | null {
170239 const style = window . getComputedStyle ( el )
171- const clientRect = el . getBoundingClientRect ( )
240+
241+ // Get children to be measured
242+ const segmentTimeline = el . querySelector ( '.segment-timeline' )
243+ const dashboardPanel = el . querySelector ( '.rundown-view-shelf.dashboard-panel' )
244+
245+ if ( ! segmentTimeline ) return null
246+
247+ // Segment height
248+ const segmentRect = segmentTimeline . getBoundingClientRect ( )
249+ let totalHeight = segmentRect . height
250+
251+ // Dashboard panel height if present
252+ if ( dashboardPanel ) {
253+ const panelRect = dashboardPanel . getBoundingClientRect ( )
254+ totalHeight += panelRect . height
255+ }
172256
173257 return {
174258 width : style . width || 'auto' ,
175- clientHeight : clientRect . height ,
259+ clientHeight : totalHeight ,
176260 marginTop : style . marginTop || undefined ,
177261 marginBottom : style . marginBottom || undefined ,
178262 marginLeft : style . marginLeft || undefined ,
0 commit comments