@fluentui/react-portal
Advanced tools
Comparing version 9.0.0-alpha.12 to 9.0.0-alpha.13
@@ -5,3 +5,26 @@ { | ||
{ | ||
"date": "Tue, 18 May 2021 07:31:38 GMT", | ||
"date": "Wed, 19 May 2021 07:30:20 GMT", | ||
"tag": "@fluentui/react-portal_v9.0.0-alpha.13", | ||
"version": "9.0.0-alpha.13", | ||
"comments": { | ||
"prerelease": [ | ||
{ | ||
"comment": "Render portals with virtual parent", | ||
"author": "lingfan.gao@microsoft.com", | ||
"commit": "90cd8ca21a6904680a1a06ba4800053dc69a9a48", | ||
"package": "@fluentui/react-portal" | ||
} | ||
], | ||
"patch": [ | ||
{ | ||
"comment": "Bump @fluentui/react-conformance to v0.3.1", | ||
"author": "martinhochel@microsoft.com", | ||
"commit": "616b4b7c381c757871e8a590564d8eff7337834c", | ||
"package": "@fluentui/react-portal" | ||
} | ||
] | ||
} | ||
}, | ||
{ | ||
"date": "Tue, 18 May 2021 07:34:38 GMT", | ||
"tag": "@fluentui/react-portal_v9.0.0-alpha.12", | ||
@@ -8,0 +31,0 @@ "version": "9.0.0-alpha.12", |
# Change Log - @fluentui/react-portal | ||
This log was last generated on Tue, 18 May 2021 07:31:38 GMT and should not be manually modified. | ||
This log was last generated on Wed, 19 May 2021 07:30:20 GMT and should not be manually modified. | ||
<!-- Start content --> | ||
## [9.0.0-alpha.13](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.0.0-alpha.13) | ||
Wed, 19 May 2021 07:30:20 GMT | ||
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.0.0-alpha.12..@fluentui/react-portal_v9.0.0-alpha.13) | ||
### Patches | ||
- Bump @fluentui/react-conformance to v0.3.1 ([PR #18194](https://github.com/microsoft/fluentui/pull/18194) by martinhochel@microsoft.com) | ||
### Changes | ||
- Render portals with virtual parent ([PR #18148](https://github.com/microsoft/fluentui/pull/18148) by lingfan.gao@microsoft.com) | ||
## [9.0.0-alpha.12](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.0.0-alpha.12) | ||
Tue, 18 May 2021 07:31:38 GMT | ||
Tue, 18 May 2021 07:34:38 GMT | ||
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.0.0-alpha.11..@fluentui/react-portal_v9.0.0-alpha.12) | ||
@@ -11,0 +24,0 @@ |
import * as React_2 from 'react'; | ||
/** | ||
* Similar functionality to `element.contains` DOM API for use with out of order DOM elements that | ||
* checks the virtual parent hierarchy. If a virtual parents exists, it is chosen over the actual parent | ||
* | ||
* @returns true if the child can find the parent in its virtual hierarchy | ||
*/ | ||
export declare function elementContains(parent: HTMLElement | null, child: HTMLElement | null): boolean; | ||
/** | ||
* Component that renders children in a React portal | ||
@@ -23,2 +31,6 @@ */ | ||
shouldRender: boolean; | ||
/** | ||
* Ref to the root span element as virtual parent | ||
*/ | ||
virtualParentRootRef: React_2.MutableRefObject<HTMLSpanElement | null>; | ||
} | ||
@@ -29,5 +41,13 @@ | ||
*/ | ||
export declare const renderPortal: (state: PortalState) => React_2.ReactPortal | null; | ||
export declare const renderPortal: (state: PortalState) => React_2.ReactElement; | ||
/** | ||
* Sets the virtual parent of an element. | ||
* | ||
* @param child - Theme element to set the virtual parent | ||
* @param parent - The virtual parent, use `undefined` to remove a virtual parent relationship | ||
*/ | ||
export declare function setVirtualParent(child: HTMLElement, parent?: HTMLElement): void; | ||
/** | ||
* Create the state required to render Portal. | ||
@@ -34,0 +54,0 @@ * |
@@ -10,2 +10,5 @@ ## API Report File for "@fluentui/react-portal" | ||
// @public | ||
export function elementContains(parent: HTMLElement | null, child: HTMLElement | null): boolean; | ||
// @public | ||
export const Portal: React_2.FC<PortalProps>; | ||
@@ -22,8 +25,12 @@ | ||
shouldRender: boolean; | ||
virtualParentRootRef: React_2.MutableRefObject<HTMLSpanElement | null>; | ||
} | ||
// @public | ||
export const renderPortal: (state: PortalState) => React_2.ReactPortal | null; | ||
export const renderPortal: (state: PortalState) => React_2.ReactElement; | ||
// @public | ||
export function setVirtualParent(child: HTMLElement, parent?: HTMLElement): void; | ||
// @public | ||
export const usePortal: (props: PortalProps, defaultProps?: PortalProps | undefined) => PortalState; | ||
@@ -30,0 +37,0 @@ |
@@ -16,2 +16,6 @@ import * as React from 'react'; | ||
shouldRender: boolean; | ||
/** | ||
* Ref to the root span element as virtual parent | ||
*/ | ||
virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>; | ||
} |
@@ -6,2 +6,2 @@ import * as React from 'react'; | ||
*/ | ||
export declare const renderPortal: (state: PortalState) => React.ReactPortal | null; | ||
export declare const renderPortal: (state: PortalState) => React.ReactElement; |
@@ -1,2 +0,2 @@ | ||
define(["require", "exports", "react-dom"], function (require, exports, ReactDOM) { | ||
define(["require", "exports", "react-dom", "react"], function (require, exports, ReactDOM, React) { | ||
"use strict"; | ||
@@ -9,6 +9,3 @@ Object.defineProperty(exports, "__esModule", { value: true }); | ||
var renderPortal = function (state) { | ||
if (state.shouldRender && state.mountNode) { | ||
return ReactDOM.createPortal(state.children, state.mountNode); | ||
} | ||
return null; | ||
return (React.createElement("span", { hidden: true, ref: state.virtualParentRootRef }, state.shouldRender && state.mountNode && ReactDOM.createPortal(state.children, state.mountNode))); | ||
}; | ||
@@ -15,0 +12,0 @@ exports.renderPortal = renderPortal; |
@@ -1,2 +0,2 @@ | ||
define(["require", "exports", "@fluentui/react-utilities", "./usePortalMountNode"], function (require, exports, react_utilities_1, usePortalMountNode_1) { | ||
define(["require", "exports", "react", "@fluentui/react-utilities", "./usePortalMountNode", "../../virtualParent/index"], function (require, exports, React, react_utilities_1, usePortalMountNode_1, index_1) { | ||
"use strict"; | ||
@@ -16,5 +16,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); | ||
var _a; | ||
var state = mergeProps({ shouldRender: !react_utilities_1.useIsSSR() }, defaultProps, props); | ||
var virtualParentRootRef = React.useRef(null); | ||
var state = mergeProps({ shouldRender: !react_utilities_1.useIsSSR(), virtualParentRootRef: virtualParentRootRef }, defaultProps, props); | ||
var fallbackMountNode = usePortalMountNode_1.usePortalMountNode({ disabled: !!state.mountNode }); | ||
state.mountNode = (_a = state.mountNode) !== null && _a !== void 0 ? _a : fallbackMountNode; | ||
React.useEffect(function () { | ||
if (state.virtualParentRootRef.current && state.mountNode) { | ||
index_1.setVirtualParent(state.mountNode, state.virtualParentRootRef.current); | ||
} | ||
}, [state.virtualParentRootRef, state.mountNode]); | ||
return state; | ||
@@ -21,0 +27,0 @@ }; |
export * from './components/Portal/index'; | ||
export { elementContains, setVirtualParent } from './virtualParent/index'; |
@@ -1,6 +0,9 @@ | ||
define(["require", "exports", "tslib", "./components/Portal/index"], function (require, exports, tslib_1, index_1) { | ||
define(["require", "exports", "tslib", "./components/Portal/index", "./virtualParent/index"], function (require, exports, tslib_1, index_1, index_2) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.setVirtualParent = exports.elementContains = void 0; | ||
tslib_1.__exportStar(index_1, exports); | ||
Object.defineProperty(exports, "elementContains", { enumerable: true, get: function () { return index_2.elementContains; } }); | ||
Object.defineProperty(exports, "setVirtualParent", { enumerable: true, get: function () { return index_2.setVirtualParent; } }); | ||
}); | ||
//# sourceMappingURL=index.js.map |
@@ -16,2 +16,6 @@ import * as React from 'react'; | ||
shouldRender: boolean; | ||
/** | ||
* Ref to the root span element as virtual parent | ||
*/ | ||
virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>; | ||
} |
@@ -6,2 +6,2 @@ import * as React from 'react'; | ||
*/ | ||
export declare const renderPortal: (state: PortalState) => React.ReactPortal | null; | ||
export declare const renderPortal: (state: PortalState) => React.ReactElement; |
@@ -5,2 +5,3 @@ "use strict"; | ||
var ReactDOM = require("react-dom"); | ||
var React = require("react"); | ||
/** | ||
@@ -10,8 +11,5 @@ * Render the final JSX of Portal | ||
var renderPortal = function (state) { | ||
if (state.shouldRender && state.mountNode) { | ||
return ReactDOM.createPortal(state.children, state.mountNode); | ||
} | ||
return null; | ||
return (React.createElement("span", { hidden: true, ref: state.virtualParentRootRef }, state.shouldRender && state.mountNode && ReactDOM.createPortal(state.children, state.mountNode))); | ||
}; | ||
exports.renderPortal = renderPortal; | ||
//# sourceMappingURL=renderPortal.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.usePortal = void 0; | ||
var React = require("react"); | ||
var react_utilities_1 = require("@fluentui/react-utilities"); | ||
var usePortalMountNode_1 = require("./usePortalMountNode"); | ||
var index_1 = require("../../virtualParent/index"); | ||
var mergeProps = react_utilities_1.makeMergeProps(); | ||
@@ -17,5 +19,11 @@ /** | ||
var _a; | ||
var state = mergeProps({ shouldRender: !react_utilities_1.useIsSSR() }, defaultProps, props); | ||
var virtualParentRootRef = React.useRef(null); | ||
var state = mergeProps({ shouldRender: !react_utilities_1.useIsSSR(), virtualParentRootRef: virtualParentRootRef }, defaultProps, props); | ||
var fallbackMountNode = usePortalMountNode_1.usePortalMountNode({ disabled: !!state.mountNode }); | ||
state.mountNode = (_a = state.mountNode) !== null && _a !== void 0 ? _a : fallbackMountNode; | ||
React.useEffect(function () { | ||
if (state.virtualParentRootRef.current && state.mountNode) { | ||
index_1.setVirtualParent(state.mountNode, state.virtualParentRootRef.current); | ||
} | ||
}, [state.virtualParentRootRef, state.mountNode]); | ||
return state; | ||
@@ -22,0 +30,0 @@ }; |
export * from './components/Portal/index'; | ||
export { elementContains, setVirtualParent } from './virtualParent/index'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.setVirtualParent = exports.elementContains = void 0; | ||
var tslib_1 = require("tslib"); | ||
tslib_1.__exportStar(require("./components/Portal/index"), exports); | ||
var index_1 = require("./virtualParent/index"); | ||
Object.defineProperty(exports, "elementContains", { enumerable: true, get: function () { return index_1.elementContains; } }); | ||
Object.defineProperty(exports, "setVirtualParent", { enumerable: true, get: function () { return index_1.setVirtualParent; } }); | ||
//# sourceMappingURL=index.js.map |
@@ -16,2 +16,6 @@ import * as React from 'react'; | ||
shouldRender: boolean; | ||
/** | ||
* Ref to the root span element as virtual parent | ||
*/ | ||
virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>; | ||
} |
@@ -6,2 +6,2 @@ import * as React from 'react'; | ||
*/ | ||
export declare const renderPortal: (state: PortalState) => React.ReactPortal | null; | ||
export declare const renderPortal: (state: PortalState) => React.ReactElement; |
import * as ReactDOM from 'react-dom'; | ||
import * as React from 'react'; | ||
/** | ||
@@ -6,7 +7,4 @@ * Render the final JSX of Portal | ||
export var renderPortal = function (state) { | ||
if (state.shouldRender && state.mountNode) { | ||
return ReactDOM.createPortal(state.children, state.mountNode); | ||
} | ||
return null; | ||
return (React.createElement("span", { hidden: true, ref: state.virtualParentRootRef }, state.shouldRender && state.mountNode && ReactDOM.createPortal(state.children, state.mountNode))); | ||
}; | ||
//# sourceMappingURL=renderPortal.js.map |
@@ -0,3 +1,5 @@ | ||
import * as React from 'react'; | ||
import { makeMergeProps, useIsSSR } from '@fluentui/react-utilities'; | ||
import { usePortalMountNode } from './usePortalMountNode'; | ||
import { setVirtualParent } from '../../virtualParent/index'; | ||
var mergeProps = makeMergeProps(); | ||
@@ -14,7 +16,13 @@ /** | ||
var _a; | ||
var state = mergeProps({ shouldRender: !useIsSSR() }, defaultProps, props); | ||
var virtualParentRootRef = React.useRef(null); | ||
var state = mergeProps({ shouldRender: !useIsSSR(), virtualParentRootRef: virtualParentRootRef }, defaultProps, props); | ||
var fallbackMountNode = usePortalMountNode({ disabled: !!state.mountNode }); | ||
state.mountNode = (_a = state.mountNode) !== null && _a !== void 0 ? _a : fallbackMountNode; | ||
React.useEffect(function () { | ||
if (state.virtualParentRootRef.current && state.mountNode) { | ||
setVirtualParent(state.mountNode, state.virtualParentRootRef.current); | ||
} | ||
}, [state.virtualParentRootRef, state.mountNode]); | ||
return state; | ||
}; | ||
//# sourceMappingURL=usePortal.js.map |
export * from './components/Portal/index'; | ||
export { elementContains, setVirtualParent } from './virtualParent/index'; |
export * from './components/Portal/index'; | ||
export { elementContains, setVirtualParent } from './virtualParent/index'; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@fluentui/react-portal", | ||
"version": "9.0.0-alpha.12", | ||
"version": "9.0.0-alpha.13", | ||
"description": "A utility component that creates portals compatible with Fluent UI", | ||
@@ -27,4 +27,4 @@ "main": "lib-commonjs/index.js", | ||
"@fluentui/eslint-plugin": "^1.2.0", | ||
"@fluentui/jest-serializer-make-styles": "^9.0.0-alpha.11", | ||
"@fluentui/react-conformance": "^0.3.0", | ||
"@fluentui/jest-serializer-make-styles": "^9.0.0-alpha.12", | ||
"@fluentui/react-conformance": "^0.3.1", | ||
"@fluentui/scripts": "^1.0.0", | ||
@@ -43,5 +43,5 @@ "@types/enzyme": "3.10.3", | ||
"dependencies": { | ||
"@fluentui/react-make-styles": "^9.0.0-alpha.34", | ||
"@fluentui/react-shared-contexts": "^9.0.0-alpha.10", | ||
"@fluentui/react-utilities": "^9.0.0-alpha.21", | ||
"@fluentui/react-make-styles": "^9.0.0-alpha.35", | ||
"@fluentui/react-shared-contexts": "^9.0.0-alpha.11", | ||
"@fluentui/react-utilities": "^9.0.0-alpha.22", | ||
"tslib": "^2.1.0" | ||
@@ -48,0 +48,0 @@ }, |
114
README.md
@@ -34,1 +34,115 @@ # @fluentui/react-portal | ||
`Portal` renders React children directly to the default/configured DOM node. Therefore styling should be applied to the `children` by users directly. | ||
### Virtual parents | ||
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. | ||
```tsx | ||
const outerButtonRef = React.useRef(); | ||
const innerButtonRef = React.useRef(); | ||
<Portal> | ||
<div> | ||
<button ref={outerButtonRef}> Outer button </button> | ||
<Portal> | ||
<div> | ||
<button ref={innerButtonRef}> Inner button </button> | ||
</div> | ||
</Portal> | ||
</div> | ||
</Portal> | ||
// DOM output | ||
<div> | ||
<button>Outer button</button> | ||
</div> | ||
<div> | ||
<button>Inner button</button> | ||
</div> | ||
// Let's add an event listener to 'dismss' the outer portal when clicked outside | ||
// ⚠⚠⚠ This will always be called when clicking on the inner button | ||
document.addEventListener((event) => { | ||
if (outerButtonRef.current.contains(event.target)) { | ||
dismissOuterPortal(); | ||
} | ||
}) | ||
``` | ||
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` | ||
`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. | ||
- `setVirtualParent` - sets virtual parent. Portal uses this already internally. | ||
- `elementContains` - similar to `element.contains` but uses the virtual hierarchy as reference | ||
Below shows what a virtual parent is | ||
```tsx | ||
// Setting a virtual parent | ||
const parent document.getElementById('parent') | ||
const child document.getElement.ById('child'); | ||
child._virtual.parent = parent; | ||
``` | ||
`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. | ||
```tsx | ||
<FluentProvider> | ||
<Portal id="portal-1" /> | ||
<Portal id="portal-2" /> | ||
</FluentProvider> | ||
``` | ||
DOM output: | ||
```tsx | ||
<body> | ||
<div> | ||
{/* Virtual parent for portal*/} | ||
<span aria-hidden /> | ||
{/* Virtual parent for portal*/} | ||
<span aria-hidden /> | ||
</div> | ||
<div id="portal-1" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
<div id="portal-2" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
</body> | ||
``` | ||
```tsx | ||
<FluentProvider> | ||
<Portal id="portal-1"> | ||
<Portal id="portal-2" /> | ||
</Portal> | ||
</FluentProvider> | ||
``` | ||
DOM output: | ||
```tsx | ||
<body> | ||
<div> | ||
{/* Virtual parent for outer portal*/} | ||
<span aria-hidden></span> | ||
</div> | ||
<div id="portal-1" class="theme-provider-0"> | ||
{/* Virtual parent for inner portal*/} | ||
<span aria-hidden /> | ||
{children} | ||
</div> | ||
<div id="portal-2" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
</body> | ||
``` |
106
Spec.md
@@ -111,9 +111,66 @@ # Portal Spec | ||
### Virtual parents | ||
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. | ||
```tsx | ||
const outerButtonRef = React.useRef(); | ||
const innerButtonRef = React.useRef(); | ||
<Portal> | ||
<div> | ||
<button ref={outerButtonRef}> Outer button </button> | ||
<Portal> | ||
<div> | ||
<button ref={innerButtonRef}> Inner button </button> | ||
</div> | ||
</Portal> | ||
</div> | ||
</Portal> | ||
// DOM output | ||
<div> | ||
<button>Outer button</button> | ||
</div> | ||
<div> | ||
<button>Inner button</button> | ||
</div> | ||
// Let's add an event listener to 'dismss' the outer portal when clicked outside | ||
// ⚠⚠⚠ This will always be called when clicking on the inner button | ||
document.addEventListener((event) => { | ||
if (outerButtonRef.current.contains(event.target)) { | ||
dismissOuterPortal(); | ||
} | ||
}) | ||
``` | ||
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` | ||
`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. | ||
- `setVirtualParent` - sets virtual parent | ||
- `elementContains` - similar to `element.contains` but uses the virtual hierarchy as reference | ||
Below shows what a virtual parent is | ||
```tsx | ||
// Setting a virtual parent | ||
const parent document.getElementById('parent') | ||
const child document.getElement.ById('child'); | ||
child._virtual.parent = parent; | ||
``` | ||
## Structure | ||
``` | ||
<FluentProvider | ||
```tsx | ||
<FluentProvider> | ||
<Portal id="portal-1" /> | ||
<Portal id="portal-2" /> | ||
</FluentProvider | ||
</FluentProvider> | ||
``` | ||
@@ -125,9 +182,46 @@ | ||
<body> | ||
<div>Maintree</div> | ||
<div> | ||
{/* Virtual parent for portal*/} | ||
<span aria-hidden /> | ||
{/* Virtual parent for portal*/} | ||
<span aria-hidden /> | ||
</div> | ||
<div id="portal-1" class="theme-provider-0"}>{children}</div> | ||
<div id="portal-2" class="theme-provider-0"}>{children}</div> | ||
<div id="portal-1" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
<div id="portal-2" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
</body> | ||
``` | ||
```tsx | ||
<FluentProvider> | ||
<Portal id="portal-1"> | ||
<Portal id="portal-2" /> | ||
</Portal> | ||
</FluentProvider> | ||
``` | ||
DOM output: | ||
```tsx | ||
<body> | ||
<div> | ||
{/* Virtual parent for outer portal*/} | ||
<span aria-hidden></span> | ||
</div> | ||
<div id="portal-1" class="theme-provider-0"> | ||
{/* Virtual parent for inner portal*/} | ||
<span aria-hidden /> | ||
{children} | ||
</div> | ||
<div id="portal-2" class="theme-provider-0"> | ||
{children} | ||
</div> | ||
</body> | ||
``` | ||
## Migration | ||
@@ -134,0 +228,0 @@ |
@@ -24,2 +24,7 @@ import * as React from 'react'; | ||
shouldRender: boolean; | ||
/** | ||
* Ref to the root span element as virtual parent | ||
*/ | ||
virtualParentRootRef: React.MutableRefObject<HTMLSpanElement | null>; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
118553
157
1330
148