Skip to content

Commit 9701d73

Browse files
authored
Merge pull request #52 from bbc/upstream/fix-autoscroll-detaches-onair-line
Fix Autoscroll detaches from Onair Line
2 parents 36804d0 + d542aa9 commit 9701d73

File tree

4 files changed

+278
-149
lines changed

4 files changed

+278
-149
lines changed

packages/webui/src/client/lib/VirtualElement.tsx

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
1+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState, useRef } from 'react'
22
import { InView } from 'react-intersection-observer'
3+
import { getViewPortScrollingState } from './viewPort'
34

45
interface IElementMeasurements {
56
width: string | number
@@ -11,12 +12,14 @@ interface IElementMeasurements {
1112
id: string | undefined
1213
}
1314

14-
const OPTIMIZE_PERIOD = 5000
1515
const 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 = /^((?!chrome|android).)*safari/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

169238
function 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

Comments
 (0)