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:
parent
50512a223b
commit
82881ab261
|
@ -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><iframe></code>!
|
||||
</p>
|
||||
);
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue