react-magic-portal 1.2.1 → 1.3.1

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.3.1](https://github.com/molvqingtai/react-magic-portal/compare/v1.3.0...v1.3.1) (2025-11-07)
2
+
3
+
4
+ ### Performance Improvements
5
+
6
+ * better position name ([af11ae6](https://github.com/molvqingtai/react-magic-portal/commit/af11ae67e1bf8998067d0cf756128b279ea1f385))
7
+
8
+ # [1.3.0](https://github.com/molvqingtai/react-magic-portal/compare/v1.2.1...v1.3.0) (2025-10-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * enforce single child ([8fe438d](https://github.com/molvqingtai/react-magic-portal/commit/8fe438d149a04d3070fe8eed725d7932492da01c))
14
+
1
15
  ## [1.2.1](https://github.com/molvqingtai/react-magic-portal/compare/v1.2.0...v1.2.1) (2025-10-10)
2
16
 
3
17
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ React Magic Portal solves these challenges by automatically detecting when targe
26
26
  ## Features
27
27
 
28
28
  - **Dynamic Anchor Detection** - Automatically detects when target elements appear or disappear in the DOM
29
- - **Multiple Positioning Options** - Support for `append`, `prepend`, `before`, and `after` positioning
29
+ - **Multiple Positioning Options** - Support for `last`, `first`, `before`, and `after` positioning
30
30
  - **Flexible Anchor Selection** - Support for CSS selectors, element references, functions, and direct elements
31
31
  - **Lifecycle Callbacks** - `onMount` and `onUnmount` callbacks for portal lifecycle management
32
32
  - **TypeScript Support** - Full TypeScript support with comprehensive type definitions
@@ -77,11 +77,11 @@ function App() {
77
77
  <div>Content before target</div>
78
78
  </MagicPortal>
79
79
 
80
- <MagicPortal anchor="#target" position="prepend">
80
+ <MagicPortal anchor="#target" position="first">
81
81
  <div>Content at start of target</div>
82
82
  </MagicPortal>
83
83
 
84
- <MagicPortal anchor="#target" position="append">
84
+ <MagicPortal anchor="#target" position="last">
85
85
  <div>Content at end of target</div>
86
86
  </MagicPortal>
87
87
 
@@ -100,9 +100,9 @@ function App() {
100
100
  | Prop | Type | Default | Description |
101
101
  | ----------- | ------------------------------------------------------------------------------------------ | --------------- | ------------------------------------------------------------ |
102
102
  | `anchor` | `string \| (() => Element \| null) \| Element \| React.RefObject<Element \| null> \| null` | **Required** | The target element where the portal content will be rendered |
103
- | `position` | `'append' \| 'prepend' \| 'before' \| 'after'` | `'append'` | Position relative to the anchor element |
103
+ | `position` | `'last' \| 'first' \| 'before' \| 'after'` | `'last'` | Position relative to the anchor element |
104
104
  | `root` | `Element` | `document.body` | The root element to observe for DOM mutations |
105
- | `children` | `React.ReactElement \| React.ReactElement[]` | `undefined` | The content to render in the portal |
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
 
@@ -150,7 +150,7 @@ const elementRef = useRef(null)
150
150
 
151
151
  ### Position Options
152
152
 
153
- #### `append` (default)
153
+ #### `last` (default)
154
154
 
155
155
  Adds content inside the anchor element at the end:
156
156
 
@@ -161,7 +161,7 @@ Adds content inside the anchor element at the end:
161
161
  </div>
162
162
  ```
163
163
 
164
- #### `prepend`
164
+ #### `first`
165
165
 
166
166
  Adds content inside the anchor element at the beginning:
167
167
 
@@ -191,7 +191,7 @@ describe('MagicPortal', () => {
191
191
  document.body.appendChild(anchor)
192
192
  })
193
193
 
194
- it('should append by default', () => {
194
+ it('should last by default', () => {
195
195
  render(
196
196
  <MagicPortal anchor="#position-anchor">
197
197
  <div data-testid="portal-content">Portal Content</div>
@@ -204,9 +204,9 @@ describe('MagicPortal', () => {
204
204
  expect(anchor.contains(portalContent)).toBe(true)
205
205
  })
206
206
 
207
- it('should prepend when position is prepend', () => {
207
+ it('should first when position is first', () => {
208
208
  render(
209
- <MagicPortal anchor="#position-anchor" position="prepend">
209
+ <MagicPortal anchor="#position-anchor" position="first">
210
210
  <div data-testid="portal-content">Portal Content</div>
211
211
  </MagicPortal>
212
212
  )
@@ -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')
@@ -411,7 +389,7 @@ describe('MagicPortal', () => {
411
389
  document.body.appendChild(anchor)
412
390
 
413
391
  const { rerender } = render(
414
- <MagicPortal anchor="#position-ref-anchor" position="append">
392
+ <MagicPortal anchor="#position-ref-anchor" position="last">
415
393
  <div ref={contentRef} data-testid="portal-content">
416
394
  Portal Content
417
395
  </div>
@@ -425,7 +403,7 @@ describe('MagicPortal', () => {
425
403
  contentRef.mockClear()
426
404
 
427
405
  rerender(
428
- <MagicPortal anchor="#position-ref-anchor" position="prepend">
406
+ <MagicPortal anchor="#position-ref-anchor" position="first">
429
407
  <div ref={contentRef} data-testid="portal-content">
430
408
  Portal Content
431
409
  </div>
@@ -466,43 +444,26 @@ describe('MagicPortal', () => {
466
444
  expect(refCalls[0]?.textContent).toBe('Click me')
467
445
  })
468
446
 
469
- it('should handle single element content', () => {
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 = 'single-content-anchor'
450
+ anchor.id = 'multiple-children-anchor'
472
451
  document.body.appendChild(anchor)
473
452
 
474
453
  render(
475
- <MagicPortal anchor="#single-content-anchor">
476
- <span data-testid="single-content">Just content</span>
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(screen.getByTestId('single-content')).toBeTruthy()
481
- expect(anchor.contains(screen.getByTestId('single-content'))).toBe(true)
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
- expect(ref1).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
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', () => {
@@ -583,10 +544,10 @@ describe('MagicPortal', () => {
583
544
 
584
545
  render(
585
546
  <div>
586
- <MagicPortal anchor="#multi-anchor" position="prepend">
547
+ <MagicPortal anchor="#multi-anchor" position="first">
587
548
  <div data-testid="portal-1">Portal 1</div>
588
549
  </MagicPortal>
589
- <MagicPortal anchor="#multi-anchor" position="append">
550
+ <MagicPortal anchor="#multi-anchor" position="last">
590
551
  <div data-testid="portal-2">Portal 2</div>
591
552
  </MagicPortal>
592
553
  </div>
@@ -707,61 +668,56 @@ describe('MagicPortal', () => {
707
668
  })
708
669
 
709
670
  describe('Text Node Handling', () => {
710
- it('should not render pure text content', () => {
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 = 'text-only-anchor'
674
+ anchor.id = 'fragment-anchor'
713
675
  document.body.appendChild(anchor)
714
676
 
715
677
  render(
716
- // @ts-expect-error - testing that text nodes are not rendered
717
- <MagicPortal anchor="#text-only-anchor">Just plain text</MagicPortal>
678
+ <MagicPortal anchor="#fragment-anchor">
679
+ <>
680
+ <div>Child 1</div>
681
+ <div>Child 2</div>
682
+ </>
683
+ </MagicPortal>
718
684
  )
719
685
 
720
- // Anchor should remain empty since text nodes are not rendered
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 mixed text and element content (only elements)', () => {
698
+ it('should not render pure text content', () => {
726
699
  const anchor = document.createElement('div')
727
- anchor.id = 'mixed-text-anchor'
700
+ anchor.id = 'text-only-anchor'
728
701
  document.body.appendChild(anchor)
729
702
 
730
703
  render(
731
- // @ts-expect-error - testing mixed content behavior
732
- <MagicPortal anchor="#mixed-text-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
- // Only the div element should be rendered, text nodes should be ignored
741
- expect(screen.getByTestId('element-content')).toBeTruthy()
742
- expect(anchor.contains(screen.getByTestId('element-content'))).toBe(true)
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 number or boolean primitives', () => {
713
+ it('should not render null children', () => {
751
714
  const anchor = document.createElement('div')
752
- anchor.id = 'primitive-anchor'
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 primitives are not rendered
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-magic-portal",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "React Portal with dynamic mounting support",
5
5
  "main": "packages/component/dist/index.js",
6
6
  "type": "module",
@@ -3,9 +3,9 @@ import React from "react";
3
3
  //#region src/index.d.ts
4
4
  interface MagicPortalProps {
5
5
  anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null;
6
- position?: 'append' | 'prepend' | 'before' | 'after';
6
+ position?: 'last' | 'first' | 'before' | 'after';
7
7
  root?: Element;
8
- children?: React.ReactElement | 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,GAGQ,KAAA,CAAM,YAHd,EAAA;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;aAAqB,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,MAAA,GAAgB,OAAA,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,useRef as i,useState as a}from"react";import{createPortal as 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?t===`prepend`||t===`append`?e:e.parentElement:null,u=(e,t)=>{if(typeof e==`function`)return e(t);e!=null&&(e.current=t)},d=(...e)=>t=>{let n=e.map(e=>u(e,t));return()=>n.forEach((t,n)=>typeof t==`function`?t():u(e[n],null))},f=({anchor:u,position:f=`append`,root:p=document.body,children:m,onMount:h,onUnmount:g})=>{let _=i(null),[v,y]=a(null),b=t(e=>{if(!e)return;let t=_.current;if(!t)return;let n=l(t,f);if(!n)return;let r=!1;switch(f){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`}[f],e)},[f]),x=e.Children.map(m,t=>{if(!e.isValidElement(t))return null;let n=s(t);return e.cloneElement(t,{ref:d(n,b)})}),S=t(()=>{_.current=c(u);let e=l(_.current,f);e&&(e.__reactWarnedAboutChildrenConflict=!0),y(e)},[u,f]);return r(()=>{S();let e=new MutationObserver(e=>{!e.flatMap(({addedNodes:e,removedNodes:t})=>[...e,...t]).some(e=>v?.contains(e))&&S()});return e.observe(p,{childList:!0,subtree:!0}),()=>e.disconnect()},[S,u,v,p]),n(()=>{if(v&&_.current)return h?.(_.current,v),()=>{g?.(_.current,v)}},[h,g,v]),v?o(x,v):null};f.displayName=`MagicPortal`;var p=f;export{p as default};
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===`first`||t===`last`?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=`last`,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`last`:r=e.parentElement===n&&n.lastChild===e;break;case`first`: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`,first:`afterbegin`,last:`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 | React.ReactElement[]\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 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, insertNode)\n })\n })\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 ? createPortal(nodes, container) : null\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"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"}
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?: 'last' | 'first' | '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 === 'first' || position === 'last' ? 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 = 'last',\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 'last':\n alreadyPlaced = node.parentElement === containerElement && containerElement.lastChild === node\n break\n case 'first':\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 first: 'afterbegin',\n last: '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
- position?: 'append' | 'prepend' | 'before' | 'after'
6
+ position?: 'last' | 'first' | 'before' | 'after'
7
7
  root?: Element
