Skip to content

Commit ebcfb3f

Browse files
committed
fix(webui): Try and fix last popup preview leaks [copilot]
1 parent 3bbaa23 commit ebcfb3f

5 files changed

Lines changed: 47 additions & 35 deletions

File tree

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ export const PreviewPopUp = React.forwardRef<
6060
}),
6161
[padding]
6262
)
63+
const virtualPositionRef = useRef({
64+
x: initialOffsetX ?? anchor?.getBoundingClientRect().x ?? 0,
65+
y: anchor?.getBoundingClientRect().y ?? 0,
66+
})
6367
const virtualElement = useRef<VirtualElement>({
64-
getBoundingClientRect: generateGetBoundingClientRect(
65-
initialOffsetX ?? anchor?.getBoundingClientRect().x ?? 0,
66-
anchor?.getBoundingClientRect().y ?? 0
67-
),
68+
getBoundingClientRect: () =>
69+
generateVirtualBoundingClientRect(virtualPositionRef.current.x, virtualPositionRef.current.y),
6870
})
6971
const anchorRef = useRef(anchor)
7072
const anchorYRef = useRef(anchor?.getBoundingClientRect().y ?? 0)
@@ -83,13 +85,17 @@ export const PreviewPopUp = React.forwardRef<
8385
useEffect(() => {
8486
anchorRef.current = anchor
8587
anchorYRef.current = anchor?.getBoundingClientRect().y ?? 0
86-
}, [anchor])
88+
virtualPositionRef.current = {
89+
x: initialOffsetX ?? anchor?.getBoundingClientRect().x ?? 0,
90+
y: anchor?.getBoundingClientRect().y ?? 0,
91+
}
92+
}, [anchor, initialOffsetX])
8793

