react-magic-portal 1.1.5 → 1.1.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,3 +1,17 @@
1
+ ## [1.1.7](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.6...v1.1.7) (2025-09-28)
2
+
3
+
4
+ ### Performance Improvements
5
+
6
+ * add peerDependencies in the root directory ([dd60ab0](https://github.com/molvqingtai/react-magic-portal/commit/dd60ab0f22ed32bfc024fb956d89b00d68ef4d81))
7
+
8
+ ## [1.1.6](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.5...v1.1.6) (2025-09-27)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * support Cleanup functions for refs ([a31b2cb](https://github.com/molvqingtai/react-magic-portal/commit/a31b2cb6323b1fc2d38836ec22ef99c9456901e2))
14
+
1
15
  ## [1.1.5](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.4...v1.1.5) (2025-09-26)
2
16
 
3
17
 
@@ -0,0 +1,6 @@
1
+ # Run
2
+
3
+ ```bash
4
+ pnpm install
5
+ pnpm dev
6
+ ```
@@ -21,15 +21,15 @@
21
21
  "@eslint/js": "^9.36.0",
22
22
  "@testing-library/react": "^16.3.0",
23
23
  "@testing-library/user-event": "^14.6.1",
24
- "@types/react": "^19.1.13",
24
+ "@types/react": "^19.1.14",
25
25
  "@types/react-dom": "^19.1.9",
26
- "@vitejs/plugin-react": "^5.0.3",
26
+ "@vitejs/plugin-react": "^5.0.4",
27
27
  "@vitest/coverage-v8": "^3.2.4",
28
28
  "@vitest/ui": "^3.2.4",
29
29
  "eslint": "^9.36.0",
30
30
  "eslint-plugin-prettier": "^5.5.4",
31
31
  "eslint-plugin-react-hooks": "^5.2.0",
32
- "eslint-plugin-react-refresh": "^0.4.21",
32
+ "eslint-plugin-react-refresh": "^0.4.22",
33
33
  "globals": "^16.4.0",
34
34
  "happy-dom": "^18.0.1",
35
35
  "react": "^19.1.1",
@@ -768,4 +768,27 @@ describe('MagicPortal', () => {
768
768
  expect(screen.queryByTestId('portal-content')).toBeNull()
769
769
  })
770
770
  })
771
+ it('should call ref cleanup function on unmount', () => {
772
+ const cleanupFn = vi.fn()
773
+ // React 19: Cleanup functions for refs
774
+ const customRef = () => () => cleanupFn()
775
+
776
+ const anchor = document.createElement('div')
777
+ anchor.id = 'cleanup-ref-anchor'
778
+ document.body.appendChild(anchor)
779
+
780
+ const { unmount } = render(
781
+ <MagicPortal anchor="#cleanup-ref-anchor">
782
+ <div ref={customRef} data-testid="portal-content">
783
+ Portal Content
784
+ </div>
785
+ </MagicPortal>
786
+ )
787
+
788
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
789
+
790
+ unmount()
791
+
792
+ expect(cleanupFn).toHaveBeenCalledTimes(1)
793
+ })
771
794
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-magic-portal",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "React Portal with dynamic mounting support",
5
5
  "main": "packages/component/dist/index.js",
6
6
  "type": "module",
@@ -22,23 +22,12 @@
22
22
  "prepare": "husky"
23
23
  },
24
24
  "keywords": [
25
- "react",
26
- "portal",
27
- "dynamic",
25
+ "react-portal",
26
+ "anchor",
28
27
  "browser-extension",
29
28
  "content-script",
30
29
  "mutation-observer",
31
- "inject",
32
- "mount",
33
- "anchor",
34
- "reactdom",
35
- "createportal",
36
- "web-extension",
37
- "chrome-extension",
38
- "firefox-extension",
39
- "dynamic-content",
40
- "dom-manipulation",
41
- "typescript"
30
+ "dynamic-content"
42
31
  ],
