@samitouri / QOS-React-2 / commits / 82881ab261

Allow Inspecting Elements Within Iframes (#355)

This change adds support for element inspecting within `<iframe/>`s. The iframes are kept in a Set so that we only append listeners once and clean them up properly. I’ve run a few tests and it seems to behave as expected. The fixture includes a cross-origin iframe to make sure we do not error in this case.

Philipp Spiess committed Jul 28, 2019 at 17:40 UTC 82881ab261d35c7840e4599327c097955a40ed0f
4 files changed +108 -7
shells/dev/app/Iframe/index.js new
+67
@@ -0,0 +1,67 @@
1 +import React from 'react';
2 +import ReactDOM from 'react-dom';
3 +
4 +export default function Iframe() {
5 + return (
6 + <>
7 + <h2>Iframe</h2>
8 + <div>
9 + <Frame>
10 + <Greeting />
11 + </Frame>
12 + </div>
13 + </>
14 + );
15 +}
16 +
17 +const iframeStyle = { border: '2px solid #eee', height: 80 };
18 +
19 +function Frame(props) {
20 + const [element, setElement] = React.useState(null);
21 +
22 + const ref = React.useRef();
23 +
24 + React.useLayoutEffect(function() {
25 + const iframe = ref.current;
26 +
27 + if (iframe) {
28 + const html = `
29 + <!DOCTYPE html>
30 + <html>
31 + <body>
32 + <div id="root"></div>
33 + </body>
34 + </html>
35 + `;
36 +
37 + const document = iframe.contentDocument;
38 +
39 + document.open();
40 + document.write(html);
41 + document.close();
42 +
43 + setElement(document.getElementById('root'));
44 + }
45 + }, []);
46 +
47 + return (
48 + <>
49 + <iframe title="Test Iframe" ref={ref} style={iframeStyle} />
50 + <iframe
51 + title="Secured Iframe"
52 + src="https://example.com"
53 + style={iframeStyle}
54 + />
55 +
56 + {element ? ReactDOM.createPortal(props.children, element) : null}
57 + </>
58 + );
59 +}
60 +
61 +function Greeting() {
62 + return (
63 + <p>
64 + Hello from within an <code>&lt;iframe&gt;</code>!
65 + </p>
66 + );
67 +}
shells/dev/app/index.js
+2
@@ -8,6 +8,7 @@ import {
8 unstable_createRoot as createRoot,
9 } from 'react-dom';
10 import DeeplyNestedComponents from './DeeplyNestedComponents';
11 +import Iframe from './Iframe';
12 import EditableProps from './EditableProps';
13 import ElementTypes from './ElementTypes';
14 import Hydration from './Hydration';
@@ -46,6 +47,7 @@ function mountTestApp() {
47 mountHelper(Toggle);
48 mountHelper(SuspenseTree);
49 mountHelper(DeeplyNestedComponents);
50 + mountHelper(Iframe);
51 }
52
53 function unmountTestApp() {
src/backend/agent.js
+9 -7
@@ -185,13 +185,15 @@ export default class Agent extends EventEmitter<{|
185
186 getIDForNode(node: Object): number | null {
187 for (let rendererID in this._rendererInterfaces) {
188 - // A renderer will throw if it can't find a fiber for the specified node.
189 - try {
190 - const renderer = ((this._rendererInterfaces[
191 - (rendererID: any)
192 - ]: any): RendererInterface);
193 - return renderer.getFiberIDForNative(node, true);
194 - } catch (e) {}
188 + const renderer = ((this._rendererInterfaces[
189 + (rendererID: any)
190 + ]: any): RendererInterface);
191 +
192 + const id = renderer.getFiberIDForNative(node, true);
193 +
194 + if (id !== null) {
195 + return id;
196 + }
197 }
198 return null;
199 }
src/backend/views/Highlighter/index.js
+30
@@ -12,6 +12,8 @@ import type { BackendBridge } from 'src/bridge';
12 // It is not currently the mechanism used to highlight React Native views.
13 // That is done by the React Native Inspector component.
14
15 +let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
16 +
17 export default function setupHighlighter(
18 bridge: BackendBridge,
19 agent: Agent
@@ -26,6 +28,10 @@ export default function setupHighlighter(
28 bridge.addListener('stopInspectingNative', stopInspectingNative);
29
30 function startInspectingNative() {
31 + registerListenersOnWindow(window);
32 + }
33 +
34 + function registerListenersOnWindow(window) {
35 // This plug-in may run in non-DOM environments (e.g. React Native).
36 if (window && typeof window.addEventListener === 'function') {
37 window.addEventListener('click', onClick, true);
@@ -40,7 +46,18 @@ export default function setupHighlighter(
46
47 function stopInspectingNative() {
48 hideOverlay();
49 + removeListenersOnWindow(window);
50 + iframesListeningTo.forEach(function(frame) {
51 + try {
52 + removeListenersOnWindow(frame.contentWindow);
53 + } catch (error) {
54 + // This can error when the iframe is on a cross-origin.
55 + }
56 + });
57 + iframesListeningTo = new Set();
58 + }
59
60 + function removeListenersOnWindow(window) {
61 // This plug-in may run in non-DOM environments (e.g. React Native).
62 if (window && typeof window.removeEventListener === 'function') {
63 window.removeEventListener('click', onClick, true);
@@ -130,6 +147,19 @@ export default function setupHighlighter(
147
148 const target = ((event.target: any): HTMLElement);
149
150 + if (target.tagName === 'IFRAME') {
151 + const iframe: HTMLIFrameElement = (target: any);
152 + try {
153 + if (!iframesListeningTo.has(iframe)) {
154 + const window = iframe.contentWindow;
155 + registerListenersOnWindow(window);
156 + iframesListeningTo.add(iframe);
157 + }
158 + } catch (error) {
159 + // This can error when the iframe is on a cross-origin.
160 + }
161 + }
162 +
163 // Don't pass the name explicitly.
164 // It will be inferred from DOM tag and Fiber owner.
165 showOverlay([target], null, false);