8894
useEffect(() => {
8995
if (trackMouse) {
9096
const listener = ({ clientX: x }: MouseEvent) => {
9197
if (isDetachedHTMLElementAnchor(anchorRef.current)) return
92-
virtualElement.current.getBoundingClientRect = generateGetBoundingClientRect(x, anchorYRef.current)
98+
virtualPositionRef.current = { x, y: anchorYRef.current }
9399
// If update is available, call it to reposition the popper:
94100
if (updateRef.current) {
95101
updateRef.current().catch((e) => console.error(e))
@@ -107,11 +113,7 @@ export const PreviewPopUp = React.forwardRef<
107113
return () => {
108114
anchorRef.current = null
109115
anchorYRef.current = 0
110-
// Clear the virtualElement completely to break closure references
111-
if (virtualElement.current) {
112-
virtualElement.current.getBoundingClientRect = generateGetBoundingClientRect(0, 0)
113-
virtualElement.current = null as any
114-
}
116+
virtualPositionRef.current = { x: 0, y: 0 }
115117
updateRef.current = null
116118
}
117119
}, [])
@@ -145,8 +147,8 @@ export type PreviewPopUpHandle = {
145147
readonly update: () => void
146148
}
147149

148-
function generateGetBoundingClientRect(x = 0, y = 0) {
149-
return () => ({
150+
function generateVirtualBoundingClientRect(x = 0, y = 0) {
151+
return {
150152
width: 0,
151153
height: 0,
152154
x: x,
@@ -156,5 +158,5 @@ function generateGetBoundingClientRect(x = 0, y = 0) {
156158
bottom: y,
157159
left: x,
158160
toJSON: () => '',
159-
})
161+
}
160162
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ export const PreviewPopUpContext = React.createContext<IPreviewPopUpContext>({
379379
})
380380

381381
interface PreviewSession {
382+
token: number
382383
anchor: HTMLElement | VirtualElement
383384
padding: Padding
384385
placement: Placement
@@ -391,6 +392,7 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
391392
const currentHandle = useRef<IPreviewPopUpSession>()
392393
const previewRef = useRef<PreviewPopUpHandle>(null)
393394
const closeSessionRef = useRef<() => void>(() => undefined)
395+
const sessionTokenRef = useRef(0)
394396

395397
const [previewSession, setPreviewSession] = useState<PreviewSession | null>(null)
396398
const [previewContent, setPreviewContent] = useState<PreviewContentUI[] | null>(null)
@@ -402,6 +404,8 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
402404
}
403405

404406
const closeSession = useCallback(() => {
407+
sessionTokenRef.current += 1
408+
405409
const previousHandle = currentHandle.current
406410
if (previousHandle) {
407411
currentHandle.current = undefined
@@ -425,11 +429,15 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
425429

426430
useEffect(() => {
427431
if (!previewSession) return
432+
const token = previewSession.token
428433
const anchor = previewSession.anchor
429434
if (!(anchor instanceof HTMLElement)) return
430435

431436
let rafHandle: number | undefined
437+
let disposed = false
432438
const checkAnchorConnection = () => {
439+
if (disposed) return
440+
if (sessionTokenRef.current !== token) return
433441
if (!anchor.isConnected) {
434442
closeSessionRef.current()
435443
return
@@ -440,6 +448,7 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
440448
rafHandle = window.requestAnimationFrame(checkAnchorConnection)
441449

442450
return () => {
451+
disposed = true
443452
if (rafHandle !== undefined) {
444453
window.cancelAnimationFrame(rafHandle)
445454
}
@@ -460,13 +469,15 @@ export function PreviewPopUpContextProvider({ children }: React.PropsWithChildre
460469

461470
closeSession()
462471
setPreviewSessionKey((prev) => prev + 1)
472+
const token = ++sessionTokenRef.current
463473

464474
if (opts?.time !== undefined) {
465475
setTime(opts.time)
466476
} else {
467477
setTime(null)
468478
}
469479
setPreviewSession({
480+
token,
470481
anchor,
471482
padding: opts?.padding ?? 0,
472483
placement: opts?.placement ?? 'top',

packages/webui/src/client/ui/PreviewPopUp/Previews/IFramePreview.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useRef } from 'react'
1+
import { useEffect, useRef } from 'react'
22
import { relativeToSiteRootUrl } from '../../../url.js'
33

44
interface IFramePreviewProps {
@@ -15,28 +15,24 @@ interface IFramePreviewProps {
1515
export function IFramePreview({ content }: IFramePreviewProps): React.ReactElement {
1616
const iFrameElement = useRef<HTMLIFrameElement>(null)
1717

18-
const onLoadListener = useCallback(() => {
19-
if (content.postMessage) {
20-
// use * as URL reference to avoid cors when posting message with new reference:
21-
iFrameElement.current?.contentWindow?.postMessage(content.postMessage, '*')
22-
}
23-
}, [content.postMessage, content.href])
24-
2518
useEffect(() => {
26-
// Create a stable reference to the iframe element:
2719
const currentIFrame = iFrameElement.current
2820
if (!currentIFrame) return
29-
currentIFrame.addEventListener('load', onLoadListener)
3021

31-
return () => currentIFrame.removeEventListener('load', onLoadListener)
32-
}, [onLoadListener])
22+
const postMessageToIFrame = () => {
23+
if (content.postMessage) {
24+
// use * as URL reference to avoid cors when posting message with new reference:
25+
currentIFrame.contentWindow?.postMessage(content.postMessage, '*')
26+
}
27+
}
3328

34-
// Handle postMessage updates when iframe is already loaded
35-
useEffect(() => {
36-
if (content.postMessage && iFrameElement.current?.contentWindow) {
37-
// use * as URL reference to avoid cors when posting message with new reference:
38-
iFrameElement.current.contentWindow.postMessage(content.postMessage, '*')
29+
currentIFrame.addEventListener('load', postMessageToIFrame)
30+
31+
if (currentIFrame.contentDocument?.readyState === 'complete') {
32+
postMessageToIFrame()
3933
}
34+
35+
return () => currentIFrame.removeEventListener('load', postMessageToIFrame)
4036
}, [content.postMessage, content.href])
4137

4238
const style: Record<string, string | number> = {}

packages/webui/src/client/ui/SegmentTimeline/Parts/InvalidPartCover.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useEffect, useRef } from 'react'
1+
import { useContext, useEffect, useRef } from 'react'
22
import type { PartInvalidReason } from '@sofie-automation/corelib/dist/dataModel/Part'
33
import { type IPreviewPopUpSession, PreviewPopUpContext } from '../../PreviewPopUp/PreviewPopUpContext.js'
44

@@ -12,12 +12,12 @@ interface IProps {
1212
}
1313

1414
export function InvalidPartCover({ className, invalidReason }: Readonly<IProps>): JSX.Element {
15-
const element = React.createRef<HTMLDivElement>()
15+
const element = useRef<HTMLDivElement>(null)
1616

1717
const previewContext = useContext(PreviewPopUpContext)
1818
const previewSession = useRef<IPreviewPopUpSession | null>(null)
1919

20-
function onMouseEnter(e: React.MouseEvent<HTMLDivElement>) {
20+
function onMouseEnter() {
2121
if (!element.current) {
2222
return
2323
}
@@ -28,7 +28,7 @@ export function InvalidPartCover({ className, invalidReason }: Readonly<IProps>)
2828
}
2929

3030
if (invalidReason?.message && !previewSession.current) {
31-
previewSession.current = previewContext.requestPreview(e.currentTarget, [
31+
previewSession.current = previewContext.requestPreview(element.current, [
3232
{
3333
type: 'warning',
3434
content: invalidReason.message,

packages/webui/src/client/ui/Shelf/DashboardPieceButton/usePreviewPopUpSession.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export function usePreviewPopUpSession(args: {
2525
} {
2626
const previewSessionRef = useRef<IPreviewPopUpSession | null>(null)
2727
const hoverTimeoutRef = useRef<number | null>(null)
28+
const isMountedRef = useRef(true)
2829

2930
useEffect(() => {
3031
return () => {
32+
isMountedRef.current = false
3133
if (hoverTimeoutRef.current) {
3234
Meteor.clearTimeout(hoverTimeoutRef.current)
3335
hoverTimeoutRef.current = null
@@ -48,6 +50,7 @@ export function usePreviewPopUpSession(args: {
4850
const startHoverTimeout = useCallback(() => {
4951
if (hoverTimeoutRef.current) Meteor.clearTimeout(hoverTimeoutRef.current)
5052
hoverTimeoutRef.current = Meteor.setTimeout(() => {
53+
if (!isMountedRef.current) return
5154
if (previewSessionRef.current) {
5255
previewSessionRef.current.close()
5356
previewSessionRef.current = null

0 commit comments

Comments
 (0)