@fluentui/react-portal 0.0.0-nightly-20220302-0405.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +2010 -0
- package/CHANGELOG.md +749 -0
- package/LICENSE +15 -0
- package/README.md +147 -0
- package/Spec.md +254 -0
- package/dist/react-portal.d.ts +62 -0
- package/lib/Portal.d.ts +1 -0
- package/lib/Portal.js +2 -0
- package/lib/Portal.js.map +1 -0
- package/lib/components/Portal/Portal.d.ts +7 -0
- package/lib/components/Portal/Portal.js +13 -0
- package/lib/components/Portal/Portal.js.map +1 -0
- package/lib/components/Portal/Portal.types.d.ts +22 -0
- package/lib/components/Portal/Portal.types.js +2 -0
- package/lib/components/Portal/Portal.types.js.map +1 -0
- package/lib/components/Portal/index.d.ts +4 -0
- package/lib/components/Portal/index.js +5 -0
- package/lib/components/Portal/index.js.map +1 -0
- package/lib/components/Portal/renderPortal.d.ts +6 -0
- package/lib/components/Portal/renderPortal.js +13 -0
- package/lib/components/Portal/renderPortal.js.map +1 -0
- package/lib/components/Portal/usePortal.d.ts +9 -0
- package/lib/components/Portal/usePortal.js +35 -0
- package/lib/components/Portal/usePortal.js.map +1 -0
- package/lib/components/Portal/usePortalMountNode.d.ts +10 -0
- package/lib/components/Portal/usePortalMountNode.js +36 -0
- package/lib/components/Portal/usePortalMountNode.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/tsdoc-metadata.json +11 -0
- package/lib/virtualParent/elementContains.d.ts +7 -0
- package/lib/virtualParent/elementContains.js +30 -0
- package/lib/virtualParent/elementContains.js.map +1 -0
- package/lib/virtualParent/getParent.d.ts +5 -0
- package/lib/virtualParent/getParent.js +10 -0
- package/lib/virtualParent/getParent.js.map +1 -0
- package/lib/virtualParent/getVirtualParent.d.ts +4 -0
- package/lib/virtualParent/getVirtualParent.js +15 -0
- package/lib/virtualParent/getVirtualParent.js.map +1 -0
- package/lib/virtualParent/index.d.ts +5 -0
- package/lib/virtualParent/index.js +6 -0
- package/lib/virtualParent/index.js.map +1 -0
- package/lib/virtualParent/isVirtualElement.d.ts +5 -0
- package/lib/virtualParent/isVirtualElement.js +7 -0
- package/lib/virtualParent/isVirtualElement.js.map +1 -0
- package/lib/virtualParent/setVirtualParent.d.ts +7 -0
- package/lib/virtualParent/setVirtualParent.js +20 -0
- package/lib/virtualParent/setVirtualParent.js.map +1 -0
- package/lib/virtualParent/types.d.ts +5 -0
- package/lib/virtualParent/types.js +2 -0
- package/lib/virtualParent/types.js.map +1 -0
- package/lib-commonjs/Portal.d.ts +1 -0
- package/lib-commonjs/Portal.js +10 -0
- package/lib-commonjs/Portal.js.map +1 -0
- package/lib-commonjs/components/Portal/Portal.d.ts +7 -0
- package/lib-commonjs/components/Portal/Portal.js +24 -0
- package/lib-commonjs/components/Portal/Portal.js.map +1 -0
- package/lib-commonjs/components/Portal/Portal.types.d.ts +22 -0
- package/lib-commonjs/components/Portal/Portal.types.js +6 -0
- package/lib-commonjs/components/Portal/Portal.types.js.map +1 -0
- package/lib-commonjs/components/Portal/index.d.ts +4 -0
- package/lib-commonjs/components/Portal/index.js +16 -0
- package/lib-commonjs/components/Portal/index.js.map +1 -0
- package/lib-commonjs/components/Portal/renderPortal.d.ts +6 -0
- package/lib-commonjs/components/Portal/renderPortal.js +24 -0
- package/lib-commonjs/components/Portal/renderPortal.js.map +1 -0
- package/lib-commonjs/components/Portal/usePortal.d.ts +9 -0
- package/lib-commonjs/components/Portal/usePortal.js +48 -0
- package/lib-commonjs/components/Portal/usePortal.js.map +1 -0
- package/lib-commonjs/components/Portal/usePortalMountNode.d.ts +10 -0
- package/lib-commonjs/components/Portal/usePortalMountNode.js +49 -0
- package/lib-commonjs/components/Portal/usePortalMountNode.js.map +1 -0
- package/lib-commonjs/index.d.ts +2 -0
- package/lib-commonjs/index.js +26 -0
- package/lib-commonjs/index.js.map +1 -0
- package/lib-commonjs/virtualParent/elementContains.d.ts +7 -0
- package/lib-commonjs/virtualParent/elementContains.js +40 -0
- package/lib-commonjs/virtualParent/elementContains.js.map +1 -0
- package/lib-commonjs/virtualParent/getParent.d.ts +5 -0
- package/lib-commonjs/virtualParent/getParent.js +20 -0
- package/lib-commonjs/virtualParent/getParent.js.map +1 -0
- package/lib-commonjs/virtualParent/getVirtualParent.d.ts +4 -0
- package/lib-commonjs/virtualParent/getVirtualParent.js +25 -0
- package/lib-commonjs/virtualParent/getVirtualParent.js.map +1 -0
- package/lib-commonjs/virtualParent/index.d.ts +5 -0
- package/lib-commonjs/virtualParent/index.js +18 -0
- package/lib-commonjs/virtualParent/index.js.map +1 -0
- package/lib-commonjs/virtualParent/isVirtualElement.d.ts +5 -0
- package/lib-commonjs/virtualParent/isVirtualElement.js +16 -0
- package/lib-commonjs/virtualParent/isVirtualElement.js.map +1 -0
- package/lib-commonjs/virtualParent/setVirtualParent.d.ts +7 -0
- package/lib-commonjs/virtualParent/setVirtualParent.js +29 -0
- package/lib-commonjs/virtualParent/setVirtualParent.js.map +1 -0
- package/lib-commonjs/virtualParent/types.d.ts +5 -0
- package/lib-commonjs/virtualParent/types.js +6 -0
- package/lib-commonjs/virtualParent/types.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
@fluentui/react-portal
|
2
|
+
|
3
|
+
Copyright (c) Microsoft Corporation
|
4
|
+
|
5
|
+
All rights reserved.
|
6
|
+
|
7
|
+
MIT License
|
8
|
+
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
14
|
+
|
15
|
+
Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license
|
package/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# @fluentui/react-portal
|
2
|
+
|
3
|
+
**React Portal components for [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)**
|
4
|
+
|
5
|
+
These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release.
|
6
|
+
|
7
|
+
This package contains the `Portal` component, which allow consumers to render [React portals](https://reactjs.org/docs/portals.html) with Fluent styling and RTL awareness.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
### Portal
|
12
|
+
|
13
|
+
`Portal` can be used as standalone with any part of a Fluent app. The component should be under a `FluentProvider` in the tree to make sure that proper theming and RTL handling is available.
|
14
|
+
|
15
|
+
By default `Portal` will render content to `document body`
|
16
|
+
|
17
|
+
```tsx
|
18
|
+
<FluentProvider>
|
19
|
+
<Portal>Content rendered by default to Fluent's document.body</Portal>
|
20
|
+
</FluentProvider>
|
21
|
+
```
|
22
|
+
|
23
|
+
The mount location of the portal can be customized
|
24
|
+
|
25
|
+
```tsx
|
26
|
+
const node = document.getElementById('customNode');
|
27
|
+
|
28
|
+
<Portal mountNode={node}>Render to a custom node in DOM</Portal>;
|
29
|
+
```
|
30
|
+
|
31
|
+
### Styling
|
32
|
+
|
33
|
+
`Portal` renders React children directly to the default/configured DOM node. Therefore styling should be applied to the `children` by users directly.
|
34
|
+
|
35
|
+
### Virtual parents
|
36
|
+
|
37
|
+
Out of order DOM elements can be problematic when using 'click outside' event listeners since you cannot rely on `element.contains(event.target)` because the `Portal` elements are out of DOM order.
|
38
|
+
|
39
|
+
```tsx
|
40
|
+
|
41
|
+
const outerButtonRef = React.useRef();
|
42
|
+
const innerButtonRef = React.useRef();
|
43
|
+
|
44
|
+
|
45
|
+
<Portal>
|
46
|
+
<div>
|
47
|
+
<button ref={outerButtonRef}> Outer button </button>
|
48
|
+
<Portal>
|
49
|
+
<div>
|
50
|
+
<button ref={innerButtonRef}> Inner button </button>
|
51
|
+
</div>
|
52
|
+
</Portal>
|
53
|
+
</div>
|
54
|
+
</Portal>
|
55
|
+
|
56
|
+
// DOM output
|
57
|
+
<div>
|
58
|
+
<button>Outer button</button>
|
59
|
+
</div>
|
60
|
+
|
61
|
+
<div>
|
62
|
+
<button>Inner button</button>
|
63
|
+
</div>
|
64
|
+
|
65
|
+
// Let's add an event listener to 'dismss' the outer portal when clicked outside
|
66
|
+
// ⚠⚠⚠ This will always be called when clicking on the inner button
|
67
|
+
document.addEventListener((event) => {
|
68
|
+
if (outerButtonRef.current.contains(event.target)) {
|
69
|
+
dismissOuterPortal();
|
70
|
+
}
|
71
|
+
})
|
72
|
+
```
|
73
|
+
|
74
|
+
When the above case is not required, using `element.contains` is perfectly fine. But nested cases should still be handled appropriately. We do this using the concept of `virtual parents`
|
75
|
+
|
76
|
+
`Portal` will make public 2 utilities that will only be used in cases where the user needs to know if an out of order DOM element will need to be used or not.
|
77
|
+
|
78
|
+
- `setVirtualParent` - sets virtual parent. Portal uses this already internally.
|
79
|
+
- `elementContains` - similar to `element.contains` but uses the virtual hierarchy as reference
|
80
|
+
|
81
|
+
Below shows what a virtual parent is
|
82
|
+
|
83
|
+
```tsx
|
84
|
+
// Setting a virtual parent
|
85
|
+
|
86
|
+
const parent document.getElementById('parent')
|
87
|
+
const child document.getElement.ById('child');
|
88
|
+
|
89
|
+
child._virtual.parent = parent;
|
90
|
+
```
|
91
|
+
|
92
|
+
`Portals` will render a hidden span that will be the virtual parent, by nesting portals virtual parens will also be nested so that `elementContains` will work predictably.
|
93
|
+
|
94
|
+
```tsx
|
95
|
+
<FluentProvider>
|
96
|
+
<Portal id="portal-1" />
|
97
|
+
<Portal id="portal-2" />
|
98
|
+
</FluentProvider>
|
99
|
+
```
|
100
|
+
|
101
|
+
DOM output:
|
102
|
+
|
103
|
+
```tsx
|
104
|
+
<body>
|
105
|
+
<div>
|
106
|
+
{/* Virtual parent for portal*/}
|
107
|
+
<span aria-hidden />
|
108
|
+
{/* Virtual parent for portal*/}
|
109
|
+
<span aria-hidden />
|
110
|
+
</div>
|
111
|
+
|
112
|
+
<div id="portal-1" class="theme-provider-0">
|
113
|
+
{children}
|
114
|
+
</div>
|
115
|
+
<div id="portal-2" class="theme-provider-0">
|
116
|
+
{children}
|
117
|
+
</div>
|
118
|
+
</body>
|
119
|
+
```
|
120
|
+
|
121
|
+
```tsx
|
122
|
+
<FluentProvider>
|
123
|
+
<Portal id="portal-1">
|
124
|
+
<Portal id="portal-2" />
|
125
|
+
</Portal>
|
126
|
+
</FluentProvider>
|
127
|
+
```
|
128
|
+
|
129
|
+
DOM output:
|
130
|
+
|
131
|
+
```tsx
|
132
|
+
<body>
|
133
|
+
<div>
|
134
|
+
{/* Virtual parent for outer portal*/}
|
135
|
+
<span aria-hidden></span>
|
136
|
+
</div>
|
137
|
+
|
138
|
+
<div id="portal-1" class="theme-provider-0">
|
139
|
+
{/* Virtual parent for inner portal*/}
|
140
|
+
<span aria-hidden />
|
141
|
+
{children}
|
142
|
+
</div>
|
143
|
+
<div id="portal-2" class="theme-provider-0">
|
144
|
+
{children}
|
145
|
+
</div>
|
146
|
+
</body>
|
147
|
+
```
|
package/Spec.md
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
# Portal Spec
|
2
|
+
|
3
|
+
## Background
|
4
|
+
|
5
|
+
Components that require positioning out of the normal DOM order such as Menu and Tooltip are generally rendered through React portals. This is very useful when the components need to break out of the bounds of a parent component so that the content is not overflowed or covered by another element with zIndex. Portals also support event bubbling in the React tree.
|
6
|
+
|
7
|
+
Since our styling system uses css variables that are written onto DOM, we need to ensure that all portals are rendered onto a part of the DOM where the css variables are available.
|
8
|
+
|
9
|
+
Portals also need need to include the same dir attribute as the parent react tree, so that RTL/LTR is displayed correctly in the portal and the parent content.
|
10
|
+
|
11
|
+
## Prior Art
|
12
|
+
|
13
|
+
Open UI research was not done for this component. `Portal` does not actually represent a UI control, but a utility component specific to React that makes the rendering of out of order DOM elements easy from within the React tree.
|
14
|
+
|
15
|
+
### v0/v8 Comparison
|
16
|
+
|
17
|
+
This section compares the noteworthy differences/similarities between the `Layer` and `Portal` components of v8 and v0 respectively that share functionality with this proposed converged component.
|
18
|
+
|
19
|
+
#### Portal Trigger
|
20
|
+
|
21
|
+
The v0 Portal component includes a `trigger` prop that allows consumers to open a React portal. The adoption is not widely used across v0's main consumer, Teams.
|
22
|
+
|
23
|
+
The v8 `Layer` component fills a similar role to `PortalInner` which is the component that actually renders a React portal.
|
24
|
+
|
25
|
+
#### Event propagation
|
26
|
+
|
27
|
+
v8 `Layer` users must explicitly use the `enableEventBubbling` behaviour, which is default in React portals. This is mainly because `Layer` is older than `React.Portal`. Since synthetic event propagation was only introduced with React portals, it had to be enabled to preserve backwards compat.
|
28
|
+
|
29
|
+
Meanwhile v0 does not allow any kind of way to disable native React event bubbling through portals, to achieve a similar effect, the user must manage this themselves.
|
30
|
+
|
31
|
+
#### DOM insertion
|
32
|
+
|
33
|
+
Both `Layer` and `Portal` allow insertion of portals to the same part of the DOM element.
|
34
|
+
|
35
|
+
v8 Layer does this through a `LayerHost` which can be rendered at any part of the React tree. The result is a HTMLDiv element with a specific CSS class and unique Id. `Layer` will attempt to find a `LayerHost` to mount either by CSS class or user provided `hostId`. The default fallback is `document.body`. Each `Layer` will render its own `Fabric` provider. The `insertFirst` property supported by `Layer` was introduced in #8065 for modeless Dialogs which was achieved through `position: static` and DOM insertion order. The same effect can be achieved through `pointer-events: none`.
|
36
|
+
|
37
|
+
The v0 `PortalBoxContext` stores a single HTMLDiv that is usually a child of `document.body` where all `Portal` components are rendered by default. The v0 `Provider` is rendered for the default portal element, where style and RTL overrides could be applied for all portals within.
|
38
|
+
|
39
|
+
#### Lifecycle methods
|
40
|
+
|
41
|
+
v0 `Portal` only supports the following lifecycle methods:
|
42
|
+
|
43
|
+
- onMount
|
44
|
+
- onUnmount
|
45
|
+
|
46
|
+
`Layer` supports the following:
|
47
|
+
|
48
|
+
- onLayerMounted
|
49
|
+
- onLayerWillUnmount
|
50
|
+
|
51
|
+
The lifecycle events are very similar, with the only difference being that `onLayerWillUnmount` is called with `hostId` changes. v0 does not consider the mountNode changing as an unmount of the component itself.
|
52
|
+
|
53
|
+
#### Focus Management
|
54
|
+
|
55
|
+
v0 `Portal` can be configured to focus trap its contents while v8 `Layer` does not offer this and users would need to use `FocusTrapZone` for this purpose.
|
56
|
+
|
57
|
+
## Sample Code
|
58
|
+
|
59
|
+
`Portal` by default mounts the content to `document.body`. In the event a consumer needs to target a specific mount node for Portal content this should be configurable via prop. Both variants should still be able to access theme and fluent context if available.
|
60
|
+
|
61
|
+
```
|
62
|
+
const customElement = document.createElement('div');
|
63
|
+
|
64
|
+
<App> // using FluentProvider of ThemeProvider but not PortalProvider
|
65
|
+
<Portal /> // attached to document.body
|
66
|
+
<Portal mountNode={customElement} /> // mounted on custom element
|
67
|
+
</App>
|
68
|
+
```
|
69
|
+
|
70
|
+
`Portal` should be used as a component at any part of the React tree:
|
71
|
+
|
72
|
+
```tsx
|
73
|
+
<ContextProvider>
|
74
|
+
<Portal>
|
75
|
+
<ContextConsumer /> // should be able to access context
|
76
|
+
</Portal>
|
77
|
+
</ContextProvider>
|
78
|
+
```
|
79
|
+
|
80
|
+
`Portal` should be able to access theme values as css variables:
|
81
|
+
|
82
|
+
```tsx
|
83
|
+
const useStyles = makeStyles({
|
84
|
+
portalContent: theme => {...}
|
85
|
+
})
|
86
|
+
|
87
|
+
|
88
|
+
const styles = useStyles();
|
89
|
+
|
90
|
+
<ThemeProvider>
|
91
|
+
<Portal>
|
92
|
+
<div className={styles.portalContent}>
|
93
|
+
Can use all theme CSS variables from the parent ThemeProvider
|
94
|
+
</div>
|
95
|
+
</Portal>
|
96
|
+
</ThemeProvider>
|
97
|
+
```
|
98
|
+
|
99
|
+
## Variants
|
100
|
+
|
101
|
+
- Mounting the portal on a custom node
|
102
|
+
|
103
|
+
## API
|
104
|
+
|
105
|
+
### Portal
|
106
|
+
|
107
|
+
| Name | Description | Required | Type | Default value |
|
108
|
+
| --------- | -------------------------------------- | -------- | ----------- | -------------------------------- |
|
109
|
+
| mountNode | Where the portal is mounted to the DOM | No | HTMLElement | ProviderContext or document.body |
|
110
|
+
|
111
|
+
### Virtual parents
|
112
|
+
|
113
|
+
Out of order DOM elements can be problematic when using 'click outside' event listeners since you cannot rely on `element.contains(event.target)` because the `Portal` elements are out of DOM order.
|
114
|
+
|
115
|
+
```tsx
|
116
|
+
|
117
|
+
const outerButtonRef = React.useRef();
|
118
|
+
const innerButtonRef = React.useRef();
|
119
|
+
|
120
|
+
|
121
|
+
<Portal>
|
122
|
+
<div>
|
123
|
+
<button ref={outerButtonRef}> Outer button </button>
|
124
|
+
<Portal>
|
125
|
+
<div>
|
126
|
+
<button ref={innerButtonRef}> Inner button </button>
|
127
|
+
</div>
|
128
|
+
</Portal>
|
129
|
+
</div>
|
130
|
+
</Portal>
|
131
|
+
|
132
|
+
// DOM output
|
133
|
+
<div>
|
134
|
+
<button>Outer button</button>
|
135
|
+
</div>
|
136
|
+
|
137
|
+
<div>
|
138
|
+
<button>Inner button</button>
|
139
|
+
</div>
|
140
|
+
|
141
|
+
// Let's add an event listener to 'dismss' the outer portal when clicked outside
|
142
|
+
// ⚠⚠⚠ This will always be called when clicking on the inner button
|
143
|
+
document.addEventListener((event) => {
|
144
|
+
if (outerButtonRef.current.contains(event.target)) {
|
145
|
+
dismissOuterPortal();
|
146
|
+
}
|
147
|
+
})
|
148
|
+
```
|
149
|
+
|
150
|
+
When the above case is not required, using `element.contains` is perfectly fine. But nested cases should still be handled appropriately. We do this using the concept of `virtual parents`
|
151
|
+
|
152
|
+
`Portal` will make public 2 utilities that will only be used in cases where the user needs to know if an out of order DOM element will need to be used or not.
|
153
|
+
|
154
|
+
- `setVirtualParent` - sets virtual parent
|
155
|
+
- `elementContains` - similar to `element.contains` but uses the virtual hierarchy as reference
|
156
|
+
|
157
|
+
Below shows what a virtual parent is
|
158
|
+
|
159
|
+
```tsx
|
160
|
+
// Setting a virtual parent
|
161
|
+
|
162
|
+
const parent document.getElementById('parent')
|
163
|
+
const child document.getElement.ById('child');
|
164
|
+
|
165
|
+
child._virtual.parent = parent;
|
166
|
+
```
|
167
|
+
|
168
|
+
## Structure
|
169
|
+
|
170
|
+
```tsx
|
171
|
+
<FluentProvider>
|
172
|
+
<Portal id="portal-1" />
|
173
|
+
<Portal id="portal-2" />
|
174
|
+
</FluentProvider>
|
175
|
+
```
|
176
|
+
|
177
|
+
DOM output:
|
178
|
+
|
179
|
+
```tsx
|
180
|
+
<body>
|
181
|
+
<div>
|
182
|
+
{/* Virtual parent for portal*/}
|
183
|
+
<span aria-hidden />
|
184
|
+
{/* Virtual parent for portal*/}
|
185
|
+
<span aria-hidden />
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<div id="portal-1" class="theme-provider-0">
|
189
|
+
{children}
|
190
|
+
</div>
|
191
|
+
<div id="portal-2" class="theme-provider-0">
|
192
|
+
{children}
|
193
|
+
</div>
|
194
|
+
</body>
|
195
|
+
```
|
196
|
+
|
197
|
+
```tsx
|
198
|
+
<FluentProvider>
|
199
|
+
<Portal id="portal-1">
|
200
|
+
<Portal id="portal-2" />
|
201
|
+
</Portal>
|
202
|
+
</FluentProvider>
|
203
|
+
```
|
204
|
+
|
205
|
+
DOM output:
|
206
|
+
|
207
|
+
```tsx
|
208
|
+
<body>
|
209
|
+
<div>
|
210
|
+
{/* Virtual parent for outer portal*/}
|
211
|
+
<span aria-hidden></span>
|
212
|
+
</div>
|
213
|
+
|
214
|
+
<div id="portal-1" class="theme-provider-0">
|
215
|
+
{/* Virtual parent for inner portal*/}
|
216
|
+
<span aria-hidden />
|
217
|
+
{children}
|
218
|
+
</div>
|
219
|
+
<div id="portal-2" class="theme-provider-0">
|
220
|
+
{children}
|
221
|
+
</div>
|
222
|
+
</body>
|
223
|
+
```
|
224
|
+
|
225
|
+
## Migration
|
226
|
+
|
227
|
+
_Describe what will need to be done to upgrade from the existing implementations:_
|
228
|
+
|
229
|
+
### v8 migration
|
230
|
+
|
231
|
+
- There will be no way to disable event bubbling, it will be up to consumers to call `stopPropagation` themselves or create extra utilities that do so
|
232
|
+
- No more concept of `LayerHost` and id/class selectors, raw HTML elements/refs can be stored in context on the consumer app and used in `mountNode` for `Portals` if required
|
233
|
+
- No more mount lifecycle methods, users can remedy this easily with `useEffect` or `useLayoutEffect` hooks
|
234
|
+
- `insertFirst` will no longer be supported, and can be handled by a custom `mountNode` if necessary, sticky Dialog can be implmented with `pointer-events: none`
|
235
|
+
|
236
|
+
### v0 migration
|
237
|
+
|
238
|
+
- No more openable portals - should use future converged `Popup`
|
239
|
+
- No more focus trapping in `Portals` do that manually (Tabster)
|
240
|
+
- No more mount lifecycle methods, users can remedy this easily with `useEffect` or `useLayoutEffect` hooks
|
241
|
+
|
242
|
+
## Behaviors
|
243
|
+
|
244
|
+
### Server Side Rendering (SSR)
|
245
|
+
|
246
|
+
The ReactDOM `createPortal` requires a valid DOM node to render. This is problematic when `document` does not actually exist during the server render. Instead during the server render `null` will be used. This is not a big problem for most components that use portals such as popups or dialogs since they must be opened from some kind of trigger element (i.e. button)
|
247
|
+
|
248
|
+
However, there are some cases where a `Portal` content will always need to be rendered on the page. Tooltips should always be rendered so that `aria` attributes will refer to actual elements. This is problematic since the Tooltip (or higher order component) needs to be aware of the server render where `null`is rendered and render the actual content on the first client render.
|
249
|
+
|
250
|
+
The `Portal` component should handle this SSR case, and should be aware of the server and client renders when calling `ReactDOM.createPortal`.
|
251
|
+
|
252
|
+
## Accessibility
|
253
|
+
|
254
|
+
This component is considered a utility to render its children to an out of order DOM element. Since the component itself does not render DOM it is up to the consumer to handle the A11y requirements of their portal content.
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import * as React_2 from 'react';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Similar functionality to `element.contains` DOM API for use with out of order DOM elements that
|
5
|
+
* checks the virtual parent hierarchy. If a virtual parents exists, it is chosen over the actual parent
|
6
|
+
*
|
7
|
+
* @returns true if the child can find the parent in its virtual hierarchy
|
8
|
+
*/
|
9
|
+
export declare function elementContains(parent: HTMLElement | null, child: HTMLElement | null): boolean;
|
10
|
+
|
11
|
+
/**
|
12
|
+
* A portal provides a way to render children into a DOM node
|
13
|
+
* that exists outside the DOM hierarchy of the parent component.
|
14
|
+
*/
|
15
|
+
export declare const Portal: React_2.FC<PortalProps>;
|
16
|
+
|
17
|
+
declare type PortalCommons = {
|
18
|
+
/**
|
19
|
+
* React children
|
20
|
+
*/
|
21
|
+
children: React_2.ReactNode;
|
22
|
+
/**
|
23
|
+
* Where the portal children are mounted on DOM
|
24
|
+
* @defaultValue a new element on document.body without any styling
|
25
|
+
*/
|
26
|
+
mountNode: HTMLDivElement | undefined;
|
27
|
+
};
|
28
|
+
|
29
|
+
export declare type PortalProps = Partial<PortalCommons>;
|
30
|
+
|
31
|
+
export declare type PortalState = PortalCommons & {
|
32
|
+
/** Indicates if a Portal should be rendered. */
|
33
|
+
shouldRender: boolean;
|
34
|
+
/**
|
35
|
+
* Ref to the root span element as virtual parent
|
36
|
+
*/
|
37
|
+
virtualParentRootRef: React_2.MutableRefObject<HTMLSpanElement | null>;
|
38
|
+
};
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Render the final JSX of Portal
|
42
|
+
*/
|
43
|
+
export declare const renderPortal_unstable: (state: PortalState) => React_2.ReactElement;
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Sets the virtual parent of an element.
|
47
|
+
*
|
48
|
+
* @param child - Theme element to set the virtual parent
|
49
|
+
* @param parent - The virtual parent, use `undefined` to remove a virtual parent relationship
|
50
|
+
*/
|
51
|
+
export declare function setVirtualParent(child: HTMLElement, parent?: HTMLElement): void;
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Create the state required to render Portal.
|
55
|
+
*
|
56
|
+
* The returned state can be modified with hooks such as usePortalStyles, before being passed to renderPortal_unstable.
|
57
|
+
*
|
58
|
+
* @param props - props from this instance of Portal
|
59
|
+
*/
|
60
|
+
export declare const usePortal_unstable: (props: PortalProps) => PortalState;
|
61
|
+
|
62
|
+
export { }
|
package/lib/Portal.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './components/Portal/index';
|
package/lib/Portal.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"Portal.js","sourceRoot":"../src/","sources":["Portal.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC","sourcesContent":["export * from './components/Portal/index';\n"]}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import * as React from 'react';
|
2
|
+
import type { PortalProps } from './Portal.types';
|
3
|
+
/**
|
4
|
+
* A portal provides a way to render children into a DOM node
|
5
|
+
* that exists outside the DOM hierarchy of the parent component.
|
6
|
+
*/
|
7
|
+
export declare const Portal: React.FC<PortalProps>;
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { usePortal_unstable } from './usePortal';
|
2
|
+
import { renderPortal_unstable } from './renderPortal';
|
3
|
+
/**
|
4
|
+
* A portal provides a way to render children into a DOM node
|
5
|
+
* that exists outside the DOM hierarchy of the parent component.
|
6
|
+
*/
|
7
|
+
|
8
|
+
export const Portal = props => {
|
9
|
+
const state = usePortal_unstable(props);
|
10
|
+
return renderPortal_unstable(state);
|
11
|
+
};
|
12
|
+
Portal.displayName = 'Portal';
|
13
|
+
//# sourceMappingURL=Portal.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["components/Portal/Portal.tsx"],"names":[],"mappings":"AAEA,SAAS,kBAAT,QAAmC,aAAnC;AACA,SAAS,qBAAT,QAAsC,gBAAtC;AAGA;;;AAGG;;AACH,OAAO,MAAM,MAAM,GAA0B,KAAK,IAAG;AACnD,QAAM,KAAK,GAAG,kBAAkB,CAAC,KAAD,CAAhC;AAEA,SAAO,qBAAqB,CAAC,KAAD,CAA5B;AACD,CAJM;AAMP,MAAM,CAAC,WAAP,GAAqB,QAArB","sourcesContent":["import * as React from 'react';\n\nimport { usePortal_unstable } from './usePortal';\nimport { renderPortal_unstable } from './renderPortal';\nimport type { PortalProps } from './Portal.types';\n\n/**\n * A portal provides a way to render children into a DOM node\n * that exists outside the DOM hierarchy of the parent component.\n */\nexport const Portal: React.FC<PortalProps> = props => {\n const state = usePortal_unstable(props);\n\n return renderPortal_unstable(state);\n};\n\nPortal.displayName = 'Portal';\n"],"sourceRoot":"../src/"}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import * as React from 'react';
|
2
|
+
declare type PortalCommons = {
|
3
|
+
/**
|
4
|
+
* React children
|
5
|
+
*/
|
6
|
+
children: React.ReactNode;
|
7
|
+
/**
|
8
|
+
* Where the portal children are mounted on DOM
|
9
|
+
* @defaultValue a new element on document.body without any styling
|
10
|
+
*/
|
11
|
+
mountNode: HTMLDivElement | undefined;
|
12
|
+
};
|
13
|
+
export declare type PortalProps = Partial<PortalCommons>;
|
14
|
+
export declare type PortalState = PortalCommons & {
|
15
|
+
/** Indicates if a Portal should be rendered. */
|
16
|
+
shouldRender: boolean;
|
17
|
+
/**
|
18
|
+
* Ref to the root span element as virtual parent
|
19
|
+
*/
|
20
|
+
virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>;
|
21
|
+
};
|
22
|
+
export {};
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"Portal.types.js","sourceRoot":"../src/","sources":["components/Portal/Portal.types.ts"],"names":[],"mappings":"","sourcesContent":["import * as React from 'react';\n\ntype PortalCommons = {\n /**\n * React children\n */\n children: React.ReactNode;\n /**\n * Where the portal children are mounted on DOM\n * @defaultValue a new element on document.body without any styling\n */\n mountNode: HTMLDivElement | undefined;\n};\n\nexport type PortalProps = Partial<PortalCommons>;\n\nexport type PortalState = PortalCommons & {\n /** Indicates if a Portal should be rendered. */\n shouldRender: boolean;\n\n /**\n * Ref to the root span element as virtual parent\n */\n virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>;\n};\n"]}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/Portal/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC","sourcesContent":["export * from './Portal';\nexport * from './Portal.types';\nexport * from './renderPortal';\nexport * from './usePortal';\n"]}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import * as ReactDOM from 'react-dom';
|
2
|
+
import * as React from 'react';
|
3
|
+
/**
|
4
|
+
* Render the final JSX of Portal
|
5
|
+
*/
|
6
|
+
|
7
|
+
export const renderPortal_unstable = state => {
|
8
|
+
return /*#__PURE__*/React.createElement("span", {
|
9
|
+
hidden: true,
|
10
|
+
ref: state.virtualParentRootRef
|
11
|
+
}, state.shouldRender && state.mountNode && /*#__PURE__*/ReactDOM.createPortal(state.children, state.mountNode));
|
12
|
+
};
|
13
|
+
//# sourceMappingURL=renderPortal.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["components/Portal/renderPortal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAZ,MAA0B,WAA1B;AACA,OAAO,KAAK,KAAZ,MAAuB,OAAvB;AAGA;;AAEG;;AACH,OAAO,MAAM,qBAAqB,GAAI,KAAD,IAA2C;AAC9E,sBACE,KAAA,CAAA,aAAA,CAAA,MAAA,EAAA;AAAM,IAAA,MAAM,EAAA,IAAZ;AAAa,IAAA,GAAG,EAAE,KAAK,CAAC;AAAxB,GAAA,EACG,KAAK,CAAC,YAAN,IAAsB,KAAK,CAAC,SAA5B,iBAAyC,QAAQ,CAAC,YAAT,CAAsB,KAAK,CAAC,QAA5B,EAAsC,KAAK,CAAC,SAA5C,CAD5C,CADF;AAKD,CANM","sourcesContent":["import * as ReactDOM from 'react-dom';\nimport * as React from 'react';\nimport type { PortalState } from './Portal.types';\n\n/**\n * Render the final JSX of Portal\n */\nexport const renderPortal_unstable = (state: PortalState): React.ReactElement => {\n return (\n <span hidden ref={state.virtualParentRootRef}>\n {state.shouldRender && state.mountNode && ReactDOM.createPortal(state.children, state.mountNode)}\n </span>\n );\n};\n"],"sourceRoot":"../src/"}
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import type { PortalProps, PortalState } from './Portal.types';
|
2
|
+
/**
|
3
|
+
* Create the state required to render Portal.
|
4
|
+
*
|
5
|
+
* The returned state can be modified with hooks such as usePortalStyles, before being passed to renderPortal_unstable.
|
6
|
+
*
|
7
|
+
* @param props - props from this instance of Portal
|
8
|
+
*/
|
9
|
+
export declare const usePortal_unstable: (props: PortalProps) => PortalState;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import * as React from 'react';
|
2
|
+
import { useIsSSR } from '@fluentui/react-utilities';
|
3
|
+
import { usePortalMountNode } from './usePortalMountNode';
|
4
|
+
import { setVirtualParent } from '../../virtualParent/index';
|
5
|
+
/**
|
6
|
+
* Create the state required to render Portal.
|
7
|
+
*
|
8
|
+
* The returned state can be modified with hooks such as usePortalStyles, before being passed to renderPortal_unstable.
|
9
|
+
*
|
10
|
+
* @param props - props from this instance of Portal
|
11
|
+
*/
|
12
|
+
|
13
|
+
export const usePortal_unstable = props => {
|
14
|
+
const {
|
15
|
+
children,
|
16
|
+
mountNode
|
17
|
+
} = props;
|
18
|
+
const virtualParentRootRef = React.useRef(null);
|
19
|
+
const fallbackMountNode = usePortalMountNode({
|
20
|
+
disabled: !!mountNode
|
21
|
+
});
|
22
|
+
const state = {
|
23
|
+
children,
|
24
|
+
mountNode: mountNode !== null && mountNode !== void 0 ? mountNode : fallbackMountNode,
|
25
|
+
shouldRender: !useIsSSR(),
|
26
|
+
virtualParentRootRef
|
27
|
+
};
|
28
|
+
React.useEffect(() => {
|
29
|
+
if (state.virtualParentRootRef.current && state.mountNode) {
|
30
|
+
setVirtualParent(state.mountNode, state.virtualParentRootRef.current);
|
31
|
+
}
|
32
|
+
}, [state.virtualParentRootRef, state.mountNode]);
|
33
|
+
return state;
|
34
|
+
};
|
35
|
+
//# sourceMappingURL=usePortal.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["components/Portal/usePortal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAZ,MAAuB,OAAvB;AACA,SAAS,QAAT,QAAyB,2BAAzB;AACA,SAAS,kBAAT,QAAmC,sBAAnC;AACA,SAAS,gBAAT,QAAiC,2BAAjC;AAGA;;;;;;AAMG;;AACH,OAAO,MAAM,kBAAkB,GAAI,KAAD,IAAoC;AACpE,QAAM;AAAE,IAAA,QAAF;AAAY,IAAA;AAAZ,MAA0B,KAAhC;AAEA,QAAM,oBAAoB,GAAG,KAAK,CAAC,MAAN,CAA8B,IAA9B,CAA7B;AACA,QAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAAE,IAAA,QAAQ,EAAE,CAAC,CAAC;AAAd,GAAD,CAA5C;AAEA,QAAM,KAAK,GAAgB;AACzB,IAAA,QADyB;AAEzB,IAAA,SAAS,EAAE,SAAS,KAAA,IAAT,IAAA,SAAS,KAAA,KAAA,CAAT,GAAA,SAAA,GAAa,iBAFC;AAGzB,IAAA,YAAY,EAAE,CAAC,QAAQ,EAHE;AAIzB,IAAA;AAJyB,GAA3B;AAOA,EAAA,KAAK,CAAC,SAAN,CAAgB,MAAK;AACnB,QAAI,KAAK,CAAC,oBAAN,CAA2B,OAA3B,IAAsC,KAAK,CAAC,SAAhD,EAA2D;AACzD,MAAA,gBAAgB,CAAC,KAAK,CAAC,SAAP,EAAkB,KAAK,CAAC,oBAAN,CAA2B,OAA7C,CAAhB;AACD;AACF,GAJD,EAIG,CAAC,KAAK,CAAC,oBAAP,EAA6B,KAAK,CAAC,SAAnC,CAJH;AAMA,SAAO,KAAP;AACD,CApBM","sourcesContent":["import * as React from 'react';\nimport { useIsSSR } from '@fluentui/react-utilities';\nimport { usePortalMountNode } from './usePortalMountNode';\nimport { setVirtualParent } from '../../virtualParent/index';\nimport type { PortalProps, PortalState } from './Portal.types';\n\n/**\n * Create the state required to render Portal.\n *\n * The returned state can be modified with hooks such as usePortalStyles, before being passed to renderPortal_unstable.\n *\n * @param props - props from this instance of Portal\n */\nexport const usePortal_unstable = (props: PortalProps): PortalState => {\n const { children, mountNode } = props;\n\n const virtualParentRootRef = React.useRef<HTMLSpanElement>(null);\n const fallbackMountNode = usePortalMountNode({ disabled: !!mountNode });\n\n const state: PortalState = {\n children,\n mountNode: mountNode ?? fallbackMountNode,\n shouldRender: !useIsSSR(),\n virtualParentRootRef,\n };\n\n React.useEffect(() => {\n if (state.virtualParentRootRef.current && state.mountNode) {\n setVirtualParent(state.mountNode, state.virtualParentRootRef.current);\n }\n }, [state.virtualParentRootRef, state.mountNode]);\n\n return state;\n};\n"],"sourceRoot":"../src/"}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
export declare type UsePortalMountNodeOptions = {
|
2
|
+
/**
|
3
|
+
* Since hooks cannot be called conditionally use this flag to disable creating the node
|
4
|
+
*/
|
5
|
+
disabled?: boolean;
|
6
|
+
};
|
7
|
+
/**
|
8
|
+
* Creates a new element on a document.body to mount portals
|
9
|
+
*/
|
10
|
+
export declare const usePortalMountNode: (options: UsePortalMountNodeOptions) => HTMLDivElement | undefined;
|