-
Notifications
You must be signed in to change notification settings - Fork 15
/
Portal.tsx
147 lines (121 loc) · 3.96 KB
/
Portal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import * as React from 'react';
import { createPortal } from 'react-dom';
import canUseDom from 'rc-util/lib/Dom/canUseDom';
import warning from 'rc-util/lib/warning';
import { supportRef, useComposeRef } from 'rc-util/lib/ref';
import OrderContext from './Context';
import useDom from './useDom';
import useScrollLocker from './useScrollLocker';
import { inlineMock } from './mock';
export type ContainerType = Element | DocumentFragment;
export type GetContainer =
| string
| ContainerType
| (() => ContainerType)
| false;
export interface PortalProps {
/** Customize container element. Default will create a div in document.body when `open` */
getContainer?: GetContainer;
children?: React.ReactNode;
/** Show the portal children */
open?: boolean;
/** Remove `children` when `open` is `false`. Set `false` will not handle remove process */
autoDestroy?: boolean;
/** Lock screen scroll when open */
autoLock?: boolean;
/** @private debug name. Do not use in prod */
debug?: string;
}
const getPortalContainer = (getContainer: GetContainer) => {
if (getContainer === false) {
return false;
}
if (!canUseDom() || !getContainer) {
return null;
}
if (typeof getContainer === 'string') {
return document.querySelector(getContainer);
}
if (typeof getContainer === 'function') {
return getContainer();
}
return getContainer;
};
const Portal = React.forwardRef<any, PortalProps>((props, ref) => {
const {
open,
autoLock,
getContainer,
debug,
autoDestroy = true,
children,
} = props;
const [shouldRender, setShouldRender] = React.useState(open);
const mergedRender = shouldRender || open;
// ========================= Warning =========================
if (process.env.NODE_ENV !== 'production') {
warning(
canUseDom() || !open,
`Portal only work in client side. Please call 'useEffect' to show Portal instead default render in SSR.`,
);
}
// ====================== Should Render ======================
React.useEffect(() => {
if (autoDestroy || open) {
setShouldRender(open);
}
}, [open, autoDestroy]);
// ======================== Container ========================
const [innerContainer, setInnerContainer] = React.useState<
ContainerType | false
>(() => getPortalContainer(getContainer));
React.useEffect(() => {
const customizeContainer = getPortalContainer(getContainer);
// Tell component that we check this in effect which is safe to be `null`
setInnerContainer(customizeContainer ?? null);
});
const [defaultContainer, queueCreate] = useDom(
mergedRender && !innerContainer,
debug,
);
const mergedContainer = innerContainer ?? defaultContainer;
// ========================= Locker ==========================
useScrollLocker(
autoLock &&
open &&
canUseDom() &&
(mergedContainer === defaultContainer ||
mergedContainer === document.body),
);
// =========================== Ref ===========================
let childRef: React.Ref<any> = null;
if (children && supportRef(children) && ref) {
({ ref: childRef } = children as any);
}
const mergedRef = useComposeRef(childRef, ref);
// ========================= Render ==========================
// Do not render when nothing need render
// When innerContainer is `undefined`, it may not ready since user use ref in the same render
if (!mergedRender || !canUseDom() || innerContainer === undefined) {
return null;
}
// Render inline
const renderInline = mergedContainer === false || inlineMock();
let reffedChildren = children;
if (ref) {
reffedChildren = React.cloneElement(children as any, {
ref: mergedRef,
});
}
return (
<OrderContext.Provider value={queueCreate}>
{renderInline
? reffedChildren
: createPortal(reffedChildren, mergedContainer)}
</OrderContext.Provider>
);
});
if (process.env.NODE_ENV !== 'production') {
Portal.displayName = 'Portal';
}
export default Portal;