Skip to content

Commit

Permalink
[Community Nux] Don't use blueprint dialog (#11660)
Browse files Browse the repository at this point in the history
### Summary & Motivation

Get rid of some super hacky logic to "follow" the blueprint modal as it
animates and just create the HTML manually myself.

For a recap of why this is necessary:
1. Iframes are reloaded from scratch if their dom node gets moved
2. Blueprint doesn't insert dialog children into the DOM unless the
modal is being shown
3. We want to show the modal only after the iframe is loaded (it needs
to be inserted into the DOM for that to happen)

So basically we want to preload the iframe that will be inside the
dialog but blueprint's API doesn't technically allow that.

So I made a custom dialog reusing the blueprint dialog class names so
that we get the same positioning, except I insert the iframe into the
DOM (rendering it offscreen) so that it can preload and then show the
modal after its done loading.

### How I Tested These Changes

https://www.loom.com/share/fd325fe2c0da41808cf415dea29d509a
  • Loading branch information
salazarm committed Jan 12, 2023
1 parent 0fbe38b commit 53399a5
Showing 1 changed file with 37 additions and 117 deletions.
154 changes: 37 additions & 117 deletions js_modules/dagit/packages/app/src/NUX/CommunityNux.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useStateWithStorage} from '@dagster-io/dagit-core/hooks/useStateWithStorage';
import {Dialog} from '@dagster-io/ui';
import React from 'react';
import ReactDOM from 'react-dom';
import {createGlobalStyle} from 'styled-components/macro';

export const CommunityNux = () => {
const [didDismissCommunityNux, dismissCommunityNux] = useStateWithStorage(
Expand Down Expand Up @@ -34,19 +34,11 @@ const CommunityNuxImpl: React.FC<{dismiss: () => void}> = ({dismiss}) => {
};
}, []);

const [iframeLoaded, setIframeLoaded] = React.useState(false);
const [width, setWidth] = React.useState(680);
const [height, setHeight] = React.useState(462);

const {preloadElement, loaded, renderInto, iframeRef} = useCommuniyNuxIframe({
height: `${height}px`,
width: `${width}px`,
});

React.useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) {
return () => {};
}
const messageListener = (event: MessageEvent) => {
if (event.data === 'dismiss') {
dismiss();
Expand All @@ -61,118 +53,46 @@ const CommunityNuxImpl: React.FC<{dismiss: () => void}> = ({dismiss}) => {
return () => {
window.removeEventListener('message', messageListener, false);
};
}, [dismiss, iframeRef]);
}, [dismiss]);

const [target, setTarget] = React.useState<HTMLDivElement | null>(null);
React.useEffect(() => {
if (shouldShowNux && loaded && target) {
renderInto(target);
}
}, [shouldShowNux, loaded, renderInto, target]);
const isOpen = shouldShowNux && iframeLoaded;

return (
<>
<Dialog
isOpen={shouldShowNux && loaded}
style={{
width: `${width}px`,
background: 'transparent',
overflow: 'hidden',
height: `${height}px`,
}}
return ReactDOM.createPortal(
<div className="bp3-portal dagit-portal">
<GlobalOffscreenStyle />
<div
className={`bp3-overlay ${isOpen ? 'bp3-overlay-open' : ''} bp3-overlay-scroll-container`}
>
<div
ref={(element: HTMLDivElement) => {
setTarget(element);
}}
style={{overflow: 'hidden'}}
/>
</Dialog>
{/** We create a portal after the dialog so that the dialog can be positioned over the blueprint overlay */}
{ReactDOM.createPortal(preloadElement, document.body)}
</>
{isOpen ? <div className="bp3-overlay-backdrop dagit-backdrop" /> : null}
<div className="bp3-dialog-container bp3-overlay-content">
<div
className={`bp3-dialog dagit-dialog ${isOpen ? '' : 'offscreen'}`}
style={{width: width + 'px', height: height + 'px'}}
>
<iframe
src={IFRAME_SRC}
width={width}
height={height}
style={{
border: 'none',
}}
onLoad={() => {
setIframeLoaded(true);
}}
/>
</div>
</div>
</div>
</div>,
document.body,
);
};

const IFRAME_SRC = 'https://dagster.io/dagit_iframes/community_nux';

type Props = {
width: string;
height: string;
};

/**
* This iframe uses a bit of a hack to allow us to show the dialog only when the iframe is fully loaded.
* To do this we render the iframe offscreen then move it on screen. The problem we run into is two fold:
* 1) The container we render the iframe into will not be on screen until the iframe is ready, so the iframe can't be initially
* put into its final location
* 2) If we move an iframe's DOM node then the iframe gets reloaded from scratch defeating the purpose of preloading it
*
* So instead we position the iframe absolutely and keep track of the position of the target element where we want the iframe to live.
*
*/
const useCommuniyNuxIframe = ({width, height}: Props) => {
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [loaded, setLoaded] = React.useState(false);
const [parentRect, setParentRect] = React.useState<DOMRect | null>(null);
const [parent, setParent] = React.useState<HTMLElement | null>(null);

React.useLayoutEffect(() => {
if (parent?.parentNode) {
const dialogFrame = parent.parentNode as HTMLDivElement;

const RO = window['ResizeObserver'] as any;
const observer = new RO(() => {
setParentRect(dialogFrame.getBoundingClientRect());
});
observer.observe(parent.parentNode);
observer.observe(document.documentElement);

const lastRect = dialogFrame.getBoundingClientRect();

// Blueprint animates the dialog, so we need to follow it for the animation
// It also doens't update every frame, so we give an allowance of up to 100 frames
// without any updates. After 100 frames of no updates we assume the animation is complete
// and we stop our measuring loop
const loopUntilAnimationFinishes = (max: number) => {
const nextRect = dialogFrame.getBoundingClientRect();
if (lastRect.left !== nextRect.left || lastRect.top !== nextRect.top || max > 0) {
setParentRect(nextRect);
requestAnimationFrame(() => loopUntilAnimationFinishes(Math.max(max - 1, 0)));
}
};
requestAnimationFrame(() => loopUntilAnimationFinishes(100));
}
}, [parent]);

return {
preloadElement: (
<iframe
style={
parentRect
? {
width,
height,
position: 'absolute',
left: parentRect.left,
top: parentRect.top,
zIndex: 21,
overflow: 'hidden',
border: 'none',
}
: {width, height, left: '-999999px', position: 'absolute', zIndex: 0}
}
src={IFRAME_SRC}
ref={iframeRef}
onLoad={() => {
setLoaded(true);
}}
/>
),
loaded,
iframeRef,
renderInto: React.useCallback((parent: HTMLElement) => {
setParent(parent);
}, []),
};
};
const GlobalOffscreenStyle = createGlobalStyle`
.offscreen {
position: absolute;
left: -99999px;
}
`;

0 comments on commit 53399a5

Please sign in to comment.