@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 CHANGED
@@ -1,12 +1,22 @@
1
1
  # Change Log - @fluentui/react-portal
2
2
 
3
- This log was last generated on Thu, 24 Apr 2025 09:56:28 GMT and should not be manually modified.
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:56:28 GMT
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
@@ -73,7 +73,6 @@ import { usePortalMountNode } from './usePortalMountNode';
73
73
  };
74
74
  }
75
75
  }, [
76
- virtualParentRootRef,
77
76
  mountNode
78
77
  ]);
79
78
  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 }, [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"}
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 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
- ]);
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
- 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
- ]);
175
+ return useModernElementFactory(factoryOptions);
77
176
  }
78
- return element;
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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"}
@@ -78,7 +78,6 @@ const usePortal_unstable = (props)=>{
78
78
  };
79
79
  }
80
80
  }, [
81
- virtualParentRootRef,
82
81
  mountNode
83
82
  ]);
84
83
  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 }, [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"}
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 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
- ]);
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
- 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
- ]);
184
+ return useModernElementFactory(factoryOptions);
86
185
  }
87
- return element;
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.6",
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.6",
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",