TryGhost/Ghost

View on GitHub
apps/comments-ui/src/components/IFrame.tsx

Summary

Maintainability
C
1 day
Test Coverage
import {Component, forwardRef} from 'react';
import {createPortal} from 'react-dom';

/**
 * This is still a class component because it causes issues with the behaviour (DOM recreation and layout glitches) if we switch to a functional component. Feel free to refactor.
 */
class IFrame extends Component<any> {
    node: any;
    iframeHtml: any;
    iframeHead: any;
    iframeRoot: any;

    constructor(props: {onResize?: (el: HTMLElement) => void, children: any}) {
        super(props);
        this.setNode = this.setNode.bind(this);
        this.node = null;
    }

    componentDidMount() {
        this.node.addEventListener('load', this.handleLoad);
    }

    handleLoad = () => {
        this.setupFrameBaseStyle();
    };

    componentWillUnmount() {
        this.node.removeEventListener('load', this.handleLoad);
    }

    setupFrameBaseStyle() {
        if (this.node.contentDocument) {
            this.iframeHtml = this.node.contentDocument.documentElement;
            this.iframeHead = this.node.contentDocument.head;
            this.iframeRoot = this.node.contentDocument.body;
            this.forceUpdate();

            if (this.props.onResize) {
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                (new ResizeObserver((_) => {
                    window.requestAnimationFrame(() => {
                        this.props.onResize(this.iframeRoot);
                    });
                }))?.observe?.(this.iframeRoot);
            }

            // This is a bit hacky, but prevents us to need to attach even listeners to all the iframes we have
            // because when we want to listen for keydown events, those are only send in the window of iframe that is focused
            // To get around this, we pass down the keydown events to the main window
            // No need to detach, because the iframe would get removed
            this.node.contentWindow.addEventListener('keydown', (e: KeyboardEvent | undefined) => {
                // dispatch a new event
                window.dispatchEvent(
                    new KeyboardEvent('keydown', e)
                );
            });
        }
    }

    setNode(node: any) {
        this.node = node;
        if (this.props.innerRef) {
            this.props.innerRef.current = node;
        }
    }

    render() {
        const {children, head, title = '', style = {}, ...rest} = this.props;
        return (
            <iframe srcDoc={`<!DOCTYPE html>`} {...rest} ref={this.setNode} frameBorder="0" style={style} title={title}>
                {this.iframeHead && createPortal(head, this.iframeHead)}
                {this.iframeRoot && createPortal(children, this.iframeRoot)}
            </iframe>
        );
    }
}

const IFrameFC = forwardRef<HTMLIFrameElement, any>(function IFrameFC(props, ref) {
    return <IFrame {...props} innerRef={ref} />;
});

export default IFrameFC;