8
- children?: React.ReactElement | 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
  }
@@ -48,7 +48,7 @@ const resolveContainer = (anchor: Element | null, position: MagicPortalProps['po
48
48
  return null
49
49
  }
50
50
 
51
- return position === 'prepend' || position === 'append' ? anchor : anchor.parentElement
51
+ return position === 'first' || position === 'last' ? anchor : anchor.parentElement
52
52
  }
53
53
 
54
54
  /**
@@ -72,7 +72,7 @@ const mergeRef = <T extends Element | null>(...refs: (React.Ref<T> | undefined)[
72
72
 
73
73
  const MagicPortal = ({
74
74
  anchor,
75
- position = 'append',
75
+ position = 'last',
76
76
  root = document.body,
77
77
  children,
78
78
  onMount,
@@ -100,10 +100,10 @@ const MagicPortal = ({
100
100
  let alreadyPlaced = false
101
101
 
102
102
  switch (position) {
103
- case 'append':
103
+ case 'last':
104
104
  alreadyPlaced = node.parentElement === containerElement && containerElement.lastChild === node
105
105
  break
106
- case 'prepend':
106
+ case 'first':
107
107
  alreadyPlaced = node.parentElement === containerElement && containerElement.firstChild === node
108
108
  break
109
109
  case 'before':
@@ -117,8 +117,8 @@ const MagicPortal = ({
117
117
  if (!alreadyPlaced) {
118
118
  const positionMap = {
119
119
  before: 'beforebegin',
120
- prepend: 'afterbegin',
121
- append: 'beforeend',
120
+ first: 'afterbegin',
121
+ last: 'beforeend',
122
122
  after: 'afterend'
123
123
  } as const
124
124
  anchorElement.insertAdjacentElement(positionMap[position], node)
@@ -127,15 +127,30 @@ const MagicPortal = ({
127
127
  [position]
128
128
  )
129
129
 
130
- const nodes = React.Children.map(children, (item) => {
131
- if (!React.isValidElement(item)) {
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
- const originalRef = getElementRef(item)
135
- return React.cloneElement(item as React.ReactElement<any>, {
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(nodes, container) : null
196
+ return container && child ? createPortal(child, container) : null
182
197
  }
183
198
 
184
199
  MagicPortal.displayName = 'MagicPortal'