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.
This commit is contained in:
Philipp Spiess 2019-07-28 17:40:02 +02:00 committed by Brian Vaughn
parent 50512a223b
commit 82881ab261
4 changed files with 108 additions and 7 deletions

View File

@ -0,0 +1,67 @@
import React from 'react';
import ReactDOM from 'react-dom';
export default function Iframe() {
return (
<>
<h2>Iframe</h2>
<div>
<Frame>
<Greeting />
</Frame>
</div>
</>
);
}
const iframeStyle = { border: '2px solid #eee', height: 80 };
function Frame(props) {
const [element, setElement] = React.useState(null);
const ref = React.useRef();
React.useLayoutEffect(function() {
const iframe = ref.current;
if (iframe) {
const html = `
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
</html>
`;
const document = iframe.contentDocument;
document.open();
document.write(html);
document.close();
setElement(document.getElementById('root'));
}
}, []);
return (
<>
<iframe title="Test Iframe" ref={ref} style={iframeStyle} />
<iframe
title="Secured Iframe"
src="https://example.com"
style={iframeStyle}
/>
{element ? ReactDOM.createPortal(props.children, element) : null}
</>
);
}
function Greeting() {
return (
<p>
Hello from within an <code>&lt;iframe&gt;</code>!
</p>
);
}

View File

@ -8,6 +8,7 @@ import {
unstable_createRoot as createRoot,
} from 'react-dom';
import DeeplyNestedComponents from './DeeplyNestedComponents';
import Iframe from './Iframe';
import EditableProps from './EditableProps';
import ElementTypes from './ElementTypes';
import Hydration from './Hydration';
@ -46,6 +47,7 @@ function mountTestApp() {
mountHelper(Toggle);
mountHelper(SuspenseTree);
mountHelper(DeeplyNestedComponents);
mountHelper(Iframe);
}
function unmountTestApp() {

View File

@ -185,13 +185,15 @@ export default class Agent extends EventEmitter<{|
getIDForNode(node: Object): number | null {
for (let rendererID in this._rendererInterfaces) {
// A renderer will throw if it can't find a fiber for the specified node.
try {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
return renderer.getFiberIDForNative(node, true);
} catch (e) {}
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const id = renderer.getFiberIDForNative(node, true);
if (id !== null) {
return id;
}
}
return null;
}

View File

@ -12,6 +12,8 @@ import type { BackendBridge } from 'src/bridge';
// It is not currently the mechanism used to highlight React Native views.
// That is done by the React Native Inspector component.
let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
export default function setupHighlighter(
bridge: BackendBridge,
agent: Agent
@ -26,6 +28,10 @@ export default function setupHighlighter(
bridge.addListener('stopInspectingNative', stopInspectingNative);
function startInspectingNative() {
registerListenersOnWindow(window);
}
function registerListenersOnWindow(window) {
// This plug-in may run in non-DOM environments (e.g. React Native).
if (window && typeof window.addEventListener === 'function') {
window.addEventListener('click', onClick, true);
@ -40,7 +46,18 @@ export default function setupHighlighter(
function stopInspectingNative() {
hideOverlay();
removeListenersOnWindow(window);
iframesListeningTo.forEach(function(frame) {
try {
removeListenersOnWindow(frame.contentWindow);
} catch (error) {
// This can error when the iframe is on a cross-origin.
}
});
iframesListeningTo = new Set();
}
function removeListenersOnWindow(window) {
// This plug-in may run in non-DOM environments (e.g. React Native).
if (window && typeof window.removeEventListener === 'function') {
window.removeEventListener('click', onClick, true);
@ -130,6 +147,19 @@ export default function setupHighlighter(
const target = ((event.target: any): HTMLElement);
if (target.tagName === 'IFRAME') {
const iframe: HTMLIFrameElement = (target: any);
try {
if (!iframesListeningTo.has(iframe)) {
const window = iframe.contentWindow;
registerListenersOnWindow(window);
iframesListeningTo.add(iframe);
}
} catch (error) {
// This can error when the iframe is on a cross-origin.
}
}
// Don't pass the name explicitly.
// It will be inferred from DOM tag and Fiber owner.
showOverlay([target], null, false);