43
32
  "author": "molvqingtai",
44
33
  "license": "MIT",
@@ -51,14 +40,18 @@
51
40
  },
52
41
  "homepage": "https://github.com/molvqingtai/react-magic-portal#readme",
53
42
  "dependencies": {
54
- "@commitlint/cli": "^19.8.1",
55
- "@commitlint/config-conventional": "^19.8.1",
43
+ "@commitlint/cli": "^20.0.0",
44
+ "@commitlint/config-conventional": "^20.0.0",
56
45
  "@semantic-release/changelog": "^6.0.3",
57
46
  "@semantic-release/git": "^10.0.1",
58
47
  "husky": "^9.1.7",
59
48
  "npm-run-all": "^4.1.5",
60
49
  "semantic-release": "^24.2.9"
61
50
  },
51
+ "peerDependencies": {
52
+ "react": ">=18.0.0",
53
+ "react-dom": ">=18.0.0"
54
+ },
62
55
  "publishConfig": {
63
56
  "access": "public"
64
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;UAGiB,gBAAA;0BACS,kBAAkB,UAAU,KAAA,CAAM,UAAU;EADrD,QAAA,CAAA,EAAA,QAAgB,GAAA,SAAA,GAAA,QAAA,GAAA,OAAA;EAAA,QAAA,CAAA,EAGpB,KAAA,CAAM,YAHc,GAGC,KAAA,CAAM,YAHP,EAAA;SACP,CAAA,EAAA,CAAA,MAAA,EAGL,OAHK,EAAA,SAAA,EAGe,OAHf,EAAA,GAAA,IAAA;WAAkB,CAAA,EAAA,CAAA,MAAA,EAIrB,OAJqB,EAAA,SAAA,EAID,OAJC,EAAA,GAAA,IAAA;KAA0B,CAAA,EAK9D,KAAA,CAAM,GALwD;;cAoDhE,WAlDa,EAAA;;IAAe,MAAM;IAAA,QAAA;IAAA,QAAA;IAAA,OAAA;IAAA,SAAA;IAAA;EAAA,CAAA,EAkDiD,gBAlDjD,CAAA,EAkDiE,KAAA,CAAA,WAlDjE,GAAA,IAAA;aACnB,EAAA,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;UAGiB,gBAAA;0BACS,kBAAkB,UAAU,KAAA,CAAM,UAAU;EADrD,QAAA,CAAA,EAAA,QAAgB,GAAA,SAAA,GAAA,QAAA,GAAA,OAAA;EAAA,QAAA,CAAA,EAGpB,KAAA,CAAM,YAHc,GAGC,KAAA,CAAM,YAHP,EAAA;SACP,CAAA,EAAA,CAAA,MAAA,EAGL,OAHK,EAAA,SAAA,EAGe,OAHf,EAAA,GAAA,IAAA;WAAkB,CAAA,EAAA,CAAA,MAAA,EAIrB,OAJqB,EAAA,SAAA,EAID,OAJC,EAAA,GAAA,IAAA;KAA0B,CAAA,EAK9D,KAAA,CAAM,GALwD;;cA4DhE,WA1Da,EAAA;;IAAe,MAAM;IAAA,QAAA;IAAA,QAAA;IAAA,OAAA;IAAA,SAAA;IAAA;EAAA,CAAA,EA0DiD,gBA1DjD,CAAA,EA0DiE,KAAA,CAAA,WA1DjE,GAAA,IAAA;aACnB,EAAA,MAAA"}
@@ -1,2 +1,2 @@
1
- import e,{useCallback as t,useEffect as n,useLayoutEffect as r,useRef as i,useState as a}from"react";import o from"react-dom";const s=e=>{let t=Object.getOwnPropertyDescriptor(e.props,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)},c=e=>typeof e==`string`?document.querySelector(e):typeof e==`function`?e():e&&`current`in e?e.current:e,l=(...e)=>t=>e.forEach(e=>{typeof e==`function`?e(t):e&&(e.current=t)}),u=({anchor:u,position:d=`append`,children:f,onMount:p,onUnmount:m,key:h})=>{let g=i(null),[_,v]=a(null),y=e.Children.map(f,t=>{if(!e.isValidElement(t))return null;let n=s(t);return e.cloneElement(t,{ref:l(n,e=>{e&&g.current?.insertAdjacentElement({before:`beforebegin`,prepend:`afterbegin`,append:`beforeend`,after:`afterend`}[d],e)})})}),b=t(()=>{g.current=c(u),v(d===`prepend`||d===`append`?g.current:g.current?.parentElement??null)},[u,d]);return r(()=>{b();let e=new MutationObserver(e=>{!e.flatMap(({addedNodes:e,removedNodes:t})=>[...e,...t]).some(e=>_?.contains(e))&&b()});return e.observe(document.body,{childList:!0,subtree:!0}),()=>e.disconnect()},[b,u,_]),n(()=>{if(_&&g.current)return p?.(g.current,_),()=>{m?.(g.current,_)}},[p,m,_]),_&&o.createPortal(y,_,h)};u.displayName=`MagicPortal`;var d=u;export{d as default};
1
+ import e,{useCallback as t,useEffect as n,useLayoutEffect as r,useRef as i,useState as a}from"react";import o from"react-dom";const s=e=>{let t=Object.getOwnPropertyDescriptor(e.props,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning;return n?e.ref:(t=Object.getOwnPropertyDescriptor(e,`ref`)?.get,n=t&&`isReactWarning`in t&&t.isReactWarning,n?e.props.ref:e.props.ref||e.ref)},c=e=>typeof e==`string`?document.querySelector(e):typeof e==`function`?e():e&&`current`in e?e.current:e,l=(e,t)=>{if(typeof e==`function`)return e(t);e!=null&&(e.current=t)},u=(...e)=>t=>{let n=e.map(e=>l(e,t));return()=>n.forEach((t,n)=>typeof t==`function`?t():l(e[n],null))},d=({anchor:l,position:d=`append`,children:f,onMount:p,onUnmount:m,key:h})=>{let g=i(null),[_,v]=a(null),y=e.Children.map(f,t=>{if(!e.isValidElement(t))return null;let n=s(t);return e.cloneElement(t,{ref:u(n,e=>{e&&g.current?.insertAdjacentElement({before:`beforebegin`,prepend:`afterbegin`,append:`beforeend`,after:`afterend`}[d],e)})})}),b=t(()=>{g.current=c(l),v(d===`prepend`||d===`append`?g.current:g.current?.parentElement??null)},[l,d]);return r(()=>{b();let e=new MutationObserver(e=>{!e.flatMap(({addedNodes:e,removedNodes:t})=>[...e,...t]).some(e=>_?.contains(e))&&b()});return e.observe(document.body,{childList:!0,subtree:!0}),()=>e.disconnect()},[b,l,_]),n(()=>{if(_&&g.current)return p?.(g.current,_),()=>{m?.(g.current,_)}},[p,m,_]),_&&o.createPortal(y,_,h)};d.displayName=`MagicPortal`;var f=d;export{f as default};
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from 'react'\nimport ReactDOM from 'react-dom'\n\nexport interface MagicPortalProps {\n anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null\n position?: 'append' | 'prepend' | 'before' | 'after'\n children?: React.ReactElement | React.ReactElement[]\n onMount?: (anchor: Element, container: Element) => void\n onUnmount?: (anchor: Element, container: Element) => void\n key?: React.Key\n}\n\n/**\n * https://github.com/radix-ui/primitives/blob/36d954d3c1b41c96b1d2e875b93fc9362c8c09e6/packages/react/slot/src/slot.tsx#L166\n */\nconst getElementRef = (element: React.ReactElement) => {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) {\n return (element as any).ref as React.Ref<Element>\n }\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) {\n return (element.props as { ref?: React.Ref<Element> }).ref\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref<Element> }).ref || ((element as any).ref as React.Ref<Element>)\n}\n\nconst resolveAnchor = (anchor: MagicPortalProps['anchor']) => {\n if (typeof anchor === 'string') {\n return document.querySelector(anchor)\n } else if (typeof anchor === 'function') {\n return anchor()\n } else if (anchor && 'current' in anchor) {\n return anchor.current\n } else {\n return anchor\n }\n}\n\nconst mergeRef = <T extends Element | null>(...refs: (React.Ref<T> | undefined)[]) => {\n return (node: T) =>\n refs.forEach((ref) => {\n if (typeof ref === 'function') {\n ref(node)\n } else if (ref) {\n ref.current = node\n }\n })\n}\n\nconst MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, key }: MagicPortalProps) => {\n const anchorRef = useRef<Element | null>(null)\n const [container, setContainer] = useState<Element | null>(null)\n\n const nodes = React.Children.map(children, (item) => {\n if (!React.isValidElement(item)) {\n return null\n }\n const originalRef = getElementRef(item)\n return React.cloneElement(item as React.ReactElement<any>, {\n ref: mergeRef(originalRef, (node) => {\n const positionMap = {\n before: 'beforebegin',\n prepend: 'afterbegin',\n append: 'beforeend',\n after: 'afterend'\n } as const\n node && anchorRef.current?.insertAdjacentElement(positionMap[position], node)\n })\n })\n })\n\n const update = useCallback(() => {\n anchorRef.current = resolveAnchor(anchor)\n setContainer(\n position === 'prepend' || position === 'append' ? anchorRef.current : (anchorRef.current?.parentElement ?? null)\n )\n }, [anchor, position])\n\n useLayoutEffect(() => {\n update()\n\n const observer = new MutationObserver((mutations) => {\n const isSelfMutation = mutations\n .flatMap(({ addedNodes, removedNodes }) => [...addedNodes, ...removedNodes])\n .some((node) => container?.contains(node))\n !isSelfMutation && update()\n })\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n })\n return () => observer.disconnect()\n }, [update, anchor, container])\n\n useEffect(() => {\n if (container && anchorRef.current) {\n onMount?.(anchorRef.current, container)\n return () => {\n onUnmount?.(anchorRef.current!, container)\n }\n }\n }, [onMount, onUnmount, container])\n\n return container && ReactDOM.createPortal(nodes, container, key)\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"8HAeA,MAAM,EAAiB,GAAgC,CAErD,IAAI,EAAS,OAAO,yBAAyB,EAAQ,MAAO,MAAM,EAAE,IAChE,EAAU,GAAU,mBAAoB,GAAU,EAAO,eAY7D,OAXI,EACM,EAAgB,KAG1B,EAAS,OAAO,yBAAyB,EAAS,MAAM,EAAE,IAC1D,EAAU,GAAU,mBAAoB,GAAU,EAAO,eACrD,EACM,EAAQ,MAAuC,IAIjD,EAAQ,MAAuC,KAAS,EAAgB,MAG5E,EAAiB,GACjB,OAAO,GAAW,SACb,SAAS,cAAc,EAAO,CAC5B,OAAO,GAAW,WACpB,GAAQ,CACN,GAAU,YAAa,EACzB,EAAO,QAEP,EAIL,GAAsC,GAAG,IACrC,GACN,EAAK,QAAS,GAAQ,CAChB,OAAO,GAAQ,WACjB,EAAI,EAAK,CACA,IACT,EAAI,QAAU,IAEhB,CAGA,GAAe,CAAE,SAAQ,WAAW,SAAU,WAAU,UAAS,YAAW,SAA4B,CAC5G,IAAM,EAAY,EAAuB,KAAK,CACxC,CAAC,EAAW,GAAgB,EAAyB,KAAK,CAE1D,EAAQ,EAAM,SAAS,IAAI,EAAW,GAAS,CACnD,GAAI,CAAC,EAAM,eAAe,EAAK,CAC7B,OAAO,KAET,IAAM,EAAc,EAAc,EAAK,CACvC,OAAO,EAAM,aAAa,EAAiC,CACzD,IAAK,EAAS,EAAc,GAAS,CAOnC,GAAQ,EAAU,SAAS,sBANP,CAClB,OAAQ,cACR,QAAS,aACT,OAAQ,YACR,MAAO,WACR,CAC4D,GAAW,EAAK,EAC7E,CACH,CAAC,EACF,CAEI,EAAS,MAAkB,CAC/B,EAAU,QAAU,EAAc,EAAO,CACzC,EACE,IAAa,WAAa,IAAa,SAAW,EAAU,QAAW,EAAU,SAAS,eAAiB,KAC5G,EACA,CAAC,EAAQ,EAAS,CAAC,CA4BtB,OA1BA,MAAsB,CACpB,GAAQ,CAER,IAAM,EAAW,IAAI,iBAAkB,GAAc,CAInD,CAHuB,EACpB,SAAS,CAAE,aAAY,kBAAmB,CAAC,GAAG,EAAY,GAAG,EAAa,CAAC,CAC3E,KAAM,GAAS,GAAW,SAAS,EAAK,CAAC,EACzB,GAAQ,EAC3B,CAMF,OAJA,EAAS,QAAQ,SAAS,KAAM,CAC9B,UAAW,GACX,QAAS,GACV,CAAC,KACW,EAAS,YAAY,EACjC,CAAC,EAAQ,EAAQ,EAAU,CAAC,CAE/B,MAAgB,CACd,GAAI,GAAa,EAAU,QAEzB,OADA,IAAU,EAAU,QAAS,EAAU,KAC1B,CACX,IAAY,EAAU,QAAU,EAAU,GAG7C,CAAC,EAAS,EAAW,EAAU,CAAC,CAE5B,GAAa,EAAS,aAAa,EAAO,EAAW,EAAI,EAGlE,EAAY,YAAc,cAE1B,IAAA,EAAe"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from 'react'\nimport ReactDOM from 'react-dom'\n\nexport interface MagicPortalProps {\n anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null\n position?: 'append' | 'prepend' | 'before' | 'after'\n children?: React.ReactElement | React.ReactElement[]\n onMount?: (anchor: Element, container: Element) => void\n onUnmount?: (anchor: Element, container: Element) => void\n key?: React.Key\n}\n\n/**\n * https://github.com/radix-ui/primitives/blob/36d954d3c1b41c96b1d2e875b93fc9362c8c09e6/packages/react/slot/src/slot.tsx#L166\n */\nconst getElementRef = (element: React.ReactElement) => {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) {\n return (element as any).ref as React.Ref<Element>\n }\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning\n if (mayWarn) {\n return (element.props as { ref?: React.Ref<Element> }).ref\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref<Element> }).ref || ((element as any).ref as React.Ref<Element>)\n}\n\nconst resolveAnchor = (anchor: MagicPortalProps['anchor']) => {\n if (typeof anchor === 'string') {\n return document.querySelector(anchor)\n } else if (typeof anchor === 'function') {\n return anchor()\n } else if (anchor && 'current' in anchor) {\n return anchor.current\n } else {\n return anchor\n }\n}\n\n/**\n * https://github.com/facebook/react/blob/d91d28c8ba6fe7c96e651f82fc47c9d5481bf5f9/packages/react-reconciler/src/ReactFiberHooks.js#L2792\n */\nconst setRef = <T>(ref: React.Ref<T> | undefined, value: T) => {\n if (typeof ref === 'function') {\n return ref(value)\n } else if (ref !== null && ref !== undefined) {\n ref.current = value\n }\n}\n\nconst mergeRef = <T extends Element | null>(...refs: (React.Ref<T> | undefined)[]) => {\n return (node: T) => {\n const cleanups = refs.map((ref) => setRef(ref, node))\n return () =>\n cleanups.forEach((cleanup, index) => (typeof cleanup === 'function' ? cleanup() : setRef(refs[index], null)))\n }\n}\n\nconst MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, key }: MagicPortalProps) => {\n const anchorRef = useRef<Element | null>(null)\n const [container, setContainer] = useState<Element | null>(null)\n\n const nodes = React.Children.map(children, (item) => {\n if (!React.isValidElement(item)) {\n return null\n }\n const originalRef = getElementRef(item)\n return React.cloneElement(item as React.ReactElement<any>, {\n ref: mergeRef(originalRef, (node) => {\n const positionMap = {\n before: 'beforebegin',\n prepend: 'afterbegin',\n append: 'beforeend',\n after: 'afterend'\n } as const\n node && anchorRef.current?.insertAdjacentElement(positionMap[position], node)\n })\n })\n })\n\n const update = useCallback(() => {\n anchorRef.current = resolveAnchor(anchor)\n setContainer(\n position === 'prepend' || position === 'append' ? anchorRef.current : (anchorRef.current?.parentElement ?? null)\n )\n }, [anchor, position])\n\n useLayoutEffect(() => {\n update()\n\n const observer = new MutationObserver((mutations) => {\n const isSelfMutation = mutations\n .flatMap(({ addedNodes, removedNodes }) => [...addedNodes, ...removedNodes])\n .some((node) => container?.contains(node))\n !isSelfMutation && update()\n })\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n })\n return () => observer.disconnect()\n }, [update, anchor, container])\n\n useEffect(() => {\n if (container && anchorRef.current) {\n onMount?.(anchorRef.current, container)\n return () => {\n onUnmount?.(anchorRef.current!, container)\n }\n }\n }, [onMount, onUnmount, container])\n\n return container && ReactDOM.createPortal(nodes, container, key)\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"8HAeA,MAAM,EAAiB,GAAgC,CAErD,IAAI,EAAS,OAAO,yBAAyB,EAAQ,MAAO,MAAM,EAAE,IAChE,EAAU,GAAU,mBAAoB,GAAU,EAAO,eAY7D,OAXI,EACM,EAAgB,KAG1B,EAAS,OAAO,yBAAyB,EAAS,MAAM,EAAE,IAC1D,EAAU,GAAU,mBAAoB,GAAU,EAAO,eACrD,EACM,EAAQ,MAAuC,IAIjD,EAAQ,MAAuC,KAAS,EAAgB,MAG5E,EAAiB,GACjB,OAAO,GAAW,SACb,SAAS,cAAc,EAAO,CAC5B,OAAO,GAAW,WACpB,GAAQ,CACN,GAAU,YAAa,EACzB,EAAO,QAEP,EAOL,GAAa,EAA+B,IAAa,CAC7D,GAAI,OAAO,GAAQ,WACjB,OAAO,EAAI,EAAM,CACR,GAAQ,OACjB,EAAI,QAAU,IAIZ,GAAsC,GAAG,IACrC,GAAY,CAClB,IAAM,EAAW,EAAK,IAAK,GAAQ,EAAO,EAAK,EAAK,CAAC,CACrD,UACE,EAAS,SAAS,EAAS,IAAW,OAAO,GAAY,WAAa,GAAS,CAAG,EAAO,EAAK,GAAQ,KAAK,CAAE,EAI7G,GAAe,CAAE,SAAQ,WAAW,SAAU,WAAU,UAAS,YAAW,SAA4B,CAC5G,IAAM,EAAY,EAAuB,KAAK,CACxC,CAAC,EAAW,GAAgB,EAAyB,KAAK,CAE1D,EAAQ,EAAM,SAAS,IAAI,EAAW,GAAS,CACnD,GAAI,CAAC,EAAM,eAAe,EAAK,CAC7B,OAAO,KAET,IAAM,EAAc,EAAc,EAAK,CACvC,OAAO,EAAM,aAAa,EAAiC,CACzD,IAAK,EAAS,EAAc,GAAS,CAOnC,GAAQ,EAAU,SAAS,sBANP,CAClB,OAAQ,cACR,QAAS,aACT,OAAQ,YACR,MAAO,WACR,CAC4D,GAAW,EAAK,EAC7E,CACH,CAAC,EACF,CAEI,EAAS,MAAkB,CAC/B,EAAU,QAAU,EAAc,EAAO,CACzC,EACE,IAAa,WAAa,IAAa,SAAW,EAAU,QAAW,EAAU,SAAS,eAAiB,KAC5G,EACA,CAAC,EAAQ,EAAS,CAAC,CA4BtB,OA1BA,MAAsB,CACpB,GAAQ,CAER,IAAM,EAAW,IAAI,iBAAkB,GAAc,CAInD,CAHuB,EACpB,SAAS,CAAE,aAAY,kBAAmB,CAAC,GAAG,EAAY,GAAG,EAAa,CAAC,CAC3E,KAAM,GAAS,GAAW,SAAS,EAAK,CAAC,EACzB,GAAQ,EAC3B,CAMF,OAJA,EAAS,QAAQ,SAAS,KAAM,CAC9B,UAAW,GACX,QAAS,GACV,CAAC,KACW,EAAS,YAAY,EACjC,CAAC,EAAQ,EAAQ,EAAU,CAAC,CAE/B,MAAgB,CACd,GAAI,GAAa,EAAU,QAEzB,OADA,IAAU,EAAU,QAAS,EAAU,KAC1B,CACX,IAAY,EAAU,QAAU,EAAU,GAG7C,CAAC,EAAS,EAAW,EAAU,CAAC,CAE5B,GAAa,EAAS,aAAa,EAAO,EAAW,EAAI,EAGlE,EAAY,YAAc,cAE1B,IAAA,EAAe"}
@@ -11,26 +11,12 @@
11
11
  "check": "tsc --noEmit"
