Line data Source code
1 : /// This file is copied almost as-is from the Portal of @primer/react
2 :
3 : import * as React from 'react'
4 : import { createPortal } from 'react-dom'
5 :
6 1 : const DASHQL_PORTAL_ROOT_ID = '__dashqlPortalRoot__'
7 1 : const DEFAULT_PORTAL_CONTAINER_NAME = '__default__'
8 :
9 1 : const portalRootRegistry: Partial<Record<string, Element>> = {}
10 :
11 : /// Register a container to serve as a portal root.
12 : /// If name is not specified, registers the default portal root.
13 1 : export function registerPortalRoot(root: Element, name = DEFAULT_PORTAL_CONTAINER_NAME): void {
14 1 : portalRootRegistry[name] = root
15 1 : }
16 :
17 : /// Ensures that a default portal root exists and is registered. If a DOM element exists
18 : /// with id __dashqlPortalRoot__, allow that element to serve as the default portal root.
19 : /// Otherwise, create that element and attach it to the end of document.body.
20 1 : function ensureDefaultPortal() {
21 1 : const existingDefaultContainer = portalRootRegistry[DEFAULT_PORTAL_CONTAINER_NAME]
22 1 : if (!existingDefaultContainer || !document.body.contains(existingDefaultContainer)) {
23 1 : let defaultContainer = document.getElementById(DASHQL_PORTAL_ROOT_ID)
24 1 : if (!(defaultContainer instanceof Element)) {
25 1 : defaultContainer = document.createElement('div')
26 1 : defaultContainer.setAttribute('id', DASHQL_PORTAL_ROOT_ID)
27 1 : defaultContainer.style.position = 'absolute'
28 1 : defaultContainer.style.top = '0'
29 1 : defaultContainer.style.left = '0'
30 1 : const suitablePortalRoot = document.querySelector('[data-portal-root]')
31 1 : if (suitablePortalRoot) {
32 0 : suitablePortalRoot.appendChild(defaultContainer)
33 : } else {
34 1 : document.body.appendChild(defaultContainer)
35 : }
36 1 : }
37 1 : registerPortalRoot(defaultContainer)
38 1 : }
39 1 : }
40 :
41 : export interface PortalProps {
42 : /// Called when this portal is added to the DOM
43 : onMount?: () => void
44 : /// Optional. Mount this portal at the container specified
45 : /// by this name. The container must be previously registered
46 : /// with `registerPortal`.
47 : containerName?: string
48 : /// The children elements
49 : children?: React.ReactElement;
50 : }
51 :
52 : /// Creates a React Portal, placing all children in a separate physical DOM root node.
53 1 : export function Portal(props: PortalProps) {
54 1 : const elementRef = React.useRef<HTMLDivElement | null>(null)
55 1 : if (!elementRef.current) {
56 1 : const div = document.createElement('div')
57 1 : div.style.position = 'relative';
58 1 : div.style.zIndex = '1000';
59 1 : elementRef.current = div
60 1 : }
61 1 : const element = elementRef.current;
62 :
63 1 : React.useLayoutEffect(() => {
64 1 : let containerName = props.containerName
65 1 : if (containerName === undefined) {
66 1 : containerName = DEFAULT_PORTAL_CONTAINER_NAME
67 1 : ensureDefaultPortal();
68 1 : }
69 1 : const parentElement = portalRootRegistry[containerName];
70 1 : if (!parentElement) {
71 0 : throw new Error(
72 0 : `Portal container '${props.containerName}' is not yet registered. Container must be registered with registerPortal before use.`,
73 0 : )
74 : }
75 1 : parentElement.appendChild(element);
76 1 : props.onMount?.();
77 :
78 1 : return () => {
79 1 : parentElement.removeChild(element)
80 1 : }
81 1 : }, [element]);
82 :
83 1 : return createPortal(props.children, element);
84 1 : }
|