react-magic-portal 1.2.1 → 1.3.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 +7 -0
- package/README.md +1 -1
- package/__tests__/src/magic-portal.test.tsx +39 -83
- package/package.json +1 -1
- package/packages/component/dist/index.d.ts +1 -1
- package/packages/component/dist/index.d.ts.map +1 -1
- package/packages/component/dist/index.js +1 -1
- package/packages/component/dist/index.js.map +1 -1
- package/packages/component/eslint.config.ts +2 -0
- package/packages/component/src/index.ts +23 -8
- package/packages/example/dist/assets/{index-jJ0JbhKk.js → index-BBgHwIii.js} +12 -12
- package/packages/example/dist/index.html +1 -1
- package/packages/example/src/App.tsx +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/molvqingtai/react-magic-portal/compare/v1.2.1...v1.3.0) (2025-10-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enforce single child ([8fe438d](https://github.com/molvqingtai/react-magic-portal/commit/8fe438d149a04d3070fe8eed725d7932492da01c))
|
|
7
|
+
|
|
1
8
|
## [1.2.1](https://github.com/molvqingtai/react-magic-portal/compare/v1.2.0...v1.2.1) (2025-10-10)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ function App() {
|
|
|
102
102
|
| `anchor` | `string \| (() => Element \| null) \| Element \| React.RefObject<Element \| null> \| null` | **Required** | The target element where the portal content will be rendered |
|
|
103
103
|
| `position` | `'append' \| 'prepend' \| 'before' \| 'after'` | `'append'` | Position relative to the anchor element |
|
|
104
104
|
| `root` | `Element` | `document.body` | The root element to observe for DOM mutations |
|
|
105
|
-
| `children` | `React.ReactElement \|
|
|
105
|
+
| `children` | `React.ReactElement \| null` | `undefined` | A single React element to render in the portal (does not support Fragment) |
|
|
106
106
|
| `onMount` | `(anchor: Element, container: Element) => void` | `undefined` | Callback fired when the portal is mounted |
|
|
107
107
|
| `onUnmount` | `(anchor: Element, container: Element) => void` | `undefined` | Callback fired when the portal is unmounted |
|
|
108
108
|
|
|
@@ -382,28 +382,6 @@ describe('MagicPortal', () => {
|
|
|
382
382
|
expect(refCallback).toHaveBeenLastCalledWith(null)
|
|
383
383
|
})
|
|
384
384
|
|
|
385
|
-
it('should handle multiple content elements with refs', () => {
|
|
386
|
-
const ref1 = vi.fn()
|
|
387
|
-
const ref2 = vi.fn()
|
|
388
|
-
const anchor = document.createElement('div')
|
|
389
|
-
anchor.id = 'multi-ref-anchor'
|
|
390
|
-
document.body.appendChild(anchor)
|
|
391
|
-
|
|
392
|
-
render(
|
|
393
|
-
<MagicPortal anchor="#multi-ref-anchor">
|
|
394
|
-
<div ref={ref1} data-testid="content-1">
|
|
395
|
-
Content 1
|
|
396
|
-
</div>
|
|
397
|
-
<span ref={ref2} data-testid="content-2">
|
|
398
|
-
Content 2
|
|
399
|
-
</span>
|
|
400
|
-
</MagicPortal>
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
expect(ref1).toHaveBeenCalledWith(expect.any(HTMLDivElement))
|
|
404
|
-
expect(ref2).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
|
|
405
|
-
})
|
|
406
|
-
|
|
407
385
|
it('should maintain content refs across position changes', async () => {
|
|
408
386
|
const contentRef = vi.fn()
|
|
409
387
|
const anchor = document.createElement('div')
|
|
@@ -466,43 +444,26 @@ describe('MagicPortal', () => {
|
|
|
466
444
|
expect(refCalls[0]?.textContent).toBe('Click me')
|
|
467
445
|
})
|
|
468
446
|
|
|
469
|
-
it('should
|
|
447
|
+
it('should log error when multiple children are provided', () => {
|
|
448
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
470
449
|
const anchor = document.createElement('div')
|
|
471
|
-
anchor.id = '
|
|
450
|
+
anchor.id = 'multiple-children-anchor'
|
|
472
451
|
document.body.appendChild(anchor)
|
|
473
452
|
|
|
474
453
|
render(
|
|
475
|
-
|
|
476
|
-
|
|
454
|
+
// @ts-expect-error - intentionally passing multiple children to assert runtime guard
|
|
455
|
+
<MagicPortal anchor="#multiple-children-anchor">
|
|
456
|
+
<span data-testid="first-element">First element</span>
|
|
457
|
+
<span data-testid="second-element">Second element</span>
|
|
477
458
|
</MagicPortal>
|
|
478
459
|
)
|
|
479
460
|
|
|
480
|
-
expect(
|
|
481
|
-
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
it('should handle multiple element content with refs', () => {
|
|
485
|
-
const ref1 = vi.fn()
|
|
486
|
-
const ref2 = vi.fn()
|
|
487
|
-
const anchor = document.createElement('div')
|
|
488
|
-
anchor.id = 'multiple-content-anchor'
|
|
489
|
-
document.body.appendChild(anchor)
|
|
490
|
-
|
|
491
|
-
render(
|
|
492
|
-
<MagicPortal anchor="#multiple-content-anchor">
|
|
493
|
-
<span ref={ref1} data-testid="first-element">
|
|
494
|
-
First element
|
|
495
|
-
</span>
|
|
496
|
-
<div ref={ref2} data-testid="second-element">
|
|
497
|
-
Second element
|
|
498
|
-
</div>
|
|
499
|
-
</MagicPortal>
|
|
461
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
462
|
+
'[react-magic-portal] Multiple children are not supported, expected to receive a single React element child.'
|
|
500
463
|
)
|
|
464
|
+
expect(anchor.children.length).toBe(0)
|
|
501
465
|
|
|
502
|
-
|
|
503
|
-
expect(ref2).toHaveBeenCalledWith(expect.any(HTMLDivElement))
|
|
504
|
-
expect(screen.getByTestId('first-element')).toBeTruthy()
|
|
505
|
-
expect(screen.getByTestId('second-element')).toBeTruthy()
|
|
466
|
+
consoleErrorSpy.mockRestore()
|
|
506
467
|
})
|
|
507
468
|
|
|
508
469
|
it('should handle nested refs correctly', () => {
|
|
@@ -707,61 +668,56 @@ describe('MagicPortal', () => {
|
|
|
707
668
|
})
|
|
708
669
|
|
|
709
670
|
describe('Text Node Handling', () => {
|
|
710
|
-
it('should not render
|
|
671
|
+
it('should not render Fragment children', () => {
|
|
672
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
711
673
|
const anchor = document.createElement('div')
|
|
712
|
-
anchor.id = '
|
|
674
|
+
anchor.id = 'fragment-anchor'
|
|
713
675
|
document.body.appendChild(anchor)
|
|
714
676
|
|
|
715
677
|
render(
|
|
716
|
-
|
|
717
|
-
|
|
678
|
+
<MagicPortal anchor="#fragment-anchor">
|
|
679
|
+
<>
|
|
680
|
+
<div>Child 1</div>
|
|
681
|
+
<div>Child 2</div>
|
|
682
|
+
</>
|
|
683
|
+
</MagicPortal>
|
|
718
684
|
)
|
|
719
685
|
|
|
720
|
-
//
|
|
686
|
+
// Should log error about Fragment not being supported
|
|
687
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
688
|
+
'[react-magic-portal] Fragment children are not supported, expected to receive a single React element child.'
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
// Anchor should remain empty since Fragment is not supported
|
|
721
692
|
expect(anchor.textContent).toBe('')
|
|
722
693
|
expect(anchor.children.length).toBe(0)
|
|
694
|
+
|
|
695
|
+
consoleErrorSpy.mockRestore()
|
|
723
696
|
})
|
|
724
697
|
|
|
725
|
-
it('should not render
|
|
698
|
+
it('should not render pure text content', () => {
|
|
726
699
|
const anchor = document.createElement('div')
|
|
727
|
-
anchor.id = '
|
|
700
|
+
anchor.id = 'text-only-anchor'
|
|
728
701
|
document.body.appendChild(anchor)
|
|
729
702
|
|
|
730
703
|
render(
|
|
731
|
-
// @ts-expect-error - testing
|
|
732
|
-
<MagicPortal anchor="#
|
|
733
|
-
Some text before
|
|
734
|
-
{/* @ts-expect-error - testing mixed content behavior*/}
|
|
735
|
-
<div data-testid="element-content">Element</div>
|
|
736
|
-
Some text after
|
|
737
|
-
</MagicPortal>
|
|
704
|
+
// @ts-expect-error - testing that text nodes are not rendered
|
|
705
|
+
<MagicPortal anchor="#text-only-anchor">Just plain text</MagicPortal>
|
|
738
706
|
)
|
|
739
707
|
|
|
740
|
-
//
|
|
741
|
-
expect(
|
|
742
|
-
expect(anchor.
|
|
743
|
-
|
|
744
|
-
// Text content should only be from the div element, not the text nodes
|
|
745
|
-
expect(anchor.textContent?.trim()).toBe('Element')
|
|
746
|
-
expect(anchor.textContent).not.toContain('Some text before')
|
|
747
|
-
expect(anchor.textContent).not.toContain('Some text after')
|
|
708
|
+
// Anchor should remain empty since text nodes are not rendered
|
|
709
|
+
expect(anchor.textContent).toBe('')
|
|
710
|
+
expect(anchor.children.length).toBe(0)
|
|
748
711
|
})
|
|
749
712
|
|
|
750
|
-
it('should not render
|
|
713
|
+
it('should not render null children', () => {
|
|
751
714
|
const anchor = document.createElement('div')
|
|
752
|
-
anchor.id = '
|
|
715
|
+
anchor.id = 'null-anchor'
|
|
753
716
|
document.body.appendChild(anchor)
|
|
754
717
|
|
|
755
|
-
render(
|
|
756
|
-
<MagicPortal anchor="#primitive-anchor">
|
|
757
|
-
{42 as any}
|
|
758
|
-
{true as any}
|
|
759
|
-
{null as any}
|
|
760
|
-
{undefined as any}
|
|
761
|
-
</MagicPortal>
|
|
762
|
-
)
|
|
718
|
+
render(<MagicPortal anchor="#null-anchor">{null}</MagicPortal>)
|
|
763
719
|
|
|
764
|
-
// Anchor should remain empty since
|
|
720
|
+
// Anchor should remain empty since null is not rendered
|
|
765
721
|
expect(anchor.textContent).toBe('')
|
|
766
722
|
expect(anchor.children.length).toBe(0)
|
|
767
723
|
})
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ interface MagicPortalProps {
|
|
|
5
5
|
anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null;
|
|
6
6
|
position?: 'append' | 'prepend' | 'before' | 'after';
|
|
7
7
|
root?: Element;
|
|
8
|
-
children?: React.ReactElement |
|
|
8
|
+
children?: React.ReactElement | null;
|
|
9
9
|
onMount?: (anchor: Element, container: Element) => void;
|
|
10
10
|
onUnmount?: (anchor: Element, container: Element) => void;
|
|
11
11
|
}
|
|
@@ -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,IAAA,CAAA,EAGxB,OAHwB;UACP,CAAA,EAGb,KAAA,CAAM,YAHO,
|
|
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,IAAA,CAAA,EAGxB,OAHwB;UACP,CAAA,EAGb,KAAA,CAAM,YAHO,GAAA,IAAA;SAAkB,CAAA,EAAA,CAAA,MAAA,EAIvB,OAJuB,EAAA,SAAA,EAIH,OAJG,EAAA,GAAA,IAAA;WAA0B,CAAA,EAAA,CAAA,MAAA,EAK/C,OAL+C,EAAA,SAAA,EAK3B,OAL2B,EAAA,GAAA,IAAA;;cAoEhE,WAlEG,EAAA;;IACI,MAAM;IAAA,QAAA;IAAA,IAAA;IAAA,QAAA;IAAA,OAAA;IAAA;EAAA,CAAA,EAwEhB,gBAxEgB,CAAA,EAwEA,KAAA,CAAA,WAxEA,GAAA,IAAA;aACE,EAAA,MAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e,{useCallback as t,useEffect as n,useLayoutEffect as r,
|
|
1
|
+
import e,{useCallback as t,useEffect as n,useLayoutEffect as r,useMemo as i,useRef as a,useState as o}from"react";import{createPortal as s}from"react-dom";const c=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)},l=e=>typeof e==`string`?document.querySelector(e):typeof e==`function`?e():e&&`current`in e?e.current:e,u=(e,t)=>e?t===`prepend`||t===`append`?e:e.parentElement:null,d=(e,t)=>{if(typeof e==`function`)return e(t);e!=null&&(e.current=t)},f=(...e)=>t=>{let n=e.map(e=>d(e,t));return()=>n.forEach((t,n)=>typeof t==`function`?t():d(e[n],null))},p=({anchor:d,position:p=`append`,root:m=document.body,children:h,onMount:g,onUnmount:_})=>{let v=a(null),[y,b]=o(null),x=t(e=>{if(!e)return;let t=v.current;if(!t)return;let n=u(t,p);if(!n)return;let r=!1;switch(p){case`append`:r=e.parentElement===n&&n.lastChild===e;break;case`prepend`:r=e.parentElement===n&&n.firstChild===e;break;case`before`:r=e.parentElement===n&&t.previousSibling===e;break;case`after`:r=e.parentElement===n&&t.nextSibling===e;break}r||t.insertAdjacentElement({before:`beforebegin`,prepend:`afterbegin`,append:`beforeend`,after:`afterend`}[p],e)},[p]),S=i(()=>{if(e.Children.count(h)>1)return console.error(`[react-magic-portal] Multiple children are not supported, expected to receive a single React element child.`),null;if(!e.isValidElement(h))return null;if(h.type===e.Fragment)return console.error(`[react-magic-portal] Fragment children are not supported, expected to receive a single React element child.`),null;let t=c(h);return e.cloneElement(h,{ref:f(t,x)})},[h,x]),C=t(()=>{v.current=l(d);let e=u(v.current,p);e&&(e.__reactWarnedAboutChildrenConflict=!0),b(e)},[d,p]);return r(()=>{C();let e=new MutationObserver(e=>{!e.flatMap(({addedNodes:e,removedNodes:t})=>[...e,...t]).some(e=>y?.contains(e))&&C()});return e.observe(m,{childList:!0,subtree:!0}),()=>e.disconnect()},[C,d,y,m]),n(()=>{if(y&&v.current)return g?.(v.current,y),()=>{_?.(v.current,y)}},[g,_,y]),y&&S?s(S,y):null};p.displayName=`MagicPortal`;var m=p;export{m 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 { createPortal } 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 root?: Element\n children?: React.ReactElement |
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback, useLayoutEffect, useMemo } from 'react'\nimport { createPortal } 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 root?: Element\n children?: React.ReactElement | null\n onMount?: (anchor: Element, container: Element) => void\n onUnmount?: (anchor: Element, container: Element) => void\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 resolveContainer = (anchor: Element | null, position: MagicPortalProps['position']): Element | null => {\n if (!anchor) {\n return null\n }\n\n return position === 'prepend' || position === 'append' ? anchor : anchor.parentElement\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 = ({\n anchor,\n position = 'append',\n root = document.body,\n children,\n onMount,\n onUnmount\n}: MagicPortalProps) => {\n const anchorRef = useRef<Element | null>(null)\n const [container, setContainer] = useState<Element | null>(null)\n\n const insertNode = useCallback(\n (node: Element | null) => {\n if (!node) {\n return\n }\n\n const anchorElement = anchorRef.current\n if (!anchorElement) {\n return\n }\n\n const containerElement = resolveContainer(anchorElement, position)\n if (!containerElement) {\n return\n }\n\n let alreadyPlaced = false\n\n switch (position) {\n case 'append':\n alreadyPlaced = node.parentElement === containerElement && containerElement.lastChild === node\n break\n case 'prepend':\n alreadyPlaced = node.parentElement === containerElement && containerElement.firstChild === node\n break\n case 'before':\n alreadyPlaced = node.parentElement === containerElement && anchorElement.previousSibling === node\n break\n case 'after':\n alreadyPlaced = node.parentElement === containerElement && anchorElement.nextSibling === node\n break\n }\n\n if (!alreadyPlaced) {\n const positionMap = {\n before: 'beforebegin',\n prepend: 'afterbegin',\n append: 'beforeend',\n after: 'afterend'\n } as const\n anchorElement.insertAdjacentElement(positionMap[position], node)\n }\n },\n [position]\n )\n\n const child = useMemo(() => {\n if (React.Children.count(children) > 1) {\n console.error(\n '[react-magic-portal] Multiple children are not supported, expected to receive a single React element child.'\n )\n return null\n }\n\n if (!React.isValidElement(children)) {\n return null\n }\n\n if (children.type === React.Fragment) {\n console.error(\n '[react-magic-portal] Fragment children are not supported, expected to receive a single React element child.'\n )\n return null\n }\n\n const originalRef = getElementRef(children)\n return React.cloneElement(children as React.ReactElement<any>, {\n ref: mergeRef(originalRef, insertNode)\n })\n }, [children, insertNode])\n\n const update = useCallback(() => {\n anchorRef.current = resolveAnchor(anchor)\n const nextContainer = resolveContainer(anchorRef.current, position)\n /**\n * React 19 in DEV\n * Suppress DevTools warning from React runtime about conflicting container children.\n * @see https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L973\n */\n if (nextContainer) {\n ;(nextContainer as { __reactWarnedAboutChildrenConflict?: boolean }).__reactWarnedAboutChildrenConflict = true\n }\n\n setContainer(nextContainer)\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(root, {\n childList: true,\n subtree: true\n })\n return () => observer.disconnect()\n }, [update, anchor, container, root])\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 && child ? createPortal(child, container) : null\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@ const config: Linter.FlatConfig[] = defineConfig([
|
|
|
27
27
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
28
28
|
'@eslint-react/no-children-map': 'off',
|
|
29
29
|
'@eslint-react/no-clone-element': 'off',
|
|
30
|
+
'@eslint-react/no-children-only': 'off',
|
|
31
|
+
'@eslint-react/no-children-count': 'off',
|
|
30
32
|
'@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 'off'
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from 'react'
|
|
1
|
+
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect, useMemo } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
|
|
4
4
|
export interface MagicPortalProps {
|
|
5
5
|
anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null
|
|
6
6
|
position?: 'append' | 'prepend' | 'before' | 'after'
|
|
7
7
|
root?: Element
|
|
8
|
-
children?: React.ReactElement |
|
|
8
|
+
children?: React.ReactElement | null
|
|
9
9
|
onMount?: (anchor: Element, container: Element) => void
|
|
10
10
|
onUnmount?: (anchor: Element, container: Element) => void
|
|
11
11
|
}
|
|
@@ -127,15 +127,30 @@ const MagicPortal = ({
|
|
|
127
127
|
[position]
|
|
128
128
|
)
|
|
129
129
|
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
130
|
+
const child = useMemo(() => {
|
|
131
|
+
if (React.Children.count(children) > 1) {
|
|
132
|
+
console.error(
|
|
133
|
+
'[react-magic-portal] Multiple children are not supported, expected to receive a single React element child.'
|
|
134
|
+
)
|
|
132
135
|
return null
|
|
133
136
|
}
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
|
|
138
|
+
if (!React.isValidElement(children)) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (children.type === React.Fragment) {
|
|
143
|
+
console.error(
|
|
144
|
+
'[react-magic-portal] Fragment children are not supported, expected to receive a single React element child.'
|
|
145
|
+
)
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const originalRef = getElementRef(children)
|
|
150
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
136
151
|
ref: mergeRef(originalRef, insertNode)
|
|
137
152
|
})
|
|
138
|
-
})
|
|
153
|
+
}, [children, insertNode])
|
|
139
154
|
|
|
140
155
|
const update = useCallback(() => {
|
|
141
156
|
anchorRef.current = resolveAnchor(anchor)
|
|
@@ -178,7 +193,7 @@ const MagicPortal = ({
|
|
|
178
193
|
}
|
|
179
194
|
}, [onMount, onUnmount, container])
|
|
180
195
|
|
|
181
|
-
return container ? createPortal(
|
|
196
|
+
return container && child ? createPortal(child, container) : null
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
MagicPortal.displayName = 'MagicPortal'
|