12
12
  },
13
13
  "keywords": [
14
- "react",
15
- "portal",
16
- "dynamic",
14
+ "react-portal",
15
+ "anchor",
17
16
  "browser-extension",
18
17
  "content-script",
19
- "dom",
20
18
  "mutation-observer",
21
- "inject",
22
- "mount",
23
- "anchor",
24
- "reactdom",
25
- "createportal",
26
- "spa",
27
- "single-page-application",
28
- "web-extension",
29
- "chrome-extension",
30
- "firefox-extension",
31
- "dynamic-content",
32
- "dom-manipulation",
33
- "typescript"
19
+ "dynamic-content"
34
20
  ],
35
21
  "author": "molvqingtai",
36
22
  "license": "MIT",
@@ -42,28 +28,28 @@
42
28
  "url": "https://github.com/molvqingtai/react-magic-portal/issues"
43
29
  },
44
30
  "homepage": "https://github.com/molvqingtai/react-magic-portal#readme",
45
- "peerDependencies": {
46
- "react": ">=18.0.0",
47
- "react-dom": ">=18.0.0"
48
- },
49
31
  "devDependencies": {
50
32
  "@eslint/js": "^9.36.0",
51
33
  "@types/node": "^24.5.2",
52
- "@types/react": "^19.1.13",
34
+ "@types/react": "^19.1.14",
53
35
  "@types/react-dom": "^19.1.9",
54
36
  "eslint": "^9.36.0",
55
37
  "eslint-config-prettier": "^10.1.8",
56
38
  "eslint-plugin-prettier": "^5.5.4",
57
39
  "eslint-plugin-react-hooks": "^5.2.0",
58
- "eslint-plugin-react-refresh": "^0.4.21",
40
+ "eslint-plugin-react-refresh": "^0.4.22",
59
41
  "globals": "^16.4.0",
60
42
  "prettier": "^3.6.2",
61
43
  "react": "^19.1.1",
62
44
  "react-dom": "^19.1.1",
63
- "tsdown": "^0.15.4",
45
+ "tsdown": "^0.15.5",
64
46
  "typescript": "^5.9.2",
65
47
  "typescript-eslint": "^8.44.1"
66
48
  },
