@fluentui/react-portal 9.5.6 → 9.5.7
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.md +12 -2
- package/lib/components/Portal/usePortal.js +0 -1
- package/lib/components/Portal/usePortal.js.map +1 -1
- package/lib/components/Portal/usePortalMountNode.js +160 -60
- package/lib/components/Portal/usePortalMountNode.js.map +1 -1
- package/lib-commonjs/components/Portal/usePortal.js +0 -1
- package/lib-commonjs/components/Portal/usePortal.js.map +1 -1
- package/lib-commonjs/components/Portal/usePortalMountNode.js +160 -60
- package/lib-commonjs/components/Portal/usePortalMountNode.js.map +1 -1
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
# Change Log - @fluentui/react-portal
|
2
2
|
|
3
|
-
This log was last generated on
|
3
|
+
This log was last generated on Wed, 14 May 2025 18:45:47 GMT and should not be manually modified.
|
4
4
|
|
5
5
|
<!-- Start content -->
|
6
6
|
|
7
|
+
## [9.5.7](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.5.7)
|
8
|
+
|
9
|
+
Wed, 14 May 2025 18:45:47 GMT
|
10
|
+
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.5.6..@fluentui/react-portal_v9.5.7)
|
11
|
+
|
12
|
+
### Patches
|
13
|
+
|
14
|
+
- fix: improve support of concurrent mode, work with React 19 ([PR #33978](https://github.com/microsoft/fluentui/pull/33978) by olfedias@microsoft.com)
|
15
|
+
- Bump @fluentui/react-tabster to v9.24.7 ([PR #34449](https://github.com/microsoft/fluentui/pull/34449) by beachball)
|
16
|
+
|
7
17
|
## [9.5.6](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.5.6)
|
8
18
|
|
9
|
-
Thu, 24 Apr 2025 09:
|
19
|
+
Thu, 24 Apr 2025 09:59:45 GMT
|
10
20
|
[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.5.5..@fluentui/react-portal_v9.5.6)
|
11
21
|
|
12
22
|
### Patches
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/components/Portal/usePortal.ts"],"sourcesContent":["import { setVirtualParent } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { toMountNodeProps } from '../../utils/toMountNodeProps';\nimport { usePortalMountNode } from './usePortalMountNode';\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 { element, className } = toMountNodeProps(props.mountNode);\n\n const virtualParentRootRef = React.useRef<HTMLSpanElement>(null);\n const fallbackElement = usePortalMountNode({ disabled: !!element, className });\n\n const mountNode = element ?? fallbackElement;\n const state: PortalState = {\n children: props.children,\n mountNode,\n virtualParentRootRef,\n };\n\n React.useEffect(() => {\n if (!mountNode) {\n return;\n }\n\n const virtualParent = virtualParentRootRef.current;\n\n // By default, we create a mount node for portal on `document.body` (see usePortalMountNode()) and have following structure:\n //\n // <body>\n // <!-- ⚛️ application root -->\n // <div id=\"root\">\n // <!-- ⬇️ portal node rendered in a tree to anchor (virtual parent node) -->\n // <span aria-hidden=\"true\"></span>\n // </div>\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n // </div>\n // </body>\n //\n // To make sure that `.elementContains()` works correctly, we link a virtual parent to a portal node (a virtual parent node becomes a parent of mount node):\n // virtual.contains(mountNode) === false\n // (while we need ⬇️⬇️⬇️)\n // elementsContains(virtualParent, mountNode) === true\n // elementsContains(mountNode, virtualParent) === false\n //\n // For more details, check docs for virtual parent utils.\n //\n // However, if a user provides a custom mount node (via `props`) the structure could be different:\n //\n // <body>\n // <!-- application root -->\n // <div id=\"root\">\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n //\n // <span aria-hidden=\"true\"></span>\n // </div>\n // </div>\n // </body>\n //\n // A mount node in this case contains portal's content and a virtual parent node. In this case nodes linking is redundant and the check below avoids it.\n //\n // Otherwise, there is a circular reference - both elements are parents of each other:\n // elementsContains(mountNode, virtualParent) === true\n // elementsContains(virtualParent, mountNode) === true\n const isVirtualParentInsideChild = mountNode.contains(virtualParent);\n\n if (virtualParent && !isVirtualParentInsideChild) {\n setVirtualParent(mountNode, virtualParent);\n\n return () => {\n setVirtualParent(mountNode, undefined);\n };\n }\n }, [
|
1
|
+
{"version":3,"sources":["../src/components/Portal/usePortal.ts"],"sourcesContent":["import { setVirtualParent } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { toMountNodeProps } from '../../utils/toMountNodeProps';\nimport { usePortalMountNode } from './usePortalMountNode';\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 { element, className } = toMountNodeProps(props.mountNode);\n\n const virtualParentRootRef = React.useRef<HTMLSpanElement>(null);\n const fallbackElement = usePortalMountNode({ disabled: !!element, className });\n\n const mountNode = element ?? fallbackElement;\n const state: PortalState = {\n children: props.children,\n mountNode,\n virtualParentRootRef,\n };\n\n React.useEffect(() => {\n if (!mountNode) {\n return;\n }\n\n const virtualParent = virtualParentRootRef.current;\n\n // By default, we create a mount node for portal on `document.body` (see usePortalMountNode()) and have following structure:\n //\n // <body>\n // <!-- ⚛️ application root -->\n // <div id=\"root\">\n // <!-- ⬇️ portal node rendered in a tree to anchor (virtual parent node) -->\n // <span aria-hidden=\"true\"></span>\n // </div>\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n // </div>\n // </body>\n //\n // To make sure that `.elementContains()` works correctly, we link a virtual parent to a portal node (a virtual parent node becomes a parent of mount node):\n // virtual.contains(mountNode) === false\n // (while we need ⬇️⬇️⬇️)\n // elementsContains(virtualParent, mountNode) === true\n // elementsContains(mountNode, virtualParent) === false\n //\n // For more details, check docs for virtual parent utils.\n //\n // However, if a user provides a custom mount node (via `props`) the structure could be different:\n //\n // <body>\n // <!-- application root -->\n // <div id=\"root\">\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n //\n // <span aria-hidden=\"true\"></span>\n // </div>\n // </div>\n // </body>\n //\n // A mount node in this case contains portal's content and a virtual parent node. In this case nodes linking is redundant and the check below avoids it.\n //\n // Otherwise, there is a circular reference - both elements are parents of each other:\n // elementsContains(mountNode, virtualParent) === true\n // elementsContains(virtualParent, mountNode) === true\n const isVirtualParentInsideChild = mountNode.contains(virtualParent);\n\n if (virtualParent && !isVirtualParentInsideChild) {\n setVirtualParent(mountNode, virtualParent);\n\n return () => {\n setVirtualParent(mountNode, undefined);\n };\n }\n }, [mountNode]);\n\n return state;\n};\n"],"names":["setVirtualParent","React","toMountNodeProps","usePortalMountNode","usePortal_unstable","props","element","className","mountNode","virtualParentRootRef","useRef","fallbackElement","disabled","state","children","useEffect","virtualParent","current","isVirtualParentInsideChild","contains","undefined"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA,SAASA,gBAAgB,QAAQ,4BAA4B;AAC7D,YAAYC,WAAW,QAAQ;AAE/B,SAASC,gBAAgB,QAAQ,+BAA+B;AAChE,SAASC,kBAAkB,QAAQ,uBAAuB;AAG1D;;;;;;CAMC,GACD,OAAO,MAAMC,qBAAqB,CAACC;IACjC,MAAM,EAAEC,OAAO,EAAEC,SAAS,EAAE,GAAGL,iBAAiBG,MAAMG,SAAS;IAE/D,MAAMC,uBAAuBR,MAAMS,MAAM,CAAkB;IAC3D,MAAMC,kBAAkBR,mBAAmB;QAAES,UAAU,CAAC,CAACN;QAASC;IAAU;IAE5E,MAAMC,YAAYF,oBAAAA,qBAAAA,UAAWK;IAC7B,MAAME,QAAqB;QACzBC,UAAUT,MAAMS,QAAQ;QACxBN;QACAC;IACF;IAEAR,MAAMc,SAAS,CAAC;QACd,IAAI,CAACP,WAAW;YACd;QACF;QAEA,MAAMQ,gBAAgBP,qBAAqBQ,OAAO;QAElD,4HAA4H;QAC5H,EAAE;QACF,SAAS;QACT,iCAAiC;QACjC,oBAAoB;QACpB,iFAAiF;QACjF,uCAAuC;QACvC,WAAW;QACX,iCAAiC;QACjC,gCAAgC;QAChC,WAAW;QACX,UAAU;QACV,EAAE;QACF,4JAA4J;QAC5J,0CAA0C;QAC1C,2BAA2B;QAC3B,wDAAwD;QACxD,yDAAyD;QACzD,EAAE;QACF,yDAAyD;QACzD,EAAE;QACF,kGAAkG;QAClG,EAAE;QACF,SAAS;QACT,8BAA8B;QAC9B,oBAAoB;QACpB,mCAAmC;QACnC,kCAAkC;QAClC,EAAE;QACF,yCAAyC;QACzC,aAAa;QACb,WAAW;QACX,UAAU;QACV,EAAE;QACF,wJAAwJ;QACxJ,EAAE;QACF,sFAAsF;QACtF,wDAAwD;QACxD,wDAAwD;QACxD,MAAMC,6BAA6BV,UAAUW,QAAQ,CAACH;QAEtD,IAAIA,iBAAiB,CAACE,4BAA4B;YAChDlB,iBAAiBQ,WAAWQ;YAE5B,OAAO;gBACLhB,iBAAiBQ,WAAWY;YAC9B;QACF;IACF,GAAG;QAACZ;KAAU;IAEd,OAAOK;AACT,EAAE"}
|
@@ -2,9 +2,158 @@ import * as React from 'react';
|
|
2
2
|
import { useThemeClassName_unstable as useThemeClassName, useFluent_unstable as useFluent, usePortalMountNode as usePortalMountNodeContext } from '@fluentui/react-shared-contexts';
|
3
3
|
import { mergeClasses } from '@griffel/react';
|
4
4
|
import { useFocusVisible } from '@fluentui/react-tabster';
|
5
|
-
import { useDisposable } from 'use-disposable';
|
6
5
|
import { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';
|
7
6
|
const useInsertionEffect = React['useInsertion' + 'Effect'];
|
7
|
+
/**
|
8
|
+
* Legacy element factory for React 17 and below. It's not safe for concurrent rendering.
|
9
|
+
*
|
10
|
+
* Creates a new element on a "document.body" to mount portals.
|
11
|
+
*/ const useLegacyElementFactory = (options)=>{
|
12
|
+
const { className, dir, focusVisibleRef, targetNode } = options;
|
13
|
+
const targetElement = React.useMemo(()=>{
|
14
|
+
if (targetNode === undefined || options.disabled) {
|
15
|
+
return null;
|
16
|
+
}
|
17
|
+
const element = targetNode.ownerDocument.createElement('div');
|
18
|
+
targetNode.appendChild(element);
|
19
|
+
return element;
|
20
|
+
}, [
|
21
|
+
targetNode,
|
22
|
+
options.disabled
|
23
|
+
]);
|
24
|
+
// Heads up!
|
25
|
+
// This useMemo() call is intentional for React 17 & below.
|
26
|
+
//
|
27
|
+
// We don't want to re-create the portal element when its attributes change. This also cannot not be done in an effect
|
28
|
+
// because, changing the value of CSS variables after an initial mount will trigger interesting CSS side effects like
|
29
|
+
// transitions.
|
30
|
+
React.useMemo(()=>{
|
31
|
+
if (!targetElement) {
|
32
|
+
return;
|
33
|
+
}
|
34
|
+
targetElement.className = className;
|
35
|
+
targetElement.setAttribute('dir', dir);
|
36
|
+
targetElement.setAttribute('data-portal-node', 'true');
|
37
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
38
|
+
focusVisibleRef.current = targetElement;
|
39
|
+
}, [
|
40
|
+
className,
|
41
|
+
dir,
|
42
|
+
targetElement,
|
43
|
+
focusVisibleRef
|
44
|
+
]);
|
45
|
+
React.useEffect(()=>{
|
46
|
+
return ()=>{
|
47
|
+
targetElement === null || targetElement === void 0 ? void 0 : targetElement.remove();
|
48
|
+
};
|
49
|
+
}, [
|
50
|
+
targetElement
|
51
|
+
]);
|
52
|
+
return targetElement;
|
53
|
+
};
|
54
|
+
/**
|
55
|
+
* This is a modern element factory for React 18 and above. It is safe for concurrent rendering.
|
56
|
+
*
|
57
|
+
* It abuses the fact that React will mount DOM once (unlike hooks), so by using a proxy we can intercept:
|
58
|
+
* - the `remove()` method (we call it in `useEffect()`) and remove the element only when the portal is unmounted
|
59
|
+
* - all other methods (and properties) will be called by React once a portal is mounted
|
60
|
+
*/ const useModernElementFactory = (options)=>{
|
61
|
+
const { className, dir, focusVisibleRef, targetNode } = options;
|
62
|
+
const [elementFactory] = React.useState(()=>{
|
63
|
+
let currentElement = undefined;
|
64
|
+
function get(targetRoot, forceCreation) {
|
65
|
+
if (currentElement) {
|
66
|
+
return currentElement;
|
67
|
+
}
|
68
|
+
if (forceCreation) {
|
69
|
+
currentElement = targetRoot.ownerDocument.createElement('div');
|
70
|
+
targetRoot.appendChild(currentElement);
|
71
|
+
}
|
72
|
+
return currentElement;
|
73
|
+
}
|
74
|
+
function dispose() {
|
75
|
+
if (currentElement) {
|
76
|
+
currentElement.remove();
|
77
|
+
currentElement = undefined;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
return {
|
81
|
+
get,
|
82
|
+
dispose
|
83
|
+
};
|
84
|
+
});
|
85
|
+
const elementProxy = React.useMemo(()=>{
|
86
|
+
if (targetNode === undefined || options.disabled) {
|
87
|
+
return null;
|
88
|
+
}
|
89
|
+
return new Proxy({}, {
|
90
|
+
get (_, property) {
|
91
|
+
// Heads up!
|
92
|
+
// We intercept the `remove()` method to remove the mount node only when portal has been unmounted already.
|
93
|
+
if (property === 'remove') {
|
94
|
+
const targetElement = elementFactory.get(targetNode, false);
|
95
|
+
if (targetElement) {
|
96
|
+
// If the mountElement has children, the portal is still mounted
|
97
|
+
const portalHasNoChildren = targetElement.childNodes.length === 0;
|
98
|
+
if (portalHasNoChildren) {
|
99
|
+
return targetElement.remove.bind(targetElement);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
return ()=>{
|
103
|
+
// If the mountElement has children, ignore the remove call
|
104
|
+
};
|
105
|
+
}
|
106
|
+
const targetElement = elementFactory.get(targetNode, true);
|
107
|
+
const targetProperty = targetElement[property];
|
108
|
+
if (typeof targetProperty === 'function') {
|
109
|
+
return targetProperty.bind(targetElement);
|
110
|
+
}
|
111
|
+
return targetProperty;
|
112
|
+
},
|
113
|
+
set (_, property, value) {
|
114
|
+
const targetElement = elementFactory.get(targetNode, true);
|
115
|
+
if (targetElement) {
|
116
|
+
Object.assign(targetElement, {
|
117
|
+
[property]: value
|
118
|
+
});
|
119
|
+
return true;
|
120
|
+
}
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
});
|
124
|
+
}, [
|
125
|
+
elementFactory,
|
126
|
+
targetNode,
|
127
|
+
options.disabled
|
128
|
+
]);
|
129
|
+
React.useEffect(()=>{
|
130
|
+
return ()=>{
|
131
|
+
elementProxy === null || elementProxy === void 0 ? void 0 : elementProxy.remove();
|
132
|
+
};
|
133
|
+
}, [
|
134
|
+
elementProxy
|
135
|
+
]);
|
136
|
+
useInsertionEffect(()=>{
|
137
|
+
if (!elementProxy) {
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
const classesToApply = className.split(' ').filter(Boolean);
|
141
|
+
elementProxy.classList.add(...classesToApply);
|
142
|
+
elementProxy.setAttribute('dir', dir);
|
143
|
+
elementProxy.setAttribute('data-portal-node', 'true');
|
144
|
+
focusVisibleRef.current = elementProxy;
|
145
|
+
return ()=>{
|
146
|
+
elementProxy.classList.remove(...classesToApply);
|
147
|
+
elementProxy.removeAttribute('dir');
|
148
|
+
};
|
149
|
+
}, [
|
150
|
+
className,
|
151
|
+
dir,
|
152
|
+
elementProxy,
|
153
|
+
focusVisibleRef
|
154
|
+
]);
|
155
|
+
return elementProxy;
|
156
|
+
};
|
8
157
|
/**
|
9
158
|
* Creates a new element on a "document.body" to mount portals.
|
10
159
|
*/ export const usePortalMountNode = (options)=>{
|
@@ -14,66 +163,17 @@ const useInsertionEffect = React['useInsertion' + 'Effect'];
|
|
14
163
|
const focusVisibleRef = useFocusVisible();
|
15
164
|
const classes = usePortalMountNodeStylesStyles();
|
16
165
|
const themeClassName = useThemeClassName();
|
17
|
-
const
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
];
|
25
|
-
}
|
26
|
-
const newElement = targetNode.ownerDocument.createElement('div');
|
27
|
-
targetNode.appendChild(newElement);
|
28
|
-
return [
|
29
|
-
newElement,
|
30
|
-
()=>newElement.remove()
|
31
|
-
];
|
32
|
-
}, [
|
33
|
-
targetNode
|
34
|
-
]);
|
166
|
+
const factoryOptions = {
|
167
|
+
dir,
|
168
|
+
disabled: options.disabled,
|
169
|
+
focusVisibleRef,
|
170
|
+
className: mergeClasses(themeClassName, classes.root, options.className),
|
171
|
+
targetNode: mountNode !== null && mountNode !== void 0 ? mountNode : targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body
|
172
|
+
};
|
35
173
|
if (useInsertionEffect) {
|
36
174
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
37
|
-
|
38
|
-
if (!element) {
|
39
|
-
return;
|
40
|
-
}
|
41
|
-
const classesToApply = className.split(' ').filter(Boolean);
|
42
|
-
element.classList.add(...classesToApply);
|
43
|
-
element.setAttribute('dir', dir);
|
44
|
-
element.setAttribute('data-portal-node', 'true');
|
45
|
-
focusVisibleRef.current = element;
|
46
|
-
return ()=>{
|
47
|
-
element.classList.remove(...classesToApply);
|
48
|
-
element.removeAttribute('dir');
|
49
|
-
};
|
50
|
-
}, [
|
51
|
-
className,
|
52
|
-
dir,
|
53
|
-
element,
|
54
|
-
focusVisibleRef
|
55
|
-
]);
|
56
|
-
} else {
|
57
|
-
// This useMemo call is intentional for React 17
|
58
|
-
// We don't want to re-create the portal element when its attributes change.
|
59
|
-
// This also should not be done in an effect because, changing the value of css variables
|
60
|
-
// after initial mount can trigger interesting CSS side effects like transitions.
|
61
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
62
|
-
React.useMemo(()=>{
|
63
|
-
if (!element) {
|
64
|
-
return;
|
65
|
-
}
|
66
|
-
// Force replace all classes
|
67
|
-
element.className = className;
|
68
|
-
element.setAttribute('dir', dir);
|
69
|
-
element.setAttribute('data-portal-node', 'true');
|
70
|
-
focusVisibleRef.current = element;
|
71
|
-
}, [
|
72
|
-
className,
|
73
|
-
dir,
|
74
|
-
element,
|
75
|
-
focusVisibleRef
|
76
|
-
]);
|
175
|
+
return useModernElementFactory(factoryOptions);
|
77
176
|
}
|
78
|
-
|
177
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
178
|
+
return useLegacyElementFactory(factoryOptions);
|
79
179
|
};
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/components/Portal/usePortalMountNode.ts"],"sourcesContent":["import * as React from 'react';\nimport {\n useThemeClassName_unstable as useThemeClassName,\n useFluent_unstable as useFluent,\n usePortalMountNode as usePortalMountNodeContext,\n} from '@fluentui/react-shared-contexts';\nimport { mergeClasses } from '@griffel/react';\nimport { useFocusVisible } from '@fluentui/react-tabster';\nimport { useDisposable } from 'use-disposable';\n\nimport { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';\n\nconst useInsertionEffect = (React as never)['useInsertion' + 'Effect'] as typeof React.useLayoutEffect | undefined;\n\nexport type UsePortalMountNodeOptions = {\n /**\n * Since hooks cannot be called conditionally use this flag to disable creating the node\n */\n disabled?: boolean;\n\n className?: string;\n};\n\n/**\n * Creates a new element on a \"document.body\" to mount portals.\n */\nexport const usePortalMountNode = (options: UsePortalMountNodeOptions): HTMLElement | null => {\n 'use no memo';\n\n const { targetDocument, dir } = useFluent();\n const mountNode = usePortalMountNodeContext();\n\n const focusVisibleRef = useFocusVisible<HTMLDivElement>() as React.MutableRefObject<HTMLElement | null>;\n const classes = usePortalMountNodeStylesStyles();\n const themeClassName = useThemeClassName();\n\n const className = mergeClasses(themeClassName, classes.root, options.className);\n const targetNode: HTMLElement | ShadowRoot | undefined = mountNode ?? targetDocument?.body;\n\n const element = useDisposable(() => {\n if (targetNode === undefined || options.disabled) {\n return [null, () => null];\n }\n\n const newElement = targetNode.ownerDocument.createElement('div');\n targetNode.appendChild(newElement);\n return [newElement, () => newElement.remove()];\n }, [targetNode]);\n\n if (useInsertionEffect) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useInsertionEffect(() => {\n if (!element) {\n return;\n }\n\n const classesToApply = className.split(' ').filter(Boolean);\n\n element.classList.add(...classesToApply);\n element.setAttribute('dir', dir);\n element.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = element;\n\n return () => {\n element.classList.remove(...classesToApply);\n element.removeAttribute('dir');\n };\n }, [className, dir, element, focusVisibleRef]);\n } else {\n // This useMemo call is intentional for React 17\n // We don't want to re-create the portal element when its attributes change.\n // This also should not be done in an effect because, changing the value of css variables\n // after initial mount can trigger interesting CSS side effects like transitions.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n React.useMemo(() => {\n if (!element) {\n return;\n }\n\n // Force replace all classes\n element.className = className;\n element.setAttribute('dir', dir);\n element.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = element;\n }, [className, dir, element, focusVisibleRef]);\n }\n\n return element;\n};\n"],"names":["React","useThemeClassName_unstable","useThemeClassName","useFluent_unstable","useFluent","usePortalMountNode","usePortalMountNodeContext","mergeClasses","useFocusVisible","useDisposable","usePortalMountNodeStylesStyles","useInsertionEffect","options","targetDocument","dir","mountNode","focusVisibleRef","classes","themeClassName","className","root","targetNode","body","element","undefined","disabled","newElement","ownerDocument","createElement","appendChild","remove","classesToApply","split","filter","Boolean","classList","add","setAttribute","current","removeAttribute","useMemo"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SACEC,8BAA8BC,iBAAiB,EAC/CC,sBAAsBC,SAAS,EAC/BC,sBAAsBC,yBAAyB,QAC1C,kCAAkC;AACzC,SAASC,YAAY,QAAQ,iBAAiB;AAC9C,SAASC,eAAe,QAAQ,0BAA0B;AAC1D,SAASC,aAAa,QAAQ,iBAAiB;AAE/C,SAASC,8BAA8B,QAAQ,oCAAoC;AAEnF,MAAMC,qBAAqB,AAACX,KAAe,CAAC,iBAAiB,SAAS;AAWtE;;CAEC,GACD,OAAO,MAAMK,qBAAqB,CAACO;IACjC;IAEA,MAAM,EAAEC,cAAc,EAAEC,GAAG,EAAE,GAAGV;IAChC,MAAMW,YAAYT;IAElB,MAAMU,kBAAkBR;IACxB,MAAMS,UAAUP;IAChB,MAAMQ,iBAAiBhB;IAEvB,MAAMiB,YAAYZ,aAAaW,gBAAgBD,QAAQG,IAAI,EAAER,QAAQO,SAAS;IAC9E,MAAME,aAAmDN,sBAAAA,uBAAAA,YAAaF,2BAAAA,qCAAAA,eAAgBS,IAAI;IAE1F,MAAMC,UAAUd,cAAc;QAC5B,IAAIY,eAAeG,aAAaZ,QAAQa,QAAQ,EAAE;YAChD,OAAO;gBAAC;gBAAM,IAAM;aAAK;QAC3B;QAEA,MAAMC,aAAaL,WAAWM,aAAa,CAACC,aAAa,CAAC;QAC1DP,WAAWQ,WAAW,CAACH;QACvB,OAAO;YAACA;YAAY,IAAMA,WAAWI,MAAM;SAAG;IAChD,GAAG;QAACT;KAAW;IAEf,IAAIV,oBAAoB;QACtB,sDAAsD;QACtDA,mBAAmB;YACjB,IAAI,CAACY,SAAS;gBACZ;YACF;YAEA,MAAMQ,iBAAiBZ,UAAUa,KAAK,CAAC,KAAKC,MAAM,CAACC;YAEnDX,QAAQY,SAAS,CAACC,GAAG,IAAIL;YACzBR,QAAQc,YAAY,CAAC,OAAOvB;YAC5BS,QAAQc,YAAY,CAAC,oBAAoB;YAEzCrB,gBAAgBsB,OAAO,GAAGf;YAE1B,OAAO;gBACLA,QAAQY,SAAS,CAACL,MAAM,IAAIC;gBAC5BR,QAAQgB,eAAe,CAAC;YAC1B;QACF,GAAG;YAACpB;YAAWL;YAAKS;YAASP;SAAgB;IAC/C,OAAO;QACL,gDAAgD;QAChD,4EAA4E;QAC5E,yFAAyF;QACzF,iFAAiF;QACjF,sDAAsD;QACtDhB,MAAMwC,OAAO,CAAC;YACZ,IAAI,CAACjB,SAAS;gBACZ;YACF;YAEA,4BAA4B;YAC5BA,QAAQJ,SAAS,GAAGA;YACpBI,QAAQc,YAAY,CAAC,OAAOvB;YAC5BS,QAAQc,YAAY,CAAC,oBAAoB;YAEzCrB,gBAAgBsB,OAAO,GAAGf;QAC5B,GAAG;YAACJ;YAAWL;YAAKS;YAASP;SAAgB;IAC/C;IAEA,OAAOO;AACT,EAAE"}
|
1
|
+
{"version":3,"sources":["../src/components/Portal/usePortalMountNode.ts"],"sourcesContent":["import * as React from 'react';\nimport {\n useThemeClassName_unstable as useThemeClassName,\n useFluent_unstable as useFluent,\n usePortalMountNode as usePortalMountNodeContext,\n} from '@fluentui/react-shared-contexts';\nimport { mergeClasses } from '@griffel/react';\nimport { useFocusVisible } from '@fluentui/react-tabster';\n\nimport { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';\n\nconst useInsertionEffect = (React as never)['useInsertion' + 'Effect'] as typeof React.useLayoutEffect | undefined;\n\nexport type UsePortalMountNodeOptions = {\n /**\n * Since hooks cannot be called conditionally use this flag to disable creating the node\n */\n disabled?: boolean;\n\n className?: string;\n};\n\ntype UseElementFactoryOptions = {\n className: string;\n dir: string;\n disabled: boolean | undefined;\n focusVisibleRef: React.MutableRefObject<HTMLElement | null>;\n targetNode: HTMLElement | ShadowRoot | undefined;\n};\ntype UseElementFactory = (options: UseElementFactoryOptions) => HTMLDivElement | null;\n\n/**\n * Legacy element factory for React 17 and below. It's not safe for concurrent rendering.\n *\n * Creates a new element on a \"document.body\" to mount portals.\n */\nconst useLegacyElementFactory: UseElementFactory = options => {\n const { className, dir, focusVisibleRef, targetNode } = options;\n\n const targetElement = React.useMemo(() => {\n if (targetNode === undefined || options.disabled) {\n return null;\n }\n\n const element = targetNode.ownerDocument.createElement('div');\n targetNode.appendChild(element);\n\n return element;\n }, [targetNode, options.disabled]);\n\n // Heads up!\n // This useMemo() call is intentional for React 17 & below.\n //\n // We don't want to re-create the portal element when its attributes change. This also cannot not be done in an effect\n // because, changing the value of CSS variables after an initial mount will trigger interesting CSS side effects like\n // transitions.\n React.useMemo(() => {\n if (!targetElement) {\n return;\n }\n\n targetElement.className = className;\n targetElement.setAttribute('dir', dir);\n targetElement.setAttribute('data-portal-node', 'true');\n\n // eslint-disable-next-line react-compiler/react-compiler\n focusVisibleRef.current = targetElement;\n }, [className, dir, targetElement, focusVisibleRef]);\n\n React.useEffect(() => {\n return () => {\n targetElement?.remove();\n };\n }, [targetElement]);\n\n return targetElement;\n};\n\n/**\n * This is a modern element factory for React 18 and above. It is safe for concurrent rendering.\n *\n * It abuses the fact that React will mount DOM once (unlike hooks), so by using a proxy we can intercept:\n * - the `remove()` method (we call it in `useEffect()`) and remove the element only when the portal is unmounted\n * - all other methods (and properties) will be called by React once a portal is mounted\n */\nconst useModernElementFactory: UseElementFactory = options => {\n const { className, dir, focusVisibleRef, targetNode } = options;\n\n const [elementFactory] = React.useState(() => {\n let currentElement: HTMLDivElement | undefined = undefined;\n\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: false): HTMLDivElement | undefined;\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: true): HTMLDivElement;\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: boolean): HTMLDivElement | undefined {\n if (currentElement) {\n return currentElement;\n }\n\n if (forceCreation) {\n currentElement = targetRoot.ownerDocument.createElement('div');\n targetRoot.appendChild(currentElement);\n }\n\n return currentElement;\n }\n\n function dispose() {\n if (currentElement) {\n currentElement.remove();\n currentElement = undefined;\n }\n }\n\n return {\n get,\n dispose,\n };\n });\n\n const elementProxy = React.useMemo(() => {\n if (targetNode === undefined || options.disabled) {\n return null;\n }\n\n return new Proxy({} as HTMLDivElement, {\n get(_, property: keyof HTMLDivElement) {\n // Heads up!\n // We intercept the `remove()` method to remove the mount node only when portal has been unmounted already.\n if (property === 'remove') {\n const targetElement = elementFactory.get(targetNode, false);\n\n if (targetElement) {\n // If the mountElement has children, the portal is still mounted\n const portalHasNoChildren = targetElement.childNodes.length === 0;\n\n if (portalHasNoChildren) {\n return targetElement.remove.bind(targetElement);\n }\n }\n\n return () => {\n // If the mountElement has children, ignore the remove call\n };\n }\n\n const targetElement = elementFactory.get(targetNode, true);\n const targetProperty = targetElement[property];\n\n if (typeof targetProperty === 'function') {\n return targetProperty.bind(targetElement);\n }\n\n return targetProperty;\n },\n\n set(_, property: keyof HTMLDivElement, value) {\n const targetElement = elementFactory.get(targetNode, true);\n\n if (targetElement) {\n Object.assign(targetElement, { [property]: value });\n return true;\n }\n\n return false;\n },\n });\n }, [elementFactory, targetNode, options.disabled]);\n\n React.useEffect(() => {\n return () => {\n elementProxy?.remove();\n };\n }, [elementProxy]);\n\n useInsertionEffect!(() => {\n if (!elementProxy) {\n return;\n }\n\n const classesToApply = className.split(' ').filter(Boolean);\n\n elementProxy.classList.add(...classesToApply);\n elementProxy.setAttribute('dir', dir);\n elementProxy.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = elementProxy;\n\n return () => {\n elementProxy.classList.remove(...classesToApply);\n elementProxy.removeAttribute('dir');\n };\n }, [className, dir, elementProxy, focusVisibleRef]);\n\n return elementProxy;\n};\n\n/**\n * Creates a new element on a \"document.body\" to mount portals.\n */\nexport const usePortalMountNode = (options: UsePortalMountNodeOptions): HTMLElement | null => {\n 'use no memo';\n\n const { targetDocument, dir } = useFluent();\n const mountNode = usePortalMountNodeContext();\n\n const focusVisibleRef = useFocusVisible<HTMLDivElement>() as React.MutableRefObject<HTMLElement | null>;\n const classes = usePortalMountNodeStylesStyles();\n const themeClassName = useThemeClassName();\n\n const factoryOptions: UseElementFactoryOptions = {\n dir,\n disabled: options.disabled,\n focusVisibleRef,\n\n className: mergeClasses(themeClassName, classes.root, options.className),\n targetNode: mountNode ?? targetDocument?.body,\n };\n\n if (useInsertionEffect) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useModernElementFactory(factoryOptions);\n }\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useLegacyElementFactory(factoryOptions);\n};\n"],"names":["React","useThemeClassName_unstable","useThemeClassName","useFluent_unstable","useFluent","usePortalMountNode","usePortalMountNodeContext","mergeClasses","useFocusVisible","usePortalMountNodeStylesStyles","useInsertionEffect","useLegacyElementFactory","options","className","dir","focusVisibleRef","targetNode","targetElement","useMemo","undefined","disabled","element","ownerDocument","createElement","appendChild","setAttribute","current","useEffect","remove","useModernElementFactory","elementFactory","useState","currentElement","get","targetRoot","forceCreation","dispose","elementProxy","Proxy","_","property","portalHasNoChildren","childNodes","length","bind","targetProperty","set","value","Object","assign","classesToApply","split","filter","Boolean","classList","add","removeAttribute","targetDocument","mountNode","classes","themeClassName","factoryOptions","root","body"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/components/Portal/usePortal.ts"],"sourcesContent":["import { setVirtualParent } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { toMountNodeProps } from '../../utils/toMountNodeProps';\nimport { usePortalMountNode } from './usePortalMountNode';\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 { element, className } = toMountNodeProps(props.mountNode);\n\n const virtualParentRootRef = React.useRef<HTMLSpanElement>(null);\n const fallbackElement = usePortalMountNode({ disabled: !!element, className });\n\n const mountNode = element ?? fallbackElement;\n const state: PortalState = {\n children: props.children,\n mountNode,\n virtualParentRootRef,\n };\n\n React.useEffect(() => {\n if (!mountNode) {\n return;\n }\n\n const virtualParent = virtualParentRootRef.current;\n\n // By default, we create a mount node for portal on `document.body` (see usePortalMountNode()) and have following structure:\n //\n // <body>\n // <!-- ⚛️ application root -->\n // <div id=\"root\">\n // <!-- ⬇️ portal node rendered in a tree to anchor (virtual parent node) -->\n // <span aria-hidden=\"true\"></span>\n // </div>\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n // </div>\n // </body>\n //\n // To make sure that `.elementContains()` works correctly, we link a virtual parent to a portal node (a virtual parent node becomes a parent of mount node):\n // virtual.contains(mountNode) === false\n // (while we need ⬇️⬇️⬇️)\n // elementsContains(virtualParent, mountNode) === true\n // elementsContains(mountNode, virtualParent) === false\n //\n // For more details, check docs for virtual parent utils.\n //\n // However, if a user provides a custom mount node (via `props`) the structure could be different:\n //\n // <body>\n // <!-- application root -->\n // <div id=\"root\">\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n //\n // <span aria-hidden=\"true\"></span>\n // </div>\n // </div>\n // </body>\n //\n // A mount node in this case contains portal's content and a virtual parent node. In this case nodes linking is redundant and the check below avoids it.\n //\n // Otherwise, there is a circular reference - both elements are parents of each other:\n // elementsContains(mountNode, virtualParent) === true\n // elementsContains(virtualParent, mountNode) === true\n const isVirtualParentInsideChild = mountNode.contains(virtualParent);\n\n if (virtualParent && !isVirtualParentInsideChild) {\n setVirtualParent(mountNode, virtualParent);\n\n return () => {\n setVirtualParent(mountNode, undefined);\n };\n }\n }, [
|
1
|
+
{"version":3,"sources":["../src/components/Portal/usePortal.ts"],"sourcesContent":["import { setVirtualParent } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport { toMountNodeProps } from '../../utils/toMountNodeProps';\nimport { usePortalMountNode } from './usePortalMountNode';\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 { element, className } = toMountNodeProps(props.mountNode);\n\n const virtualParentRootRef = React.useRef<HTMLSpanElement>(null);\n const fallbackElement = usePortalMountNode({ disabled: !!element, className });\n\n const mountNode = element ?? fallbackElement;\n const state: PortalState = {\n children: props.children,\n mountNode,\n virtualParentRootRef,\n };\n\n React.useEffect(() => {\n if (!mountNode) {\n return;\n }\n\n const virtualParent = virtualParentRootRef.current;\n\n // By default, we create a mount node for portal on `document.body` (see usePortalMountNode()) and have following structure:\n //\n // <body>\n // <!-- ⚛️ application root -->\n // <div id=\"root\">\n // <!-- ⬇️ portal node rendered in a tree to anchor (virtual parent node) -->\n // <span aria-hidden=\"true\"></span>\n // </div>\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n // </div>\n // </body>\n //\n // To make sure that `.elementContains()` works correctly, we link a virtual parent to a portal node (a virtual parent node becomes a parent of mount node):\n // virtual.contains(mountNode) === false\n // (while we need ⬇️⬇️⬇️)\n // elementsContains(virtualParent, mountNode) === true\n // elementsContains(mountNode, virtualParent) === false\n //\n // For more details, check docs for virtual parent utils.\n //\n // However, if a user provides a custom mount node (via `props`) the structure could be different:\n //\n // <body>\n // <!-- application root -->\n // <div id=\"root\">\n // <div id=\"portal-mount-node\">\n // <!-- 🧩portal content -->\n //\n // <span aria-hidden=\"true\"></span>\n // </div>\n // </div>\n // </body>\n //\n // A mount node in this case contains portal's content and a virtual parent node. In this case nodes linking is redundant and the check below avoids it.\n //\n // Otherwise, there is a circular reference - both elements are parents of each other:\n // elementsContains(mountNode, virtualParent) === true\n // elementsContains(virtualParent, mountNode) === true\n const isVirtualParentInsideChild = mountNode.contains(virtualParent);\n\n if (virtualParent && !isVirtualParentInsideChild) {\n setVirtualParent(mountNode, virtualParent);\n\n return () => {\n setVirtualParent(mountNode, undefined);\n };\n }\n }, [mountNode]);\n\n return state;\n};\n"],"names":["usePortal_unstable","props","element","className","toMountNodeProps","mountNode","virtualParentRootRef","React","useRef","fallbackElement","usePortalMountNode","disabled","state","children","useEffect","virtualParent","current","isVirtualParentInsideChild","contains","setVirtualParent","undefined"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";;;;+BAcaA;;;eAAAA;;;;gCAdoB;iEACV;kCAEU;oCACE;AAU5B,MAAMA,qBAAqB,CAACC;IACjC,MAAM,EAAEC,OAAO,EAAEC,SAAS,EAAE,GAAGC,IAAAA,kCAAAA,EAAiBH,MAAMI,SAAS;IAE/D,MAAMC,uBAAuBC,OAAMC,MAAM,CAAkB;IAC3D,MAAMC,kBAAkBC,IAAAA,sCAAAA,EAAmB;QAAEC,UAAU,CAAC,CAACT;QAASC;IAAU;IAE5E,MAAME,YAAYH,YAAAA,QAAAA,YAAAA,KAAAA,IAAAA,UAAWO;IAC7B,MAAMG,QAAqB;QACzBC,UAAUZ,MAAMY,QAAQ;QACxBR;QACAC;IACF;IAEAC,OAAMO,SAAS,CAAC;QACd,IAAI,CAACT,WAAW;YACd;QACF;QAEA,MAAMU,gBAAgBT,qBAAqBU,OAAO;QAElD,4HAA4H;QAC5H,EAAE;QACF,SAAS;QACT,iCAAiC;QACjC,oBAAoB;QACpB,iFAAiF;QACjF,uCAAuC;QACvC,WAAW;QACX,iCAAiC;QACjC,gCAAgC;QAChC,WAAW;QACX,UAAU;QACV,EAAE;QACF,4JAA4J;QAC5J,0CAA0C;QAC1C,2BAA2B;QAC3B,wDAAwD;QACxD,yDAAyD;QACzD,EAAE;QACF,yDAAyD;QACzD,EAAE;QACF,kGAAkG;QAClG,EAAE;QACF,SAAS;QACT,8BAA8B;QAC9B,oBAAoB;QACpB,mCAAmC;QACnC,kCAAkC;QAClC,EAAE;QACF,yCAAyC;QACzC,aAAa;QACb,WAAW;QACX,UAAU;QACV,EAAE;QACF,wJAAwJ;QACxJ,EAAE;QACF,sFAAsF;QACtF,wDAAwD;QACxD,wDAAwD;QACxD,MAAMC,6BAA6BZ,UAAUa,QAAQ,CAACH;QAEtD,IAAIA,iBAAiB,CAACE,4BAA4B;YAChDE,IAAAA,gCAAAA,EAAiBd,WAAWU;YAE5B,OAAO;gBACLI,IAAAA,gCAAAA,EAAiBd,WAAWe;YAC9B;QACF;IACF,GAAG;QAACf;KAAU;IAEd,OAAOO;AACT"}
|
@@ -13,9 +13,158 @@ const _react = /*#__PURE__*/ _interop_require_wildcard._(require("react"));
|
|
13
13
|
const _reactsharedcontexts = require("@fluentui/react-shared-contexts");
|
14
14
|
const _react1 = require("@griffel/react");
|
15
15
|
const _reacttabster = require("@fluentui/react-tabster");
|
16
|
-
const _usedisposable = require("use-disposable");
|
17
16
|
const _usePortalMountNodeStylesstyles = require("./usePortalMountNodeStyles.styles");
|
18
17
|
const useInsertionEffect = _react['useInsertion' + 'Effect'];
|
18
|
+
/**
|
19
|
+
* Legacy element factory for React 17 and below. It's not safe for concurrent rendering.
|
20
|
+
*
|
21
|
+
* Creates a new element on a "document.body" to mount portals.
|
22
|
+
*/ const useLegacyElementFactory = (options)=>{
|
23
|
+
const { className, dir, focusVisibleRef, targetNode } = options;
|
24
|
+
const targetElement = _react.useMemo(()=>{
|
25
|
+
if (targetNode === undefined || options.disabled) {
|
26
|
+
return null;
|
27
|
+
}
|
28
|
+
const element = targetNode.ownerDocument.createElement('div');
|
29
|
+
targetNode.appendChild(element);
|
30
|
+
return element;
|
31
|
+
}, [
|
32
|
+
targetNode,
|
33
|
+
options.disabled
|
34
|
+
]);
|
35
|
+
// Heads up!
|
36
|
+
// This useMemo() call is intentional for React 17 & below.
|
37
|
+
//
|
38
|
+
// We don't want to re-create the portal element when its attributes change. This also cannot not be done in an effect
|
39
|
+
// because, changing the value of CSS variables after an initial mount will trigger interesting CSS side effects like
|
40
|
+
// transitions.
|
41
|
+
_react.useMemo(()=>{
|
42
|
+
if (!targetElement) {
|
43
|
+
return;
|
44
|
+
}
|
45
|
+
targetElement.className = className;
|
46
|
+
targetElement.setAttribute('dir', dir);
|
47
|
+
targetElement.setAttribute('data-portal-node', 'true');
|
48
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
49
|
+
focusVisibleRef.current = targetElement;
|
50
|
+
}, [
|
51
|
+
className,
|
52
|
+
dir,
|
53
|
+
targetElement,
|
54
|
+
focusVisibleRef
|
55
|
+
]);
|
56
|
+
_react.useEffect(()=>{
|
57
|
+
return ()=>{
|
58
|
+
targetElement === null || targetElement === void 0 ? void 0 : targetElement.remove();
|
59
|
+
};
|
60
|
+
}, [
|
61
|
+
targetElement
|
62
|
+
]);
|
63
|
+
return targetElement;
|
64
|
+
};
|
65
|
+
/**
|
66
|
+
* This is a modern element factory for React 18 and above. It is safe for concurrent rendering.
|
67
|
+
*
|
68
|
+
* It abuses the fact that React will mount DOM once (unlike hooks), so by using a proxy we can intercept:
|
69
|
+
* - the `remove()` method (we call it in `useEffect()`) and remove the element only when the portal is unmounted
|
70
|
+
* - all other methods (and properties) will be called by React once a portal is mounted
|
71
|
+
*/ const useModernElementFactory = (options)=>{
|
72
|
+
const { className, dir, focusVisibleRef, targetNode } = options;
|
73
|
+
const [elementFactory] = _react.useState(()=>{
|
74
|
+
let currentElement = undefined;
|
75
|
+
function get(targetRoot, forceCreation) {
|
76
|
+
if (currentElement) {
|
77
|
+
return currentElement;
|
78
|
+
}
|
79
|
+
if (forceCreation) {
|
80
|
+
currentElement = targetRoot.ownerDocument.createElement('div');
|
81
|
+
targetRoot.appendChild(currentElement);
|
82
|
+
}
|
83
|
+
return currentElement;
|
84
|
+
}
|
85
|
+
function dispose() {
|
86
|
+
if (currentElement) {
|
87
|
+
currentElement.remove();
|
88
|
+
currentElement = undefined;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
return {
|
92
|
+
get,
|
93
|
+
dispose
|
94
|
+
};
|
95
|
+
});
|
96
|
+
const elementProxy = _react.useMemo(()=>{
|
97
|
+
if (targetNode === undefined || options.disabled) {
|
98
|
+
return null;
|
99
|
+
}
|
100
|
+
return new Proxy({}, {
|
101
|
+
get (_, property) {
|
102
|
+
// Heads up!
|
103
|
+
// We intercept the `remove()` method to remove the mount node only when portal has been unmounted already.
|
104
|
+
if (property === 'remove') {
|
105
|
+
const targetElement = elementFactory.get(targetNode, false);
|
106
|
+
if (targetElement) {
|
107
|
+
// If the mountElement has children, the portal is still mounted
|
108
|
+
const portalHasNoChildren = targetElement.childNodes.length === 0;
|
109
|
+
if (portalHasNoChildren) {
|
110
|
+
return targetElement.remove.bind(targetElement);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
return ()=>{
|
114
|
+
// If the mountElement has children, ignore the remove call
|
115
|
+
};
|
116
|
+
}
|
117
|
+
const targetElement = elementFactory.get(targetNode, true);
|
118
|
+
const targetProperty = targetElement[property];
|
119
|
+
if (typeof targetProperty === 'function') {
|
120
|
+
return targetProperty.bind(targetElement);
|
121
|
+
}
|
122
|
+
return targetProperty;
|
123
|
+
},
|
124
|
+
set (_, property, value) {
|
125
|
+
const targetElement = elementFactory.get(targetNode, true);
|
126
|
+
if (targetElement) {
|
127
|
+
Object.assign(targetElement, {
|
128
|
+
[property]: value
|
129
|
+
});
|
130
|
+
return true;
|
131
|
+
}
|
132
|
+
return false;
|
133
|
+
}
|
134
|
+
});
|
135
|
+
}, [
|
136
|
+
elementFactory,
|
137
|
+
targetNode,
|
138
|
+
options.disabled
|
139
|
+
]);
|
140
|
+
_react.useEffect(()=>{
|
141
|
+
return ()=>{
|
142
|
+
elementProxy === null || elementProxy === void 0 ? void 0 : elementProxy.remove();
|
143
|
+
};
|
144
|
+
}, [
|
145
|
+
elementProxy
|
146
|
+
]);
|
147
|
+
useInsertionEffect(()=>{
|
148
|
+
if (!elementProxy) {
|
149
|
+
return;
|
150
|
+
}
|
151
|
+
const classesToApply = className.split(' ').filter(Boolean);
|
152
|
+
elementProxy.classList.add(...classesToApply);
|
153
|
+
elementProxy.setAttribute('dir', dir);
|
154
|
+
elementProxy.setAttribute('data-portal-node', 'true');
|
155
|
+
focusVisibleRef.current = elementProxy;
|
156
|
+
return ()=>{
|
157
|
+
elementProxy.classList.remove(...classesToApply);
|
158
|
+
elementProxy.removeAttribute('dir');
|
159
|
+
};
|
160
|
+
}, [
|
161
|
+
className,
|
162
|
+
dir,
|
163
|
+
elementProxy,
|
164
|
+
focusVisibleRef
|
165
|
+
]);
|
166
|
+
return elementProxy;
|
167
|
+
};
|
19
168
|
const usePortalMountNode = (options)=>{
|
20
169
|
'use no memo';
|
21
170
|
const { targetDocument, dir } = (0, _reactsharedcontexts.useFluent_unstable)();
|
@@ -23,66 +172,17 @@ const usePortalMountNode = (options)=>{
|
|
23
172
|
const focusVisibleRef = (0, _reacttabster.useFocusVisible)();
|
24
173
|
const classes = (0, _usePortalMountNodeStylesstyles.usePortalMountNodeStylesStyles)();
|
25
174
|
const themeClassName = (0, _reactsharedcontexts.useThemeClassName_unstable)();
|
26
|
-
const
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
];
|
34
|
-
}
|
35
|
-
const newElement = targetNode.ownerDocument.createElement('div');
|
36
|
-
targetNode.appendChild(newElement);
|
37
|
-
return [
|
38
|
-
newElement,
|
39
|
-
()=>newElement.remove()
|
40
|
-
];
|
41
|
-
}, [
|
42
|
-
targetNode
|
43
|
-
]);
|
175
|
+
const factoryOptions = {
|
176
|
+
dir,
|
177
|
+
disabled: options.disabled,
|
178
|
+
focusVisibleRef,
|
179
|
+
className: (0, _react1.mergeClasses)(themeClassName, classes.root, options.className),
|
180
|
+
targetNode: mountNode !== null && mountNode !== void 0 ? mountNode : targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body
|
181
|
+
};
|
44
182
|
if (useInsertionEffect) {
|
45
183
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
46
|
-
|
47
|
-
if (!element) {
|
48
|
-
return;
|
49
|
-
}
|
50
|
-
const classesToApply = className.split(' ').filter(Boolean);
|
51
|
-
element.classList.add(...classesToApply);
|
52
|
-
element.setAttribute('dir', dir);
|
53
|
-
element.setAttribute('data-portal-node', 'true');
|
54
|
-
focusVisibleRef.current = element;
|
55
|
-
return ()=>{
|
56
|
-
element.classList.remove(...classesToApply);
|
57
|
-
element.removeAttribute('dir');
|
58
|
-
};
|
59
|
-
}, [
|
60
|
-
className,
|
61
|
-
dir,
|
62
|
-
element,
|
63
|
-
focusVisibleRef
|
64
|
-
]);
|
65
|
-
} else {
|
66
|
-
// This useMemo call is intentional for React 17
|
67
|
-
// We don't want to re-create the portal element when its attributes change.
|
68
|
-
// This also should not be done in an effect because, changing the value of css variables
|
69
|
-
// after initial mount can trigger interesting CSS side effects like transitions.
|
70
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
71
|
-
_react.useMemo(()=>{
|
72
|
-
if (!element) {
|
73
|
-
return;
|
74
|
-
}
|
75
|
-
// Force replace all classes
|
76
|
-
element.className = className;
|
77
|
-
element.setAttribute('dir', dir);
|
78
|
-
element.setAttribute('data-portal-node', 'true');
|
79
|
-
focusVisibleRef.current = element;
|
80
|
-
}, [
|
81
|
-
className,
|
82
|
-
dir,
|
83
|
-
element,
|
84
|
-
focusVisibleRef
|
85
|
-
]);
|
184
|
+
return useModernElementFactory(factoryOptions);
|
86
185
|
}
|
87
|
-
|
186
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
187
|
+
return useLegacyElementFactory(factoryOptions);
|
88
188
|
};
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/components/Portal/usePortalMountNode.ts"],"sourcesContent":["import * as React from 'react';\nimport {\n useThemeClassName_unstable as useThemeClassName,\n useFluent_unstable as useFluent,\n usePortalMountNode as usePortalMountNodeContext,\n} from '@fluentui/react-shared-contexts';\nimport { mergeClasses } from '@griffel/react';\nimport { useFocusVisible } from '@fluentui/react-tabster';\nimport { useDisposable } from 'use-disposable';\n\nimport { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';\n\nconst useInsertionEffect = (React as never)['useInsertion' + 'Effect'] as typeof React.useLayoutEffect | undefined;\n\nexport type UsePortalMountNodeOptions = {\n /**\n * Since hooks cannot be called conditionally use this flag to disable creating the node\n */\n disabled?: boolean;\n\n className?: string;\n};\n\n/**\n * Creates a new element on a \"document.body\" to mount portals.\n */\nexport const usePortalMountNode = (options: UsePortalMountNodeOptions): HTMLElement | null => {\n 'use no memo';\n\n const { targetDocument, dir } = useFluent();\n const mountNode = usePortalMountNodeContext();\n\n const focusVisibleRef = useFocusVisible<HTMLDivElement>() as React.MutableRefObject<HTMLElement | null>;\n const classes = usePortalMountNodeStylesStyles();\n const themeClassName = useThemeClassName();\n\n const className = mergeClasses(themeClassName, classes.root, options.className);\n const targetNode: HTMLElement | ShadowRoot | undefined = mountNode ?? targetDocument?.body;\n\n const element = useDisposable(() => {\n if (targetNode === undefined || options.disabled) {\n return [null, () => null];\n }\n\n const newElement = targetNode.ownerDocument.createElement('div');\n targetNode.appendChild(newElement);\n return [newElement, () => newElement.remove()];\n }, [targetNode]);\n\n if (useInsertionEffect) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useInsertionEffect(() => {\n if (!element) {\n return;\n }\n\n const classesToApply = className.split(' ').filter(Boolean);\n\n element.classList.add(...classesToApply);\n element.setAttribute('dir', dir);\n element.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = element;\n\n return () => {\n element.classList.remove(...classesToApply);\n element.removeAttribute('dir');\n };\n }, [className, dir, element, focusVisibleRef]);\n } else {\n // This useMemo call is intentional for React 17\n // We don't want to re-create the portal element when its attributes change.\n // This also should not be done in an effect because, changing the value of css variables\n // after initial mount can trigger interesting CSS side effects like transitions.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n React.useMemo(() => {\n if (!element) {\n return;\n }\n\n // Force replace all classes\n element.className = className;\n element.setAttribute('dir', dir);\n element.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = element;\n }, [className, dir, element, focusVisibleRef]);\n }\n\n return element;\n};\n"],"names":["usePortalMountNode","useInsertionEffect","React","options","targetDocument","dir","useFluent","mountNode","usePortalMountNodeContext","focusVisibleRef","useFocusVisible","classes","usePortalMountNodeStylesStyles","themeClassName","useThemeClassName","className","mergeClasses","root","targetNode","body","element","useDisposable","undefined","disabled","newElement","ownerDocument","createElement","appendChild","remove","classesToApply","split","filter","Boolean","classList","add","setAttribute","current","removeAttribute","useMemo"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";;;;+BA0BaA;;;eAAAA;;;;iEA1BU;qCAKhB;wBACsB;8BACG;+BACF;gDAEiB;AAE/C,MAAMC,qBAAqBC,MAAgB,CAAC,iBAAiB,SAAS;AAc/D,MAAMF,qBAAqB,CAACG;IACjC;IAEA,MAAM,EAAEC,cAAc,EAAEC,GAAG,EAAE,GAAGC,IAAAA,uCAAAA;IAChC,MAAMC,YAAYC,IAAAA,uCAAAA;IAElB,MAAMC,kBAAkBC,IAAAA,6BAAAA;IACxB,MAAMC,UAAUC,IAAAA,8DAAAA;IAChB,MAAMC,iBAAiBC,IAAAA,+CAAAA;IAEvB,MAAMC,YAAYC,IAAAA,oBAAAA,EAAaH,gBAAgBF,QAAQM,IAAI,EAAEd,QAAQY,SAAS;IAC9E,MAAMG,aAAmDX,cAAAA,QAAAA,cAAAA,KAAAA,IAAAA,YAAaH,mBAAAA,QAAAA,mBAAAA,KAAAA,IAAAA,KAAAA,IAAAA,eAAgBe,IAAI;IAE1F,MAAMC,UAAUC,IAAAA,4BAAAA,EAAc;QAC5B,IAAIH,eAAeI,aAAanB,QAAQoB,QAAQ,EAAE;YAChD,OAAO;gBAAC;gBAAM,IAAM;aAAK;QAC3B;QAEA,MAAMC,aAAaN,WAAWO,aAAa,CAACC,aAAa,CAAC;QAC1DR,WAAWS,WAAW,CAACH;QACvB,OAAO;YAACA;YAAY,IAAMA,WAAWI,MAAM;SAAG;IAChD,GAAG;QAACV;KAAW;IAEf,IAAIjB,oBAAoB;QACtB,sDAAsD;QACtDA,mBAAmB;YACjB,IAAI,CAACmB,SAAS;gBACZ;YACF;YAEA,MAAMS,iBAAiBd,UAAUe,KAAK,CAAC,KAAKC,MAAM,CAACC;YAEnDZ,QAAQa,SAAS,CAACC,GAAG,IAAIL;YACzBT,QAAQe,YAAY,CAAC,OAAO9B;YAC5Be,QAAQe,YAAY,CAAC,oBAAoB;YAEzC1B,gBAAgB2B,OAAO,GAAGhB;YAE1B,OAAO;gBACLA,QAAQa,SAAS,CAACL,MAAM,IAAIC;gBAC5BT,QAAQiB,eAAe,CAAC;YAC1B;QACF,GAAG;YAACtB;YAAWV;YAAKe;YAASX;SAAgB;IAC/C,OAAO;QACL,gDAAgD;QAChD,4EAA4E;QAC5E,yFAAyF;QACzF,iFAAiF;QACjF,sDAAsD;QACtDP,OAAMoC,OAAO,CAAC;YACZ,IAAI,CAAClB,SAAS;gBACZ;YACF;YAEA,4BAA4B;YAC5BA,QAAQL,SAAS,GAAGA;YACpBK,QAAQe,YAAY,CAAC,OAAO9B;YAC5Be,QAAQe,YAAY,CAAC,oBAAoB;YAEzC1B,gBAAgB2B,OAAO,GAAGhB;QAC5B,GAAG;YAACL;YAAWV;YAAKe;YAASX;SAAgB;IAC/C;IAEA,OAAOW;AACT"}
|
1
|
+
{"version":3,"sources":["../src/components/Portal/usePortalMountNode.ts"],"sourcesContent":["import * as React from 'react';\nimport {\n useThemeClassName_unstable as useThemeClassName,\n useFluent_unstable as useFluent,\n usePortalMountNode as usePortalMountNodeContext,\n} from '@fluentui/react-shared-contexts';\nimport { mergeClasses } from '@griffel/react';\nimport { useFocusVisible } from '@fluentui/react-tabster';\n\nimport { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';\n\nconst useInsertionEffect = (React as never)['useInsertion' + 'Effect'] as typeof React.useLayoutEffect | undefined;\n\nexport type UsePortalMountNodeOptions = {\n /**\n * Since hooks cannot be called conditionally use this flag to disable creating the node\n */\n disabled?: boolean;\n\n className?: string;\n};\n\ntype UseElementFactoryOptions = {\n className: string;\n dir: string;\n disabled: boolean | undefined;\n focusVisibleRef: React.MutableRefObject<HTMLElement | null>;\n targetNode: HTMLElement | ShadowRoot | undefined;\n};\ntype UseElementFactory = (options: UseElementFactoryOptions) => HTMLDivElement | null;\n\n/**\n * Legacy element factory for React 17 and below. It's not safe for concurrent rendering.\n *\n * Creates a new element on a \"document.body\" to mount portals.\n */\nconst useLegacyElementFactory: UseElementFactory = options => {\n const { className, dir, focusVisibleRef, targetNode } = options;\n\n const targetElement = React.useMemo(() => {\n if (targetNode === undefined || options.disabled) {\n return null;\n }\n\n const element = targetNode.ownerDocument.createElement('div');\n targetNode.appendChild(element);\n\n return element;\n }, [targetNode, options.disabled]);\n\n // Heads up!\n // This useMemo() call is intentional for React 17 & below.\n //\n // We don't want to re-create the portal element when its attributes change. This also cannot not be done in an effect\n // because, changing the value of CSS variables after an initial mount will trigger interesting CSS side effects like\n // transitions.\n React.useMemo(() => {\n if (!targetElement) {\n return;\n }\n\n targetElement.className = className;\n targetElement.setAttribute('dir', dir);\n targetElement.setAttribute('data-portal-node', 'true');\n\n // eslint-disable-next-line react-compiler/react-compiler\n focusVisibleRef.current = targetElement;\n }, [className, dir, targetElement, focusVisibleRef]);\n\n React.useEffect(() => {\n return () => {\n targetElement?.remove();\n };\n }, [targetElement]);\n\n return targetElement;\n};\n\n/**\n * This is a modern element factory for React 18 and above. It is safe for concurrent rendering.\n *\n * It abuses the fact that React will mount DOM once (unlike hooks), so by using a proxy we can intercept:\n * - the `remove()` method (we call it in `useEffect()`) and remove the element only when the portal is unmounted\n * - all other methods (and properties) will be called by React once a portal is mounted\n */\nconst useModernElementFactory: UseElementFactory = options => {\n const { className, dir, focusVisibleRef, targetNode } = options;\n\n const [elementFactory] = React.useState(() => {\n let currentElement: HTMLDivElement | undefined = undefined;\n\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: false): HTMLDivElement | undefined;\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: true): HTMLDivElement;\n function get(targetRoot: HTMLElement | ShadowRoot, forceCreation: boolean): HTMLDivElement | undefined {\n if (currentElement) {\n return currentElement;\n }\n\n if (forceCreation) {\n currentElement = targetRoot.ownerDocument.createElement('div');\n targetRoot.appendChild(currentElement);\n }\n\n return currentElement;\n }\n\n function dispose() {\n if (currentElement) {\n currentElement.remove();\n currentElement = undefined;\n }\n }\n\n return {\n get,\n dispose,\n };\n });\n\n const elementProxy = React.useMemo(() => {\n if (targetNode === undefined || options.disabled) {\n return null;\n }\n\n return new Proxy({} as HTMLDivElement, {\n get(_, property: keyof HTMLDivElement) {\n // Heads up!\n // We intercept the `remove()` method to remove the mount node only when portal has been unmounted already.\n if (property === 'remove') {\n const targetElement = elementFactory.get(targetNode, false);\n\n if (targetElement) {\n // If the mountElement has children, the portal is still mounted\n const portalHasNoChildren = targetElement.childNodes.length === 0;\n\n if (portalHasNoChildren) {\n return targetElement.remove.bind(targetElement);\n }\n }\n\n return () => {\n // If the mountElement has children, ignore the remove call\n };\n }\n\n const targetElement = elementFactory.get(targetNode, true);\n const targetProperty = targetElement[property];\n\n if (typeof targetProperty === 'function') {\n return targetProperty.bind(targetElement);\n }\n\n return targetProperty;\n },\n\n set(_, property: keyof HTMLDivElement, value) {\n const targetElement = elementFactory.get(targetNode, true);\n\n if (targetElement) {\n Object.assign(targetElement, { [property]: value });\n return true;\n }\n\n return false;\n },\n });\n }, [elementFactory, targetNode, options.disabled]);\n\n React.useEffect(() => {\n return () => {\n elementProxy?.remove();\n };\n }, [elementProxy]);\n\n useInsertionEffect!(() => {\n if (!elementProxy) {\n return;\n }\n\n const classesToApply = className.split(' ').filter(Boolean);\n\n elementProxy.classList.add(...classesToApply);\n elementProxy.setAttribute('dir', dir);\n elementProxy.setAttribute('data-portal-node', 'true');\n\n focusVisibleRef.current = elementProxy;\n\n return () => {\n elementProxy.classList.remove(...classesToApply);\n elementProxy.removeAttribute('dir');\n };\n }, [className, dir, elementProxy, focusVisibleRef]);\n\n return elementProxy;\n};\n\n/**\n * Creates a new element on a \"document.body\" to mount portals.\n */\nexport const usePortalMountNode = (options: UsePortalMountNodeOptions): HTMLElement | null => {\n 'use no memo';\n\n const { targetDocument, dir } = useFluent();\n const mountNode = usePortalMountNodeContext();\n\n const focusVisibleRef = useFocusVisible<HTMLDivElement>() as React.MutableRefObject<HTMLElement | null>;\n const classes = usePortalMountNodeStylesStyles();\n const themeClassName = useThemeClassName();\n\n const factoryOptions: UseElementFactoryOptions = {\n dir,\n disabled: options.disabled,\n focusVisibleRef,\n\n className: mergeClasses(themeClassName, classes.root, options.className),\n targetNode: mountNode ?? targetDocument?.body,\n };\n\n if (useInsertionEffect) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useModernElementFactory(factoryOptions);\n }\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useLegacyElementFactory(factoryOptions);\n};\n"],"names":["usePortalMountNode","useInsertionEffect","React","useLegacyElementFactory","options","className","dir","focusVisibleRef","targetNode","targetElement","useMemo","undefined","disabled","element","ownerDocument","createElement","appendChild","setAttribute","current","useEffect","remove","useModernElementFactory","elementFactory","useState","currentElement","get","targetRoot","forceCreation","dispose","elementProxy","Proxy","_","property","portalHasNoChildren","childNodes","length","bind","targetProperty","set","value","Object","assign","classesToApply","split","filter","Boolean","classList","add","removeAttribute","targetDocument","useFluent","mountNode","usePortalMountNodeContext","useFocusVisible","classes","usePortalMountNodeStylesStyles","themeClassName","useThemeClassName","factoryOptions","mergeClasses","root","body"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fluentui/react-portal",
|
3
|
-
"version": "9.5.
|
3
|
+
"version": "9.5.7",
|
4
4
|
"description": "A utility component that creates portals compatible with Fluent UI",
|
5
5
|
"main": "lib-commonjs/index.js",
|
6
6
|
"module": "lib/index.js",
|
@@ -19,11 +19,10 @@
|
|
19
19
|
},
|
20
20
|
"dependencies": {
|
21
21
|
"@fluentui/react-shared-contexts": "^9.23.1",
|
22
|
-
"@fluentui/react-tabster": "^9.24.
|
22
|
+
"@fluentui/react-tabster": "^9.24.7",
|
23
23
|
"@fluentui/react-utilities": "^9.19.0",
|
24
24
|
"@griffel/react": "^1.5.22",
|
25
|
-
"@swc/helpers": "^0.5.1"
|
26
|
-
"use-disposable": "^1.0.1"
|
25
|
+
"@swc/helpers": "^0.5.1"
|
27
26
|
},
|
28
27
|
"peerDependencies": {
|
29
28
|
"@types/react": ">=16.14.0 <19.0.0",
|