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><iframe></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);