49
+ "peerDependencies": {
50
+ "react": ">=18.0.0",
51
+ "react-dom": ">=18.0.0"
52
+ },
67
53
  "publishConfig": {
68
54
  "access": "public"
69
55
  }
@@ -43,15 +43,23 @@ const resolveAnchor = (anchor: MagicPortalProps['anchor']) => {
43
43
  }
44
44
  }
45
45
 
46
+ /**
47
+ * https://github.com/facebook/react/blob/d91d28c8ba6fe7c96e651f82fc47c9d5481bf5f9/packages/react-reconciler/src/ReactFiberHooks.js#L2792
48
+ */
49
+ const setRef = <T>(ref: React.Ref<T> | undefined, value: T) => {
50
+ if (typeof ref === 'function') {
51
+ return ref(value)
52
+ } else if (ref !== null && ref !== undefined) {
53
+ ref.current = value
54
+ }
55
+ }
56
+
46
57
  const mergeRef = <T extends Element | null>(...refs: (React.Ref<T> | undefined)[]) => {
47
- return (node: T) =>
48
- refs.forEach((ref) => {
49
- if (typeof ref === 'function') {
50
- ref(node)
51
- } else if (ref) {
52
- ref.current = node
53
- }
54
- })
58
+ return (node: T) => {
59
+ const cleanups = refs.map((ref) => setRef(ref, node))
60
+ return () =>
61
+ cleanups.forEach((cleanup, index) => (typeof cleanup === 'function' ? cleanup() : setRef(refs[index], null)))
62
+ }
55
63
  }
