mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
feat: add portal component
This commit is contained in:
committed by
Thomas Osmonson
parent
b83bcbff13
commit
e8bc22bb55
131
packages/ui/src/portal/index.tsx
Normal file
131
packages/ui/src/portal/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { createContext, isBrowser, __DEV__ } from '../utils';
|
||||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { usePortalManager } from './manager';
|
||||
import { useSafeLayoutEffect } from '../hooks';
|
||||
|
||||
type PortalContext = HTMLDivElement | null;
|
||||
|
||||
const [PortalCtxProvider, usePortalContext] = createContext<PortalContext>({
|
||||
strict: false,
|
||||
});
|
||||
|
||||
export interface PortalProps {
|
||||
/**
|
||||
* Function called when the portal mounts
|
||||
*/
|
||||
onMount?(): void;
|
||||
/**
|
||||
* Function called when the portal unmounts
|
||||
*/
|
||||
onUnmount?(): void;
|
||||
/**
|
||||
* Function that will be called to get the parent element
|
||||
* that the portal will be attached to.
|
||||
*/
|
||||
container?: () => HTMLElement;
|
||||
/**
|
||||
* The content or node you'll like to portal
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal
|
||||
*
|
||||
* Declarative component used to render children into a DOM node
|
||||
* that exists outside the DOM hierarchy of the parent component.
|
||||
*
|
||||
*/
|
||||
export function Portal(props: PortalProps) {
|
||||
const { onMount, onUnmount, children, container: containerProp } = props;
|
||||
|
||||
/**
|
||||
* Generate the portal's dom node. We'll wrap the children
|
||||
* in this dom node before mounting it.
|
||||
*/
|
||||
const [portal] = React.useState(() => {
|
||||
if (isBrowser) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'portal';
|
||||
return div;
|
||||
}
|
||||
// for ssr
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* This portal might be nested in another portal.
|
||||
* Let's read from the portal context to check this.
|
||||
*/
|
||||
const parentPortal = usePortalContext();
|
||||
|
||||
/**
|
||||
* If there's a PortalManager rendered, let's read from it.
|
||||
* We use the portal manager to manage multiple portals
|
||||
*/
|
||||
const manager = usePortalManager();
|
||||
|
||||
const append = React.useCallback(
|
||||
(container: HTMLElement | null) => {
|
||||
// if user specified a mount node, do nothing.
|
||||
if (!portal || !container) return;
|
||||
|
||||
// else, simply append component to the portal node
|
||||
container.appendChild(portal);
|
||||
},
|
||||
[portal]
|
||||
);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
// get the custom container from the container prop
|
||||
const mountNode = containerProp?.();
|
||||
|
||||
/**
|
||||
* We need to know where to mount this portal, we have 4 options:
|
||||
* - If a mountRef is specified, we'll use that as the container
|
||||
* - If portal is nested, use the parent portal node as container.
|
||||
* - If it's not nested, use the manager's node as container
|
||||
* - else use document.body as containers
|
||||
*/
|
||||
const container = mountNode ?? parentPortal ?? manager?.node ?? document.body;
|
||||
|
||||
/**
|
||||
* Append portal node to the computed container
|
||||
*/
|
||||
append(container);
|
||||
|
||||
onMount?.();
|
||||
|
||||
return () => {
|
||||
onUnmount?.();
|
||||
|
||||
if (!portal) return;
|
||||
|
||||
if (container?.contains(portal)) {
|
||||
container?.removeChild(portal);
|
||||
}
|
||||
};
|
||||
}, [containerProp, portal, parentPortal, onMount, onUnmount, manager && manager.node, append]);
|
||||
|
||||
const finalChildren = manager?.zIndex ? (
|
||||
<div className="portal-zIndex" style={{ zIndex: manager.zIndex }}>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
if (!portal) {
|
||||
return <React.Fragment>{finalChildren}</React.Fragment>;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<PortalCtxProvider value={portal}>{finalChildren}</PortalCtxProvider>,
|
||||
portal
|
||||
);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
Portal.displayName = 'Portal';
|
||||
}
|
||||
75
packages/ui/src/portal/manager.tsx
Normal file
75
packages/ui/src/portal/manager.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
import { createContext, __DEV__ } from '../utils';
|
||||
import { useSafeLayoutEffect, useForceUpdate } from '../hooks';
|
||||
interface PortalManagerContext {
|
||||
node: HTMLElement;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const [PortalManagerCtxProvider, usePortalManager] = createContext<PortalManagerContext>({
|
||||
strict: false,
|
||||
});
|
||||
|
||||
export { usePortalManager };
|
||||
|
||||
export interface PortalManagerProps {
|
||||
/**
|
||||
* Child elements of the Portal manager
|
||||
* Ideally, it should be at the top-level
|
||||
* of your application
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* [Z-Index war] If your has multiple elements
|
||||
* with z-index clashing, you might need to
|
||||
* apply a z-index to the Portal manager
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PortalManager
|
||||
*
|
||||
* Used to manage multiple portals within an application.
|
||||
* It must be render only once, at the root of your application.
|
||||
*
|
||||
* Inspired by BaseWeb's LayerManager component
|
||||
*/
|
||||
export function PortalManager(props: PortalManagerProps) {
|
||||
const { children, zIndex } = props;
|
||||
|
||||
/**
|
||||
* The element that wraps the stacked layers
|
||||
*/
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
/**
|
||||
* force an update on mount so the Provider works correctly
|
||||
*/
|
||||
useSafeLayoutEffect(() => {
|
||||
forceUpdate();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* let's detect if use has mutiple instances of this component
|
||||
*/
|
||||
const parentManager = usePortalManager();
|
||||
|
||||
const context = {
|
||||
node: parentManager?.node || ref.current,
|
||||
zIndex,
|
||||
};
|
||||
|
||||
return (
|
||||
<PortalManagerCtxProvider value={context}>
|
||||
{children}
|
||||
<div className="portal-manager" ref={ref} />
|
||||
</PortalManagerCtxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
PortalManager.displayName = 'PortalManager';
|
||||
}
|
||||
Reference in New Issue
Block a user