Skip to content

Commit 307c8a7

Browse files
committed
feat: added singleton resize manager
1 parent 8955053 commit 307c8a7

File tree

1 file changed

+115
-72
lines changed

1 file changed

+115
-72
lines changed

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

Lines changed: 115 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const SAFARI_VISIBILITY_DELAY = /^((?!chrome|android).)*safari/i.test(navigator.
4343
* }
4444
* @return {*} {(JSX.Element | null)}
4545
*/
46+
4647
export function VirtualElement({
4748
initialShow,
4849
placeholderHeight,
@@ -62,19 +63,21 @@ export function VirtualElement({
6263
id?: string | undefined
6364
className?: string
6465
}>): JSX.Element | null {
66+
const resizeObserverManager = ResizeObserverManager.getInstance()
6567
const [waitForInitialLoad, setWaitForInitialLoad] = useState(true)
6668
const [inView, setInView] = useState(initialShow ?? false)
6769
const [isShowingChildren, setIsShowingChildren] = useState(inView)
6870

6971
const [measurements, setMeasurements] = useState<IElementMeasurements | null>(null)
7072
const [ref, setRef] = useState<HTMLDivElement | null>(null)
7173

72-
// Store the old dashboard height to detect changes:
73-
let oldElementHeight: number | undefined
74-
let resizeTimeout: NodeJS.Timeout
74+
let inViewChangetimer: NodeJS.Timeout | undefined
7575

7676
const showPlaceholder = !isShowingChildren && !initialShow
7777

78+
const isCurrentlyObserving = useRef(false)
79+
const isTransitioning = useRef(false)
80+
7881
// Track the last visibility change to debounce
7982
const lastVisibilityChangeRef = useRef<number>(0)
8083

@@ -95,6 +98,27 @@ export function VirtualElement({
9598
[width, placeholderHeight]
9699
)
97100

101+
const handleResize = useCallback(() => {
102+
if (ref) {
103+
// Show children during measurement
104+
setIsShowingChildren(true)
105+
106+
requestAnimationFrame(() => {
107+
const measurements = measureElement(ref, placeholderHeight)
108+
if (measurements) {
109+
setMeasurements(measurements)
110+
111+
// Only hide children again if not in view
112+
if (!inView && measurements.clientHeight > 0) {
113+
setIsShowingChildren(false)
114+
} else {
115+
setIsShowingChildren(true)
116+
}
117+
}
118+
})
119+
}
120+
}, [ref, inView, placeholderHeight])
121+
98122
const onVisibleChanged = useCallback(
99123
(visible: boolean) => {
100124
const now = Date.now()
@@ -109,20 +133,33 @@ export function VirtualElement({
109133
return
110134
}
111135

112-
if (visible) {
113-
oldElementHeight = findElementHeight()
114-
if (ref) {
115-
console.log('Connecting observer for element', ref)
116-
resizeObserver.observe(ref)
117-
}
118-
} else {
119-
console.log('Disconnecting observer for element', ref)
120-
resizeObserver.disconnect()
136+
setInView(visible)
137+
138+
// Don't do updates while transitioning:
139+
if (isTransitioning.current) {
140+
return
121141
}
142+
isTransitioning.current = true
122143

123-
setInView(visible)
144+
// Clear any existing timers
145+
if (inViewChangetimer) {
146+
clearTimeout(inViewChangetimer)
147+
}
148+
149+
// Delay the observer connection to prevent flickering
150+
inViewChangetimer = setTimeout(() => {
151+
if (visible) {
152+
if (ref) {
153+
if (!isCurrentlyObserving.current) {
154+
resizeObserverManager.observe(ref, handleResize)
155+
isCurrentlyObserving.current = true
156+
}
157+
}
158+
}
159+
isTransitioning.current = false
160+
}, 100)
124161
},
125-
[inView]
162+
[ref]
126163
)
127164

128165
const isScrolling = (): boolean => {
@@ -138,62 +175,26 @@ export function VirtualElement({
138175
return false
139176
}
140177

141-
function handleResize() {
142-
if (ref) {
143-
// Show children during measurement
144-
setIsShowingChildren(true)
145-
if (resizeTimeout) {
146-
clearTimeout(resizeTimeout)
147-
}
148-
resizeTimeout = setTimeout(() => {
149-
requestAnimationFrame(() => {
150-
const measurements = measureElement(ref, placeholderHeight)
151-
if (measurements) {
152-
setMeasurements(measurements)
153-
154-
// Only hide children again if not in view
155-
if (!inView && measurements.clientHeight > 0) {
156-
setIsShowingChildren(false)
157-
} else {
158-
setIsShowingChildren(true)
159-
}
160-
}
161-
})
162-
}, 50)
163-
}
164-
}
165-
166-
const findDashboardPanel = (): HTMLElement | null => {
167-
const timelineWrapper = ref?.closest('.segment-timeline-wrapper--shelf')
168-
const dashboardPanel = timelineWrapper?.querySelector('.dashboard-panel')
169-
if (dashboardPanel) {
170-
return dashboardPanel as HTMLElement
178+
useEffect(() => {
179+
// Setup initial observer if element is in view
180+
if (ref && inView && !isCurrentlyObserving.current) {
181+
resizeObserverManager.observe(ref, handleResize)
182+
isCurrentlyObserving.current = true
171183
}
172-
return null
173-
}
174-
175-
function findElementHeight(): number {
176-
const dashboardElement = findDashboardPanel()
177-
// Get heigth of timeline wrapper
178-
const dashboardHeight = dashboardElement?.clientHeight || 0
179-
const totalHeight = ref?.clientHeight || placeholderHeight || 160 + dashboardHeight
180184

181-
return totalHeight
182-
}
183-
184-
// Handle Viewport heigth changes:
185-
const resizeObserver = new ResizeObserver(() => {
186-
console.log('Observing resize')
187-
188-
const elementHeight = findElementHeight()
189-
console.log('elementHeight', elementHeight, 'oldElementHeight', oldElementHeight)
185+
// Cleanup function
186+
return () => {
187+
// Clean up resize observer
188+
if (ref && isCurrentlyObserving.current) {
189+
resizeObserverManager.unobserve(ref)
190+
isCurrentlyObserving.current = false
191+
}
190192

191-
if (elementHeight && elementHeight !== oldElementHeight) {
192-
console.log('dashboard containerHeigth changed to ', elementHeight, 'from', oldElementHeight)
193-
oldElementHeight = elementHeight
194-
handleResize()
193+
if (inViewChangetimer) {
194+
clearTimeout(inViewChangetimer)
195+
}
195196
}
196-
})
197+
}, [ref, inView])
197198

198199
useEffect(() => {
199200
if (inView === true) {
@@ -207,12 +208,7 @@ export function VirtualElement({
207208
setMeasurements(measurements)
208209
setWaitForInitialLoad(false)
209210
}
210-
// Setup initial resize observers
211-
oldElementHeight = findElementHeight()
212-
if (ref) {
213-
resizeObserver.observe(ref)
214-
}
215-
}, 2000)
211+
}, 800)
216212

217213
return () => {
218214
window.clearTimeout(initialMeasurementTimeout)
@@ -351,3 +347,50 @@ function measureElement(wrapperEl: HTMLDivElement, placeholderHeight?: number):
351347
id: el.id,
352348
}
353349
}
350+
351+
// Singleton class to manage ResizeObserver instances
352+
export class ResizeObserverManager {
353+
private static instance: ResizeObserverManager
354+
private resizeObserver: ResizeObserver
355+
private observedElements: Map<HTMLElement, (entry: ResizeObserverEntry) => void>
356+
357+
private constructor() {
358+
this.observedElements = new Map()
359+
this.resizeObserver = new ResizeObserver((entries) => {
360+
entries.forEach((entry) => {
361+
const element = entry.target as HTMLElement
362+
const callback = this.observedElements.get(element)
363+
if (callback) {
364+
callback(entry)
365+
}
366+
})
367+
})
368+
}
369+
370+
public static getInstance(): ResizeObserverManager {
371+
if (!ResizeObserverManager.instance) {
372+
ResizeObserverManager.instance = new ResizeObserverManager()
373+
}
374+
return ResizeObserverManager.instance
375+
}
376+
377+
public observe(element: HTMLElement, callback: (entry: ResizeObserverEntry) => void): void {
378+
if (!element) return
379+
380+
// Store the callback in our map
381+
this.observedElements.set(element, callback)
382+
383+
// Start observing
384+
this.resizeObserver.observe(element)
385+
}
386+
387+
public unobserve(element: HTMLElement): void {
388+
if (!element) return
389+
390+
// Remove from our map
391+
this.observedElements.delete(element)
392+
393+
// Stop observing
394+
this.resizeObserver.unobserve(element)
395+
}
396+
}

0 commit comments

Comments
 (0)