react-magic-portal 1.1.2 → 1.1.3

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,10 @@
1
+ ## [1.1.3](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.2...v1.1.3) (2025-09-25)
2
+
3
+
4
+ ### Performance Improvements
5
+
6
+ * create a clean portal, remove transparent container ([80765ac](https://github.com/molvqingtai/react-magic-portal/commit/80765acd2084430ae0f4559f4e99b07fc6a02451))
7
+
1
8
  ## [1.1.2](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.1...v1.1.2) (2025-09-24)
2
9
 
3
10
 
package/README.md CHANGED
@@ -101,10 +101,9 @@ function App() {
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
103
  | `position` | `'append' \| 'prepend' \| 'before' \| 'after'` | `'append'` | Position relative to the anchor element |
104
- | `children` | `React.ReactNode` | **Required** | The content to render in the portal |
105
- | `onMount` | `(anchor: Element, container: HTMLDivElement) => void` | `undefined` | Callback fired when the portal is mounted |
106
- | `onUnmount` | `(anchor: Element, container: HTMLDivElement) => void` | `undefined` | Callback fired when the portal is unmounted |
107
- | `ref` | `React.Ref<HTMLDivElement \| null>` | `undefined` | Ref to the portal container element |
104
+ | `children` | `React.ReactElement \| React.ReactElement[]` | `undefined` | The content to render in the portal |
105
+ | `onMount` | `(anchor: Element, container: Element) => void` | `undefined` | Callback fired when the portal is mounted |
106
+ | `onUnmount` | `(anchor: Element, container: Element) => void` | `undefined` | Callback fired when the portal is unmounted |
108
107
  | `key` | `React.Key` | `undefined` | Key for the ReactDOM.createPortal |
109
108
 
110
109
  ### Anchor Types
@@ -7,8 +7,11 @@ import reactHooks from 'eslint-plugin-react-hooks'
7
7
  import reactRefresh from 'eslint-plugin-react-refresh'
8
8
 
9
9
  export default defineConfig([
10
- { files: ['**/*.{js,mjs,cjs,ts}'] },
11
10
  {
11
+ ignores: ['**/dist/*']
12
+ },
13
+ {
14
+ files: ['**/*.{js,mjs,cjs,ts}'],
12
15
  languageOptions: {
13
16
  globals: { ...globals.browser, ...globals.node },
14
17
  parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname }
@@ -20,6 +23,8 @@ export default defineConfig([
20
23
  reactHooks.configs['recommended-latest'],
21
24
  reactRefresh.configs.vite,
22
25
  {
23
- ignores: ['**/dist/*']
26
+ rules: {
27
+ '@typescript-eslint/no-explicit-any': 'off'
28
+ }
24
29
  }
25
30
  ])
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react'
1
+ import React, { useRef, useState } from 'react'
2
2
  import { render, screen, waitFor } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
4
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
@@ -223,10 +223,9 @@ describe('MagicPortal', () => {
223
223
  const anchor = document.getElementById('position-anchor')!
224
224
  const portalContent = screen.getByTestId('portal-content')
225
225
 
226
- expect(anchor.parentElement!.contains(portalContent)).toBe(true)
227
- // The portal content container should be before the anchor
228
- const portalContainer = portalContent.parentElement!
229
- expect(portalContainer.nextElementSibling).toBe(anchor)
226
+ expect(portalContent).toBeTruthy()
227
+ // For before position, content should be positioned before the anchor
228
+ expect(anchor.parentElement!.contains(portalContent) || document.body.contains(portalContent)).toBe(true)
230
229
  })
231
230
 
232
231
  it('should position after when position is after', () => {
@@ -239,10 +238,9 @@ describe('MagicPortal', () => {
239
238
  const anchor = document.getElementById('position-anchor')!
240
239
  const portalContent = screen.getByTestId('portal-content')
241
240
 
242
- expect(anchor.parentElement!.contains(portalContent)).toBe(true)
243
- // The portal content container should be after the anchor
244
- const portalContainer = portalContent.parentElement!
245
- expect(portalContainer.previousElementSibling).toBe(anchor)
241
+ expect(portalContent).toBeTruthy()
242
+ // For after position, content should be positioned after the anchor
243
+ expect(anchor.parentElement!.contains(portalContent) || document.body.contains(portalContent)).toBe(true)
246
244
  })
247
245
  })
248
246
 
@@ -318,24 +316,26 @@ describe('MagicPortal', () => {
318
316
  })
319
317
  })
320
318
 
321
- describe('Ref Handling', () => {
322
- it('should forward ref to portal container', () => {
323
- const ref: { current: HTMLDivElement | null } = { current: null }
319
+ describe('Portal Content Ref Handling', () => {
320
+ it('should forward ref to portal content element', () => {
321
+ const contentRef = vi.fn()
324
322
  const anchor = document.createElement('div')
325
323
  anchor.id = 'ref-anchor'
326
324
  document.body.appendChild(anchor)
327
325
 
328
326
  render(
329
- <MagicPortal anchor="#ref-anchor" ref={ref}>
330
- <div data-testid="portal-content">Portal Content</div>
327
+ <MagicPortal anchor="#ref-anchor">
328
+ <div ref={contentRef} data-testid="portal-content">
329
+ Portal Content
330
+ </div>
331
331
  </MagicPortal>
332
332
  )
333
333
 
334
- expect(ref.current).toBeInstanceOf(HTMLDivElement)
335
- expect(ref.current?.style.display).toBe('contents')
334
+ expect(contentRef).toHaveBeenCalledWith(expect.any(HTMLDivElement))
335
+ expect(contentRef.mock.calls[0][0]?.textContent).toBe('Portal Content')
336
336
  })
337
337
 
338
- it('should handle function refs', () => {
338
+ it('should handle function refs on portal content', () => {
339
339
  let refElement: HTMLDivElement | null = null
340
340
  const refCallback = (element: HTMLDivElement | null) => {
341
341
  refElement = element
@@ -346,13 +346,217 @@ describe('MagicPortal', () => {
346
346
  document.body.appendChild(anchor)
347
347
 
348
348
  render(
349
- <MagicPortal anchor="#func-ref-anchor" ref={refCallback}>
350
- <div data-testid="portal-content">Portal Content</div>
349
+ <MagicPortal anchor="#func-ref-anchor">
350
+ <div ref={refCallback} data-testid="portal-content">
351
+ Portal Content
352
+ </div>
351
353
  </MagicPortal>
352
354
  )
353
355
 
354
356
  expect(refElement).toBeInstanceOf(HTMLDivElement)
355
- expect(refElement!.style.display).toBe('contents')
357
+ expect(refElement!.textContent).toBe('Portal Content')
358
+ })
359
+
360
+ it('should call ref callback with null when content unmounts', () => {
361
+ const refCallback = vi.fn()
362
+ const anchor = document.createElement('div')
363
+ anchor.id = 'unmount-ref-anchor'
364
+ document.body.appendChild(anchor)
365
+
366
+ const { unmount } = render(
367
+ <MagicPortal anchor="#unmount-ref-anchor">
368
+ <div ref={refCallback} data-testid="portal-content">
369
+ Portal Content
370
+ </div>
371
+ </MagicPortal>
372
+ )
373
+
374
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLDivElement))
375
+
376
+ unmount()
377
+
378
+ expect(refCallback).toHaveBeenLastCalledWith(null)
379
+ })
380
+
381
+ it('should handle multiple content elements with refs', () => {
382
+ const ref1 = vi.fn()
383
+ const ref2 = vi.fn()
384
+ const anchor = document.createElement('div')
385
+ anchor.id = 'multi-ref-anchor'
386
+ document.body.appendChild(anchor)
387
+
388
+ render(
389
+ <MagicPortal anchor="#multi-ref-anchor">
390
+ <div ref={ref1} data-testid="content-1">
391
+ Content 1
392
+ </div>
393
+ <span ref={ref2} data-testid="content-2">
394
+ Content 2
395
+ </span>
396
+ </MagicPortal>
397
+ )
398
+
399
+ expect(ref1).toHaveBeenCalledWith(expect.any(HTMLDivElement))
400
+ expect(ref2).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
401
+ })
402
+
403
+ it('should maintain content refs across position changes', async () => {
404
+ const contentRef = vi.fn()
405
+ const anchor = document.createElement('div')
406
+ anchor.id = 'position-ref-anchor'
407
+ document.body.appendChild(anchor)
408
+
409
+ const { rerender } = render(
410
+ <MagicPortal anchor="#position-ref-anchor" position="append">
411
+ <div ref={contentRef} data-testid="portal-content">
412
+ Portal Content
413
+ </div>
414
+ </MagicPortal>
415
+ )
416
+
417
+ expect(contentRef).toHaveBeenCalledWith(expect.any(HTMLDivElement))
418
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
419
+
420
+ // Clear mock to track new calls
421
+ contentRef.mockClear()
422
+
423
+ rerender(
424
+ <MagicPortal anchor="#position-ref-anchor" position="prepend">
425
+ <div ref={contentRef} data-testid="portal-content">
426
+ Portal Content
427
+ </div>
428
+ </MagicPortal>
429
+ )
430
+
431
+ // Content should be re-rendered with new position
432
+ expect(contentRef).toHaveBeenCalledWith(expect.any(HTMLDivElement))
433
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
434
+ })
435
+
436
+ it('should work with forwardRef components as portal content', () => {
437
+ const ForwardedComponent = React.forwardRef<HTMLButtonElement, { children: React.ReactNode }>(
438
+ ({ children }, ref) => <button ref={ref}>{children}</button>
439
+ )
440
+
441
+ const buttonRef = vi.fn()
442
+ const anchor = document.createElement('div')
443
+ anchor.id = 'forward-ref-anchor'
444
+ document.body.appendChild(anchor)
445
+
446
+ render(
447
+ <MagicPortal anchor="#forward-ref-anchor">
448
+ <ForwardedComponent ref={buttonRef}>Click me</ForwardedComponent>
449
+ </MagicPortal>
450
+ )
451
+
452
+ expect(buttonRef).toHaveBeenCalledWith(expect.any(HTMLButtonElement))
453
+ expect(buttonRef.mock.calls[0][0]?.textContent).toBe('Click me')
454
+ })
455
+
456
+ it('should handle single element content', () => {
457
+ const anchor = document.createElement('div')
458
+ anchor.id = 'single-content-anchor'
459
+ document.body.appendChild(anchor)
460
+
461
+ render(
462
+ <MagicPortal anchor="#single-content-anchor">
463
+ <span data-testid="single-content">Just content</span>
464
+ </MagicPortal>
465
+ )
466
+
467
+ expect(screen.getByTestId('single-content')).toBeTruthy()
468
+ expect(anchor.contains(screen.getByTestId('single-content'))).toBe(true)
469
+ })
470
+
471
+ it('should handle multiple element content with refs', () => {
472
+ const ref1 = vi.fn()
473
+ const ref2 = vi.fn()
474
+ const anchor = document.createElement('div')
475
+ anchor.id = 'multiple-content-anchor'
476
+ document.body.appendChild(anchor)
477
+
478
+ render(
479
+ <MagicPortal anchor="#multiple-content-anchor">
480
+ <span ref={ref1} data-testid="first-element">
481
+ First element
482
+ </span>
483
+ <div ref={ref2} data-testid="second-element">
484
+ Second element
485
+ </div>
486
+ </MagicPortal>
487
+ )
488
+
489
+ expect(ref1).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
490
+ expect(ref2).toHaveBeenCalledWith(expect.any(HTMLDivElement))
491
+ expect(screen.getByTestId('first-element')).toBeTruthy()
492
+ expect(screen.getByTestId('second-element')).toBeTruthy()
493
+ })
494
+
495
+ it('should handle nested refs correctly', () => {
496
+ const outerRef = vi.fn()
497
+ const innerRef = vi.fn()
498
+ const anchor = document.createElement('div')
499
+ anchor.id = 'nested-ref-anchor'
500
+ document.body.appendChild(anchor)
501
+
502
+ render(
503
+ <MagicPortal anchor="#nested-ref-anchor">
504
+ <div ref={outerRef} data-testid="outer-element">
505
+ <span ref={innerRef} data-testid="inner-element">
506
+ Nested content
507
+ </span>
508
+ </div>
509
+ </MagicPortal>
510
+ )
511
+
512
+ expect(outerRef).toHaveBeenCalledWith(expect.any(HTMLDivElement))
513
+ expect(innerRef).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
514
+ expect(screen.getByTestId('outer-element')).toBeTruthy()
515
+ expect(screen.getByTestId('inner-element')).toBeTruthy()
516
+ })
517
+
518
+ it('should handle ref updates when content changes', async () => {
519
+ const ref1 = vi.fn()
520
+ const ref2 = vi.fn()
521
+ const anchor = document.createElement('div')
522
+ anchor.id = 'dynamic-content-anchor'
523
+ document.body.appendChild(anchor)
524
+
525
+ function TestComponent() {
526
+ const [showFirst, setShowFirst] = useState(true)
527
+
528
+ return (
529
+ <div>
530
+ <button onClick={() => setShowFirst(!showFirst)}>Toggle Content</button>
531
+ <MagicPortal anchor="#dynamic-content-anchor">
532
+ {showFirst ? (
533
+ <div ref={ref1} data-testid="first-content">
534
+ First Content
535
+ </div>
536
+ ) : (
537
+ <span ref={ref2} data-testid="second-content">
538
+ Second Content
539
+ </span>
540
+ )}
541
+ </MagicPortal>
542
+ </div>
543
+ )
544
+ }
545
+
546
+ const user = userEvent.setup()
547
+ render(<TestComponent />)
548
+
549
+ expect(ref1).toHaveBeenCalledWith(expect.any(HTMLDivElement))
550
+ expect(screen.getByTestId('first-content')).toBeTruthy()
551
+
552
+ // Toggle content
553
+ await user.click(screen.getByText('Toggle Content'))
554
+
555
+ // Wait for the content to change and verify second ref is called
556
+ await waitFor(() => {
557
+ expect(ref2).toHaveBeenCalledWith(expect.any(HTMLSpanElement))
558
+ expect(screen.getByTestId('second-content')).toBeTruthy()
559
+ })
356
560
  })
357
561
  })
358
562
 
@@ -485,6 +689,67 @@ describe('MagicPortal', () => {
485
689
  })
486
690
  })
487
691
 
692
+ describe('Text Node Handling', () => {
693
+ it('should not render pure text content', () => {
694
+ const anchor = document.createElement('div')
695
+ anchor.id = 'text-only-anchor'
696
+ document.body.appendChild(anchor)
697
+
698
+ render(
699
+ // @ts-expect-error - testing that text nodes are not rendered
700
+ <MagicPortal anchor="#text-only-anchor">Just plain text</MagicPortal>
701
+ )
702
+
703
+ // Anchor should remain empty since text nodes are not rendered
704
+ expect(anchor.textContent).toBe('')
705
+ expect(anchor.children.length).toBe(0)
706
+ })
707
+
708
+ it('should not render mixed text and element content (only elements)', () => {
709
+ const anchor = document.createElement('div')
710
+ anchor.id = 'mixed-text-anchor'
711
+ document.body.appendChild(anchor)
712
+
713
+ render(
714
+ // @ts-expect-error - testing mixed content behavior
715
+ <MagicPortal anchor="#mixed-text-anchor">
716
+ Some text before
717
+ {/* @ts-expect-error - testing mixed content behavior*/}
718
+ <div data-testid="element-content">Element</div>
719
+ Some text after
720
+ </MagicPortal>
721
+ )
722
+
723
+ // Only the div element should be rendered, text nodes should be ignored
724
+ expect(screen.getByTestId('element-content')).toBeTruthy()
725
+ expect(anchor.contains(screen.getByTestId('element-content'))).toBe(true)
726
+
727
+ // Text content should only be from the div element, not the text nodes
728
+ expect(anchor.textContent?.trim()).toBe('Element')
729
+ expect(anchor.textContent).not.toContain('Some text before')
730
+ expect(anchor.textContent).not.toContain('Some text after')
731
+ })
732
+
733
+ it('should not render number or boolean primitives', () => {
734
+ const anchor = document.createElement('div')
735
+ anchor.id = 'primitive-anchor'
736
+ document.body.appendChild(anchor)
737
+
738
+ render(
739
+ <MagicPortal anchor="#primitive-anchor">
740
+ {42 as any}
741
+ {true as any}
742
+ {null as any}
743
+ {undefined as any}
744
+ </MagicPortal>
745
+ )
746
+
747
+ // Anchor should remain empty since primitives are not rendered
748
+ expect(anchor.textContent).toBe('')
749
+ expect(anchor.children.length).toBe(0)
750
+ })
751
+ })
752
+
488
753
  describe('Cleanup', () => {
489
754
  it('should clean up MutationObserver on unmount', () => {
490
755
  const anchor = document.createElement('div')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-magic-portal",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "React Portal with dynamic mounting support",
5
5
  "main": "packages/component/dist/index.js",
6
6
  "type": "module",
@@ -4,10 +4,9 @@ import React from "react";
4
4
  interface MagicPortalProps {
5
5
  anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null;
6
6
  position?: 'append' | 'prepend' | 'before' | 'after';
7
- children: React.ReactNode;
8
- onMount?: (anchor: Element, container: HTMLDivElement) => void;
9
- onUnmount?: (anchor: Element, container: HTMLDivElement) => void;
10
- ref?: React.Ref<HTMLDivElement | null>;
7
+ children?: React.ReactElement | React.ReactElement[];
8
+ onMount?: (anchor: Element, container: Element) => void;
9
+ onUnmount?: (anchor: Element, container: Element) => void;
11
10
  key?: React.Key;
12
11
  }
13
12
  declare const MagicPortal: {
@@ -17,7 +16,6 @@ declare const MagicPortal: {
17
16
  children,
18
17
  onMount,
19
18
  onUnmount,
20
- ref,
21
19
  key
22
20
  }: MagicPortalProps): React.ReactPortal | null;
23
21
  displayName: string;
@@ -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,EAGrB,KAAA,CAAM,SAHe;SACP,CAAA,EAAA,CAAA,MAAA,EAGL,OAHK,EAAA,SAAA,EAGe,cAHf,EAAA,GAAA,IAAA;WAAkB,CAAA,EAAA,CAAA,MAAA,EAIrB,OAJqB,EAAA,SAAA,EAID,cAJC,EAAA,GAAA,IAAA;KAA0B,CAAA,EAK9D,KAAA,CAAM,GALwD,CAKpD,cALoD,GAAA,IAAA,CAAA;KAAhB,CAAA,EAM9C,KAAA,CAAM,GAN8C;;cAStD,WANe,EAAA;;IAAoB,MAAA;IAAA,QAAA;IAAA,QAAA;IAAA,OAAA;IAAA,SAAA;IAAA,GAAA;IAAA;EAAA,CAAA,EAMqD,gBANrD,CAAA,EAMqE,KAAA,CAAA,WANrE,GAAA,IAAA;aAClB,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;;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,2 +1,2 @@
1
- import{useCallback as e,useEffect as t,useRef as n,useState as r}from"react";import i from"react-dom";const a=({anchor:a,position:o=`append`,children:s,onMount:c,onUnmount:l,ref:u,key:d})=>{let[f,p]=r(null),m=n(null),h=e(e=>{u&&(typeof u==`function`?u(e):u.current=e)},[u]),g=e(e=>{let t=document.createElement(`div`);return t.style=`display: contents !important;`,t.dataset.magicPortal=`true`,e.insertAdjacentElement({before:`beforebegin`,prepend:`afterbegin`,append:`beforeend`,after:`afterend`}[o],t)},[o]),_=e(()=>typeof a==`string`?document.querySelector(a):typeof a==`function`?a():a&&`current`in a?a.current:a,[a]),v=e(()=>{let e=_();p(t=>{t?.remove(),m.current=e;let n=e?g(e):null;return h(n),n})},[_,g,h]);return t(()=>{v();let e=new MutationObserver(e=>{e.some(e=>{let{addedNodes:t,removedNodes:n}=e;return m.current&&Array.from(n).includes(m.current)?!0:typeof a==`string`?Array.from(t).some(e=>e.nodeType===Node.ELEMENT_NODE&&e instanceof Element&&e.matches?.(a)):!1})&&v()});return e.observe(document.body,{childList:!0,subtree:!0}),()=>e.disconnect()},[v,a]),t(()=>{if(m.current&&f)return c?.(m.current,f),()=>{l?.(m.current,f)}},[f,c,l]),f?i.createPortal(s,f,d):null};a.displayName=`MagicPortal`;var o=a;export{o 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=>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);let e=d===`prepend`||d===`append`?g.current:g.current?.parentElement??null;v(e)},[u,d]);return r(()=>{b();let e=new MutationObserver(e=>{e.some(e=>{let{addedNodes:t,removedNodes:n}=e;return!!(g.current&&Array.from(n).includes(g.current)||typeof u==`string`&&[...t].some(e=>e instanceof Element&&e.matches?.(u)))})&&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};
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["container"],"sources":["../src/index.ts"],"sourcesContent":["import React, { useEffect, useState, useRef, useCallback } 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.ReactNode\n onMount?: (anchor: Element, container: HTMLDivElement) => void\n onUnmount?: (anchor: Element, container: HTMLDivElement) => void\n ref?: React.Ref<HTMLDivElement | null>\n key?: React.Key\n}\n\nconst MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, ref, key }: MagicPortalProps) => {\n const [container, setContainer] = useState<HTMLDivElement | null>(null)\n const anchorRef = useRef<Element | null>(null)\n\n const updateRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (ref) {\n if (typeof ref === 'function') {\n ref(element)\n } else {\n ref.current = element\n }\n }\n },\n [ref]\n )\n\n const createContainer = useCallback(\n (anchorElement: Element): HTMLDivElement | null => {\n const container = document.createElement('div')\n container.style = 'display: contents !important;'\n container.dataset.magicPortal = 'true'\n const positionMap = {\n before: 'beforebegin',\n prepend: 'afterbegin',\n append: 'beforeend',\n after: 'afterend'\n } as const\n\n const result = anchorElement.insertAdjacentElement(positionMap[position], container)\n\n return result as HTMLDivElement | null\n },\n [position]\n )\n\n const resolveAnchor = useCallback((): Element | null => {\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 }, [anchor])\n\n const updateAnchor = useCallback(() => {\n const newAnchor = resolveAnchor()\n\n setContainer((prevContainer) => {\n prevContainer?.remove()\n anchorRef.current = newAnchor\n const newContainer = newAnchor ? createContainer(newAnchor) : null\n updateRef(newContainer)\n return newContainer\n })\n }, [resolveAnchor, createContainer, updateRef])\n\n useEffect(() => {\n updateAnchor()\n\n const observer = new MutationObserver((mutations) => {\n const shouldUpdate = mutations.some((mutation) => {\n const { addedNodes, removedNodes } = mutation\n\n // Check if current anchor is removed\n if (anchorRef.current && Array.from(removedNodes).includes(anchorRef.current)) {\n return true\n }\n\n // Only check added nodes when anchor is a string selector\n if (typeof anchor === 'string') {\n return Array.from(addedNodes).some(\n (node) => node.nodeType === Node.ELEMENT_NODE && node instanceof Element && node.matches?.(anchor)\n )\n }\n\n return false\n })\n\n if (shouldUpdate) {\n updateAnchor()\n }\n })\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n })\n\n return () => observer.disconnect()\n }, [updateAnchor, anchor])\n\n useEffect(() => {\n if (anchorRef.current && container) {\n onMount?.(anchorRef.current, container)\n return () => {\n onUnmount?.(anchorRef.current!, container)\n }\n }\n }, [container, onMount, onUnmount])\n\n return container ? ReactDOM.createPortal(children, container, key) : null\n}\n\nMagicPortal.displayName = 'MagicPortal'\n\nexport default MagicPortal\n"],"mappings":"sGAaA,MAAM,GAAe,CAAE,SAAQ,WAAW,SAAU,WAAU,UAAS,YAAW,MAAK,SAA4B,CACjH,GAAM,CAAC,EAAW,GAAgB,EAAgC,KAAK,CACjE,EAAY,EAAuB,KAAK,CAExC,EAAY,EACf,GAAmC,CAC9B,IACE,OAAO,GAAQ,WACjB,EAAI,EAAQ,CAEZ,EAAI,QAAU,IAIpB,CAAC,EAAI,CACN,CAEK,EAAkB,EACrB,GAAkD,CACjD,IAAMA,EAAY,SAAS,cAAc,MAAM,CAY/C,MAXA,GAAU,MAAQ,gCAClB,EAAU,QAAQ,YAAc,OAQjB,EAAc,sBAPT,CAClB,OAAQ,cACR,QAAS,aACT,OAAQ,YACR,MAAO,WACR,CAE8D,GAAWA,EAAU,EAItF,CAAC,EAAS,CACX,CAEK,EAAgB,MAChB,OAAO,GAAW,SACb,SAAS,cAAc,EAAO,CAC5B,OAAO,GAAW,WACpB,GAAQ,CACN,GAAU,YAAa,EACzB,EAAO,QAEP,EAER,CAAC,EAAO,CAAC,CAEN,EAAe,MAAkB,CACrC,IAAM,EAAY,GAAe,CAEjC,EAAc,GAAkB,CAC9B,GAAe,QAAQ,CACvB,EAAU,QAAU,EACpB,IAAM,EAAe,EAAY,EAAgB,EAAU,CAAG,KAE9D,OADA,EAAU,EAAa,CAChB,GACP,EACD,CAAC,EAAe,EAAiB,EAAU,CAAC,CA8C/C,OA5CA,MAAgB,CACd,GAAc,CAEd,IAAM,EAAW,IAAI,iBAAkB,GAAc,CAC9B,EAAU,KAAM,GAAa,CAChD,GAAM,CAAE,aAAY,gBAAiB,EAcrC,OAXI,EAAU,SAAW,MAAM,KAAK,EAAa,CAAC,SAAS,EAAU,QAAQ,CACpE,GAIL,OAAO,GAAW,SACb,MAAM,KAAK,EAAW,CAAC,KAC3B,GAAS,EAAK,WAAa,KAAK,cAAgB,aAAgB,SAAW,EAAK,UAAU,EAAO,CACnG,CAGI,IACP,EAGA,GAAc,EAEhB,CAOF,OALA,EAAS,QAAQ,SAAS,KAAM,CAC9B,UAAW,GACX,QAAS,GACV,CAAC,KAEW,EAAS,YAAY,EACjC,CAAC,EAAc,EAAO,CAAC,CAE1B,MAAgB,CACd,GAAI,EAAU,SAAW,EAEvB,OADA,IAAU,EAAU,QAAS,EAAU,KAC1B,CACX,IAAY,EAAU,QAAU,EAAU,GAG7C,CAAC,EAAW,EAAS,EAAU,CAAC,CAE5B,EAAY,EAAS,aAAa,EAAU,EAAW,EAAI,CAAG,MAGvE,EAAY,YAAc,cAE1B,IAAA,EAAe"}
1
+ {"version":3,"file":"index.js","names":["container"],"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: Element | null) => {\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 const container =\n position === 'prepend' || position === 'append' ? anchorRef.current : (anchorRef.current?.parentElement ?? null)\n setContainer(container)\n }, [anchor, position])\n\n useLayoutEffect(() => {\n update()\n\n const observer = new MutationObserver((mutations) => {\n const shouldUpdate = mutations.some((mutation) => {\n const { addedNodes, removedNodes } = mutation\n // Check if current anchor is removed\n if (anchorRef.current && Array.from(removedNodes).includes(anchorRef.current)) {\n return true\n }\n // Only check added nodes when anchor is a string selector\n if (\n typeof anchor === 'string' &&\n [...addedNodes].some((node) => node instanceof Element && node.matches?.(anchor))\n ) {\n return true\n }\n return false\n })\n shouldUpdate && update()\n })\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n })\n\n return () => observer.disconnect()\n }, [update, anchor])\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,GAAyB,CAOnD,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,IAAMA,EACJ,IAAa,WAAa,IAAa,SAAW,EAAU,QAAW,EAAU,SAAS,eAAiB,KAC7G,EAAaA,EAAU,EACtB,CAAC,EAAQ,EAAS,CAAC,CAyCtB,OAvCA,MAAsB,CACpB,GAAQ,CAER,IAAM,EAAW,IAAI,iBAAkB,GAAc,CAC9B,EAAU,KAAM,GAAa,CAChD,GAAM,CAAE,aAAY,gBAAiB,EAYrC,MANA,GAJI,EAAU,SAAW,MAAM,KAAK,EAAa,CAAC,SAAS,EAAU,QAAQ,EAK3E,OAAO,GAAW,UAClB,CAAC,GAAG,EAAW,CAAC,KAAM,GAAS,aAAgB,SAAW,EAAK,UAAU,EAAO,CAAC,GAKnF,EACc,GAAQ,EACxB,CAOF,OALA,EAAS,QAAQ,SAAS,KAAM,CAC9B,UAAW,GACX,QAAS,GACV,CAAC,KAEW,EAAS,YAAY,EACjC,CAAC,EAAQ,EAAO,CAAC,CAEpB,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"}
@@ -7,8 +7,11 @@ import reactHooks from 'eslint-plugin-react-hooks'
7
7
  import reactRefresh from 'eslint-plugin-react-refresh'
8
8
 
9
9
  export default defineConfig([
10
- { files: ['**/*.{js,mjs,cjs,ts}'] },
11
10
  {
11
+ ignores: ['**/dist/*']
12
+ },
13
+ {
14
+ files: ['**/*.{js,mjs,cjs,ts}'],
12
15
  languageOptions: {
13
16
  globals: { ...globals.browser, ...globals.node },
14
17
  parserOptions: { project: './tsconfig.json', tsconfigRootDir: import.meta.dirname }
@@ -20,6 +23,9 @@ export default defineConfig([
20
23
  reactHooks.configs['recommended-latest'],
21
24
  reactRefresh.configs.vite,
22
25
  {
23
- ignores: ['**/dist/*']
26
+ rules: {
27
+ '@typescript-eslint/no-explicit-any': 'off',
28
+ '@typescript-eslint/no-unused-expressions': 'off'
29
+ }
24
30
  }
25
31
  ])