56
64
 
57
65
  const MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, key }: MagicPortalProps) => {
@@ -17,13 +17,13 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@eslint/js": "^9.36.0",
20
- "@types/react": "^19.1.13",
20
+ "@types/react": "^19.1.14",
21
21
  "@types/react-dom": "^19.1.9",
22
- "@vitejs/plugin-react": "^5.0.3",
22
+ "@vitejs/plugin-react": "^5.0.4",
23
23
  "eslint": "^9.36.0",
24
24
  "eslint-plugin-prettier": "^5.5.4",
25
25
  "eslint-plugin-react-hooks": "^5.2.0",
26
- "eslint-plugin-react-refresh": "^0.4.21",
26
+ "eslint-plugin-react-refresh": "^0.4.22",
27
27
  "globals": "^16.4.0",
28
28
  "typescript": "~5.9.2",
29
29
  "typescript-eslint": "^8.44.1",
@@ -6,12 +6,12 @@ import './App.css'
6
6
  function App() {
7
7
  const [showAnchor, setShowAnchor] = useState(true)
8
8
 
9
- const handleMount = (anchor: Element, contaner: Element) => {
10
- console.log('Portal mounted:', { anchor, contaner })
9
+ const handleMount = (anchor: Element, container: Element) => {
10
+ console.log('Portal mounted:', { anchor, container })
11
11
  }
12
12
 
13
- const handleUnmount = (anchor: Element, contaner: Element) => {
14
- console.log('Portal unmounted:', { anchor, contaner })
13
+ const handleUnmount = (anchor: Element, container: Element) => {
14
+ console.log('Portal unmounted:', { anchor, container })
15
15
  }
16
16
 
17
17
  return (