-
-
-
- setPortalTarget(elem || undefined)}
- style={{
- position: 'absolute',
- top: 0,
- left: 0,
- padding: '0.5rem',
- border: '3px solid blue',
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
- }}
- >
-
- This div
is rendered in a custom container{' '}
- next to VisCanvas
.
-
-
-
-
- {portalTarget && (
-
- {createPortal(
-
- This paragraph appears in the same div
but is
- rendered with a separate Html
element and a
- portal.
-
,
- portalTarget,
- )}
-
- )}
-
+ <>
+ {containerMounted &&
}
+
+ {customContainer &&
+ createPortal(
+
+ This example demonstrates that, using a portal, children of{' '}
+ Html
can be rendered into a{' '}
+ custom container (itself potentially rendered
+ with Html
).
+
,
+ customContainer,
+ )}
+
-
elem && setContainer(elem)} />
-
+
+ toggleContainer()}>
+ {customContainer ? 'Unmount' : 'Mount'} container
+
+
+ >
);
},
argTypes: {
diff --git a/packages/lib/src/vis/shared/Html.tsx b/packages/lib/src/vis/shared/Html.tsx
index 7f1611621..cb924f5e6 100644
--- a/packages/lib/src/vis/shared/Html.tsx
+++ b/packages/lib/src/vis/shared/Html.tsx
@@ -1,48 +1,44 @@
+import { assertNonNull } from '@h5web/shared';
import { useThree } from '@react-three/fiber';
-import type { ReactNode } from 'react';
+import type { PropsWithChildren } from 'react';
import { useLayoutEffect, useState } from 'react';
-import ReactDOM from 'react-dom';
+import ReactDOM, { createPortal } from 'react-dom';
interface Props {
- overflowCanvas?: boolean; // allow children to overflow above axes
- container?: HTMLElement;
- children?: ReactNode;
+ overflowCanvas?: boolean;
}
-function Html(props: Props) {
- const {
- overflowCanvas = false,
- container: customContainer,
- children,
- } = props;
+function Html(props: PropsWithChildren
) {
+ const { overflowCanvas = false, children } = props;
const r3fRoot = useThree((state) => state.gl.domElement.parentElement);
- const canvasWrapper = r3fRoot?.parentElement;
+ assertNonNull(r3fRoot);
- // Choose DOM container to which to append `renderTarget`
- // (with `canvasWrapper`, `Html` children are allowed to overflow above the axes)
- const container =
- customContainer || (overflowCanvas ? canvasWrapper : r3fRoot);
+ const canvasWrapper = r3fRoot.parentElement;
+ assertNonNull(canvasWrapper);
- const [renderTarget] = useState(() => document.createElement('div'));
+ const portalContainer = overflowCanvas ? canvasWrapper : r3fRoot;
- useLayoutEffect(() => {
- ReactDOM.render(<>{children}>, renderTarget); // eslint-disable-line react/jsx-no-useless-fragment
- }, [children, renderTarget]);
+ const [renderContainer] = useState(() => {
+ const div = document.createElement('div');
+ div.setAttribute('hidden', '');
+ return div;
+ });
useLayoutEffect(() => {
- return () => {
- ReactDOM.unmountComponentAtNode(renderTarget);
- };
- }, [renderTarget]);
+ ReactDOM.render(createPortal(children, portalContainer), renderContainer);
+ }, [children, portalContainer, renderContainer]);
useLayoutEffect(() => {
- container?.append(renderTarget);
+ /* Since the children are rendered in a portal, it doesn't technically matter
+ which element `renderContainer` is appended to, as long as it stays in the DOM. */
+ r3fRoot.append(renderContainer);
return () => {
- renderTarget.remove();
+ ReactDOM.unmountComponentAtNode(renderContainer);
+ renderContainer.remove();
};
- }, [container, renderTarget]);
+ }, [r3fRoot, renderContainer]);
return null;
}