Skip to content

Commit 57e0f40

Browse files
committed
fix(webui): More fixes [copilot]
1 parent dd4d50e commit 57e0f40

3 files changed

Lines changed: 88 additions & 15 deletions

File tree

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ export class ElementObserverManager {
415415
private observedElements: Map<HTMLElement, () => void>
416416
private pendingReconnectFrame: number | undefined
417417

418+
private pruneDetachedObservedElements(): void {
419+
for (const observedElement of Array.from(this.observedElements.keys())) {
420+
if (!document.contains(observedElement)) {
421+
this.observedElements.delete(observedElement)
422+
this.resizeObserver.unobserve(observedElement)
423+
}
424+
}
425+
}
426+
418427
private constructor() {
419428
this.observedElements = new Map()
420429

@@ -431,10 +440,12 @@ export class ElementObserverManager {
431440

432441
// Configure MutationObserver
433442
this.mutationObserver = new MutationObserver((mutations) => {
443+
this.pruneDetachedObservedElements()
434444
const targets = new Set<HTMLElement>()
435445

436446
mutations.forEach((mutation) => {
437447
const target = mutation.target as HTMLElement
448+
if (!document.contains(target)) return
438449
// Find the closest observed element
439450
let element = target
440451
while (element) {
@@ -465,6 +476,7 @@ export class ElementObserverManager {
465476
public observe(element: HTMLElement, callback: () => void): void {
466477
if (!element) return
467478
if (!document.contains(element)) return
479+
this.pruneDetachedObservedElements()
468480

469481
this.observedElements.set(element, callback)
470482
this.resizeObserver.observe(element)
@@ -482,6 +494,7 @@ export class ElementObserverManager {
482494
if (!element) return
483495
this.observedElements.delete(element)
484496
this.resizeObserver.unobserve(element)
497+
this.pruneDetachedObservedElements()
485498

486499
if (this.observedElements.size === 0) {
487500
if (this.pendingReconnectFrame) {
@@ -499,12 +512,7 @@ export class ElementObserverManager {
499512

500513
// MutationObserver has no per-element unobserve, so we reconnect once per frame.
501514
this.mutationObserver.disconnect()
502-
for (const observedElement of this.observedElements.keys()) {
503-
if (!document.contains(observedElement)) {
504-
this.observedElements.delete(observedElement)
505-
this.resizeObserver.unobserve(observedElement)
506-
}
507-
}
515+
this.pruneDetachedObservedElements()
508516

509517
if (this.observedElements.size === 0) {
510518
this.resizeObserver.disconnect()

packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useRef, useState } from 'react'
22
import { PreviewPopUp, type PreviewPopUpHandle } from './PreviewPopUp.js'
33
import type { Padding, Placement } from '@popperjs/core'
44
import { PreviewPopUpContent } from './PreviewPopUpContent.js'
@@ -394,8 +394,28 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
394394
const [previewContent, setPreviewContent] = useState<PreviewContentUI[] | null>(null)
395395
const [t, setTime] = useState<number | null>(null)
396396

397+
const closeSession = useCallback(() => {
398+
const previousHandle = currentHandle.current
399+
if (previousHandle) {
400+
currentHandle.current = undefined
401+
previousHandle.onClosed?.()
402+
}
403+
404+
setPreviewSession(null)
405+
setPreviewContent(null)
406+
setTime(null)
407+
}, [])
408+
409+
useEffect(() => {
410+
return () => {
411+
closeSession()
412+
}
413+
}, [closeSession])
414+
397415
const context: IPreviewPopUpContext = {
398416
requestPreview: (anchor, content, opts) => {
417+
closeSession()
418+
399419
if (opts?.time) {
400420
setTime(opts.time)
401421
} else {
@@ -412,16 +432,16 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
412432
setPreviewContent(content)
413433

414434
const handle: IPreviewPopUpSession = {
415-
close: () => {
416-
setPreviewSession(null)
417-
},
435+
close: closeSession,
418436
update: (contents) => {
437+
if (currentHandle.current !== handle) return
419438
if (contents) {
420439
setPreviewContent(contents)
421440
}
422441
previewRef.current?.update()
423442
},
424443
setPointerTime: (t) => {
444+
if (currentHandle.current !== handle) return
425445
setTime(t)
426446
},
427447
}

packages/webui/src/client/ui/RundownView.tsx

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
337337
})(
338338
class RundownViewContent extends React.Component<Translated<IPropsWithReady & ITrackedProps>, IState> {
339339
private _hideNotificationsAfterMount: number | undefined
340+
private _goToTopIdleCallback: number | undefined
341+
private _goToLiveSegmentShortTimeout: ReturnType<typeof setTimeout> | undefined
342+
private _goToLiveSegmentLongTimeout: ReturnType<typeof setTimeout> | undefined
343+
private _headerNoteHighlightTimeout: ReturnType<typeof setTimeout> | undefined
340344

341345
constructor(props: Translated<IPropsWithReady & ITrackedProps>) {
342346
super(props)
@@ -610,6 +614,7 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
610614
document.body.classList.remove('dark', 'vertical-overflow-only')
611615
document.documentElement.removeAttribute('data-bs-theme')
612616
window.removeEventListener('beforeunload', this.onBeforeUnload)
617+
this.clearPendingDeferredCallbacks()
613618
resetViewportScrollState()
614619

615620
documentTitle.set(null)
@@ -687,13 +692,37 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
687692
}
688693
}
689694

695+
private clearPendingDeferredCallbacks = () => {
696+
if (this._goToTopIdleCallback !== undefined) {
697+
window.cancelIdleCallback(this._goToTopIdleCallback)
698+
this._goToTopIdleCallback = undefined
699+
}
700+
if (this._goToLiveSegmentShortTimeout) {
701+
clearTimeout(this._goToLiveSegmentShortTimeout)
702+
this._goToLiveSegmentShortTimeout = undefined
703+
}
704+
if (this._goToLiveSegmentLongTimeout) {
705+
clearTimeout(this._goToLiveSegmentLongTimeout)
706+
this._goToLiveSegmentLongTimeout = undefined
707+
}
708+
if (this._headerNoteHighlightTimeout) {
709+
clearTimeout(this._headerNoteHighlightTimeout)
710+
this._headerNoteHighlightTimeout = undefined
711+
}
712+
}
713+
690714
private onGoToTop = () => {
691715
scrollToPosition(0).catch((error) => {
692716
if (!error.toString().match(/another scroll/)) console.warn(error)
693717
})
694718

695-
window.requestIdleCallback(
719+
if (this._goToTopIdleCallback !== undefined) {
720+
window.cancelIdleCallback(this._goToTopIdleCallback)
721+
}
722+
723+
this._goToTopIdleCallback = window.requestIdleCallback(
696724
() => {
725+
this._goToTopIdleCallback = undefined
697726
this.setState({
698727
followLiveSegments: true,
699728
})
@@ -703,6 +732,15 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
703732
}
704733

705734
private onGoToLiveSegment = () => {
735+
if (this._goToLiveSegmentShortTimeout) {
736+
clearTimeout(this._goToLiveSegmentShortTimeout)
737+
this._goToLiveSegmentShortTimeout = undefined
738+
}
739+
if (this._goToLiveSegmentLongTimeout) {
740+
clearTimeout(this._goToLiveSegmentLongTimeout)
741+
this._goToLiveSegmentLongTimeout = undefined
742+
}
743+
706744
if (
707745
this.props.playlist &&
708746
this.props.playlist.activationId &&
@@ -713,14 +751,16 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
713751
followLiveSegments: true,
714752
})
715753
// Small delay to ensure the nextPartInfo is available
716-
setTimeout(() => {
754+
this._goToLiveSegmentShortTimeout = setTimeout(() => {
755+
this._goToLiveSegmentShortTimeout = undefined
717756
if (this.props.playlist && this.props.playlist.nextPartInfo) {
718757
scrollToPartInstance(this.props.playlist.nextPartInfo.partInstanceId, true).catch((error) => {
719758
if (!error.toString().match(/another scroll/)) console.warn(error)
720759
})
721760
}
722761
}, 120)
723-
setTimeout(() => {
762+
this._goToLiveSegmentLongTimeout = setTimeout(() => {
763+
this._goToLiveSegmentLongTimeout = undefined
724764
this.setState({
725765
followLiveSegments: true,
726766
})
@@ -733,7 +773,8 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
733773
scrollToPartInstance(this.props.playlist.currentPartInfo.partInstanceId, true).catch((error) => {
734774
if (!error.toString().match(/another scroll/)) console.warn(error)
735775
})
736-
setTimeout(() => {
776+
this._goToLiveSegmentLongTimeout = setTimeout(() => {
777+
this._goToLiveSegmentLongTimeout = undefined
737778
this.setState({
738779
followLiveSegments: true,
739780
})
@@ -899,7 +940,11 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
899940
this.setState({
900941
isNotificationsCenterOpen: level === NoteSeverity.ERROR ? NoticeLevel.CRITICAL : NoticeLevel.WARNING,
901942
})
902-
setTimeout(
943+
if (this._headerNoteHighlightTimeout) {
944+
clearTimeout(this._headerNoteHighlightTimeout)
945+
this._headerNoteHighlightTimeout = undefined
946+
}
947+
this._headerNoteHighlightTimeout = setTimeout(
903948
function () {
904949
NotificationCenter.highlightSource(
905950
segmentId,

0 commit comments

Comments
 (0)