@fluentui/react-portal 9.6.3 → 9.7.0

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 CHANGED
@@ -1,12 +1,31 @@
1
1
  # Change Log - @fluentui/react-portal
2
2
 
3
- This log was last generated on Fri, 27 Jun 2025 13:36:33 GMT and should not be manually modified.
3
+ This log was last generated on Thu, 17 Jul 2025 13:45:43 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## [9.7.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.7.0)
8
+
9
+ Thu, 17 Jul 2025 13:45:43 GMT
10
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.6.4..@fluentui/react-portal_v9.7.0)
11
+
12
+ ### Minor changes
13
+
14
+ - feat: enable griffel raw styles ([PR #34853](https://github.com/microsoft/fluentui/pull/34853) by martinhochel@microsoft.com)
15
+ - Bump @fluentui/react-tabster to v9.26.0 ([PR #34862](https://github.com/microsoft/fluentui/pull/34862) by beachball)
16
+
17
+ ## [9.6.4](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.6.4)
18
+
19
+ Fri, 04 Jul 2025 10:02:46 GMT
20
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.6.3..@fluentui/react-portal_v9.6.4)
21
+
22
+ ### Patches
23
+
24
+ - Revert "chore(react-portal): support for React 19" ([PR #34737](https://github.com/microsoft/fluentui/pull/34737) by olfedias@microsoft.com)
25
+
7
26
  ## [9.6.3](https://github.com/microsoft/fluentui/tree/@fluentui/react-portal_v9.6.3)
8
27
 
9
- Fri, 27 Jun 2025 13:36:33 GMT
28
+ Fri, 27 Jun 2025 13:39:41 GMT
10
29
  [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-portal_v9.6.2..@fluentui/react-portal_v9.6.3)
11
30
 
12
31
  ### Patches
@@ -73,6 +73,7 @@ import { usePortalMountNode } from './usePortalMountNode';
73
73
  };
74
74
  }
75
75
  }, [
76
+ virtualParentRootRef,
76
77
  mountNode
77
78
  ]);
78
79
  return state;
@@ -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 }, [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"}
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 }, [virtualParentRootRef, 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;QAACX;QAAsBD;KAAU;IAEpC,OAAOK;AACT,EAAE"}
@@ -2,158 +2,9 @@ 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';
5
6
  import { usePortalMountNodeStylesStyles } from './usePortalMountNodeStyles.styles';
6
7
  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
- };
157
8
  /**
158
9
  * Creates a new element on a "document.body" to mount portals.
159
10
  */ export const usePortalMountNode = (options)=>{
@@ -163,17 +14,66 @@ const useInsertionEffect = React['useInsertion' + 'Effect'];
163
14
  const focusVisibleRef = useFocusVisible();
164
15
  const classes = usePortalMountNodeStylesStyles();
165
16
  const themeClassName = useThemeClassName();
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
- };
17
+ const className = mergeClasses(themeClassName, classes.root, options.className);
18
+ const targetNode = mountNode !== null && mountNode !== void 0 ? mountNode : targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body;
19
+ const element = useDisposable(()=>{
20
+ if (targetNode === undefined || options.disabled) {
21
+ return [
22
+ null,
23
+ ()=>null
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
+ ]);
173
35
  if (useInsertionEffect) {
174
36
  // eslint-disable-next-line react-hooks/rules-of-hooks
175
- return useModernElementFactory(factoryOptions);
37
+ useInsertionEffect(()=>{
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
+ ]);
176
77
  }
177
- // eslint-disable-next-line react-hooks/rules-of-hooks
178
- return useLegacyElementFactory(factoryOptions);
78
+ return element;
179
79
  };
@@ -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';\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
+ {"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"}
@@ -0,0 +1,14 @@
1
+ import { makeStyles } from '@griffel/react';
2
+ export const usePortalMountNodeStylesStyles = makeStyles({
3
+ root: {
4
+ // Creates new stacking context to prevent z-index issues
5
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context
6
+ //
7
+ // Also keeps a portal on top of a page to prevent scrollbars from appearing
8
+ position: 'absolute',
9
+ top: 0,
10
+ left: 0,
11
+ right: 0,
12
+ zIndex: 1000000
13
+ }
14
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Portal/usePortalMountNodeStyles.styles.ts"],"sourcesContent":["import { makeStyles } from '@griffel/react';\n\nexport const usePortalMountNodeStylesStyles = makeStyles({\n root: {\n // Creates new stacking context to prevent z-index issues\n // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context\n //\n // Also keeps a portal on top of a page to prevent scrollbars from appearing\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n\n zIndex: 1000000,\n },\n});\n"],"names":["makeStyles","usePortalMountNodeStylesStyles","root","position","top","left","right","zIndex"],"rangeMappings":";;;;;;;;;;;;;","mappings":"AAAA,SAASA,UAAU,QAAQ,iBAAiB;AAE5C,OAAO,MAAMC,iCAAiCD,WAAW;IACvDE,MAAM;QACJ,yDAAyD;QACzD,gHAAgH;QAChH,EAAE;QACF,4EAA4E;QAC5EC,UAAU;QACVC,KAAK;QACLC,MAAM;QACNC,OAAO;QAEPC,QAAQ;IACV;AACF,GAAG"}
@@ -78,6 +78,7 @@ const usePortal_unstable = (props)=>{
78
78
  };
79
79
  }
80
80
  }, [
81
+ virtualParentRootRef,
81
82
  mountNode
82
83
  ]);
83
84
  return state;
@@ -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 }, [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"}
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 }, [virtualParentRootRef, 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;QAACd;QAAsBD;KAAU;IAEpC,OAAOO;AACT"}
@@ -13,158 +13,9 @@ 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");
16
17
  const _usePortalMountNodeStylesstyles = require("./usePortalMountNodeStyles.styles");
17
18
  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
- };
168
19
  const usePortalMountNode = (options)=>{
169
20
  'use no memo';
170
21
  const { targetDocument, dir } = (0, _reactsharedcontexts.useFluent_unstable)();
@@ -172,17 +23,66 @@ const usePortalMountNode = (options)=>{
172
23
  const focusVisibleRef = (0, _reacttabster.useFocusVisible)();
173
24
  const classes = (0, _usePortalMountNodeStylesstyles.usePortalMountNodeStylesStyles)();
174
25
  const themeClassName = (0, _reactsharedcontexts.useThemeClassName_unstable)();
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
- };
26
+ const className = (0, _react1.mergeClasses)(themeClassName, classes.root, options.className);
27
+ const targetNode = mountNode !== null && mountNode !== void 0 ? mountNode : targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.body;
28
+ const element = (0, _usedisposable.useDisposable)(()=>{
29
+ if (targetNode === undefined || options.disabled) {
30
+ return [
31
+ null,
32
+ ()=>null
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
+ ]);
182
44
  if (useInsertionEffect) {
183
45
  // eslint-disable-next-line react-hooks/rules-of-hooks
184
- return useModernElementFactory(factoryOptions);
46
+ useInsertionEffect(()=>{
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
+ ]);
185
86
  }
186
- // eslint-disable-next-line react-hooks/rules-of-hooks
187
- return useLegacyElementFactory(factoryOptions);
87
+ return element;
188
88
  };
@@ -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';\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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"}
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"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ Object.defineProperty(exports, "usePortalMountNodeStylesStyles", {
6
+ enumerable: true,
7
+ get: function() {
8
+ return usePortalMountNodeStylesStyles;
9
+ }
10
+ });
11
+ const _react = require("@griffel/react");
12
+ const usePortalMountNodeStylesStyles = (0, _react.makeStyles)({
13
+ root: {
14
+ // Creates new stacking context to prevent z-index issues
15
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context
16
+ //
17
+ // Also keeps a portal on top of a page to prevent scrollbars from appearing
18
+ position: 'absolute',
19
+ top: 0,
20
+ left: 0,
21
+ right: 0,
22
+ zIndex: 1000000
23
+ }
24
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Portal/usePortalMountNodeStyles.styles.ts"],"sourcesContent":["import { makeStyles } from '@griffel/react';\n\nexport const usePortalMountNodeStylesStyles = makeStyles({\n root: {\n // Creates new stacking context to prevent z-index issues\n // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context\n //\n // Also keeps a portal on top of a page to prevent scrollbars from appearing\n position: 'absolute',\n top: 0,\n left: 0,\n right: 0,\n\n zIndex: 1000000,\n },\n});\n"],"names":["usePortalMountNodeStylesStyles","makeStyles","root","position","top","left","right","zIndex"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;","mappings":";;;;+BAEaA;;;eAAAA;;;uBAFc;AAEpB,MAAMA,iCAAiCC,IAAAA,iBAAAA,EAAW;IACvDC,MAAM;QACJ,yDAAyD;QACzD,gHAAgH;QAChH,EAAE;QACF,4EAA4E;QAC5EC,UAAU;QACVC,KAAK;QACLC,MAAM;QACNC,OAAO;QAEPC,QAAQ;IACV;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluentui/react-portal",
3
- "version": "9.6.3",
3
+ "version": "9.7.0",
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,10 +19,11 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@fluentui/react-shared-contexts": "^9.24.0",
22
- "@fluentui/react-tabster": "^9.25.3",
22
+ "@fluentui/react-tabster": "^9.26.0",
23
23
  "@fluentui/react-utilities": "^9.22.0",
24
24
  "@griffel/react": "^1.5.22",
25
- "@swc/helpers": "^0.5.1"
25
+ "@swc/helpers": "^0.5.1",
26
+ "use-disposable": "^1.0.1"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "@types/react": ">=16.14.0 <19.0.0",