diff --git a/src/components/Application.tsx b/src/components/Application.tsx index cf3141b6..e19a6a4e 100644 --- a/src/components/Application.tsx +++ b/src/components/Application.tsx @@ -26,6 +26,10 @@ import { type ApplicationProps } from '../typedefs/ApplicationProps'; import { type ApplicationRef } from '../typedefs/ApplicationRef'; const originalDefaultTextStyle = { ...TextStyle.defaultTextStyle }; +const isElementNode = (value: unknown): value is HTMLElement => !!value + && typeof value === 'object' + && 'nodeType' in value + && value.nodeType === 1; const ApplicationImplementation = forwardRef(function Application( props, @@ -71,7 +75,7 @@ const ApplicationImplementation = forwardRef(f { if ('current' in resizeTo) { - if (resizeTo.current instanceof HTMLElement) + if (isElementNode(resizeTo.current)) { application.resizeTo = resizeTo.current; } diff --git a/src/core/createRoot.tsx b/src/core/createRoot.tsx index 875782a3..083bfb4f 100644 --- a/src/core/createRoot.tsx +++ b/src/core/createRoot.tsx @@ -22,6 +22,9 @@ export function createRoot( options: CreateRootOptions = {}, ) { + const ownerDocument = target.ownerDocument ?? document; + const ownerWindow = ownerDocument.defaultView ?? window; + // Check against mistaken use of createRoot let root = roots.get(target); let applicationState = (root?.applicationState ?? { @@ -58,21 +61,28 @@ export function createRoot( if (!root) { - let canvas; + let canvas: HTMLCanvasElement | undefined; + const ownerCanvasConstructor = ownerWindow.HTMLCanvasElement; - if (target instanceof HTMLCanvasElement) + if (ownerCanvasConstructor && target instanceof ownerCanvasConstructor) { canvas = target; } + else if (target.nodeName === 'CANVAS') + { + canvas = target as HTMLCanvasElement; + } if (!canvas) { - canvas = document.createElement('canvas'); + canvas = ownerDocument.createElement('canvas'); target.innerHTML = ''; target.appendChild(canvas); } internalState.canvas = canvas; + internalState.ownerDocument = ownerDocument; + internalState.ownerWindow = ownerWindow; const render = async ( children: ReactNode, diff --git a/src/typedefs/InternalState.ts b/src/typedefs/InternalState.ts index 4943bee4..96656bd5 100644 --- a/src/typedefs/InternalState.ts +++ b/src/typedefs/InternalState.ts @@ -3,5 +3,7 @@ import { type HostConfig } from './HostConfig'; export interface InternalState { canvas?: HTMLCanvasElement; + ownerDocument?: Document; + ownerWindow?: Window; rootContainer: HostConfig['containerInstance']; } diff --git a/test/e2e/components/Application.test.tsx b/test/e2e/components/Application.test.tsx index 38653707..6669a210 100644 --- a/test/e2e/components/Application.test.tsx +++ b/test/e2e/components/Application.test.tsx @@ -2,6 +2,7 @@ import { Application as PixiApplication, type DestroyOptions, extensions as Pixi import { createContext, createRef, + type RefObject, useContext, useEffect, } from 'react'; @@ -47,6 +48,32 @@ describe('Application', () => expect(ref.current?.getCanvas()).toBeInstanceOf(HTMLCanvasElement); }); + it('supports resizeTo refs from another window', async () => + { + const onInitSpy = vi.fn(); + const appRef = createRef(); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + + const iframeDocument = iframe.contentDocument!; + const resizeTarget = iframeDocument.createElement('div'); + iframeDocument.body.appendChild(resizeTarget); + + const resizeToRef = { current: resizeTarget } as RefObject; + + await act(async () => render(( + + ))); + + await expect.poll(() => onInitSpy.mock.calls.length).toEqual(1); + await expect.poll(() => appRef.current?.getApplication()?.resizeTo).toBe(resizeTarget); + + iframe.remove(); + }); + it('forwards context', async () => { const onInitSpy = vi.fn(); diff --git a/test/unit/core/createRoot.test.ts b/test/unit/core/createRoot.test.ts index 871dbe55..03b9cbb2 100644 --- a/test/unit/core/createRoot.test.ts +++ b/test/unit/core/createRoot.test.ts @@ -61,4 +61,30 @@ describe('createRoot', () => expect(root.applicationState.rendererDestroyOptions).toEqual({ removeView: true }); }); }); + + it('creates roots for iframe-owned elements', () => + { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + + const iframeDocument = iframe.contentDocument!; + const iframeCanvas = iframeDocument.createElement('canvas'); + const iframeContainer = iframeDocument.createElement('div'); + + iframeDocument.body.appendChild(iframeCanvas); + iframeDocument.body.appendChild(iframeContainer); + + const canvasRoot = createRoot(iframeCanvas as unknown as HTMLCanvasElement); + const containerRoot = createRoot(iframeContainer as unknown as HTMLElement); + + const createdCanvas = iframeContainer.querySelector('canvas'); + + expect(canvasRoot.applicationState.app).toBeInstanceOf(Application); + expect(containerRoot.applicationState.app).toBeInstanceOf(Application); + expect(createdCanvas?.nodeName).toBe('CANVAS'); + expect(createdCanvas?.ownerDocument).toBe(iframeDocument); + expect(containerRoot.internalState.canvas).toBe(createdCanvas); + + iframe.remove(); + }); });