react-magic-portal 1.1.2 → 1.1.4

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.4](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.3...v1.1.4) (2025-09-26)
2
+
3
+
4
+ ### Performance Improvements
5
+
6
+ * add ref forward notes ([4e36266](https://github.com/molvqingtai/react-magic-portal/commit/4e36266df2724c2c463e8877fd9d424c796aa6da))
7
+
8
+ ## [1.1.3](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.2...v1.1.3) (2025-09-25)
9
+
10
+
11
+ ### Performance Improvements
12
+
13
+ * create a clean portal, remove transparent container ([80765ac](https://github.com/molvqingtai/react-magic-portal/commit/80765acd2084430ae0f4559f4e99b07fc6a02451))
14
+
1
15
  ## [1.1.2](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.1...v1.1.2) (2025-09-24)
2
16
 
3
17
 
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
@@ -191,6 +190,88 @@ Adds content as a sibling after the anchor element:
191
190
  <!-- Portal content appears here -->
192
191
  ```
193
192
 
193
+ ## Important Notes
194
+
195
+ ### React Component Ref Requirements
196
+
197
+ When using React components as children, they **must** support ref forwarding to work correctly with MagicPortal. This is because MagicPortal needs to access the underlying DOM element to position it correctly.
198
+
199
+ #### ✅ Works - Components with ref props (React 19+)
200
+
201
+ ```jsx
202
+ interface MyComponentProps {
203
+ ref?: React.Ref<HTMLDivElement>
204
+ }
205
+
206
+ const MyComponent = ({ ref, ...props }: MyComponentProps) => {
207
+ return <div ref={ref}>My Component Content</div>
208
+ }
209
+
210
+ // This will work correctly
211
+ <MagicPortal anchor="#target">
212
+ <MyComponent />
213
+ </MagicPortal>
214
+ ```
215
+
216
+ #### ✅ Works - Components with forwardRef (React 18 and earlier)
217
+
218
+ ```jsx
219
+ import { forwardRef } from 'react'
220
+
221
+ const MyComponent = forwardRef<HTMLDivElement>((props, ref) => {
222
+ return <div ref={ref}>My Component Content</div>
223
+ })
224
+
225
+ // This will work correctly
226
+ <MagicPortal anchor="#target">
227
+ <MyComponent />
228
+ </MagicPortal>
229
+ ```
230
+
231
+ #### ✅ Works - Direct DOM elements
232
+
233
+ ```jsx
234
+ // Direct DOM elements always work
235
+ <MagicPortal anchor="#target">
236
+ <div>Direct DOM element</div>
237
+ </MagicPortal>
238
+ ```
239
+
240
+ #### ❌ Won't work - Components without ref support
241
+
242
+ ```jsx
243
+ const MyComponent = () => {
244
+ return <div>My Component Content</div>
245
+ }
246
+
247
+ // This won't position correctly because ref cannot be passed to the component
248
+ <MagicPortal anchor="#target">
249
+ <MyComponent />
250
+ </MagicPortal>
251
+ ```
252
+
253
+ #### ✅ Solution - Wrap with DOM element
254
+
255
+ ```jsx
256
+ const MyComponent = () => {
257
+ return <div>My Component Content</div>
258
+ }
259
+
260
+ // Wrap the component in a DOM element
261
+ <MagicPortal anchor="#target">
262
+ <div>
263
+ <MyComponent />
264
+ </div>
265
+ </MagicPortal>
266
+ ```
267
+
268
+ ### Browser Extension Development Tips
269
+
270
+ - **Test dynamic content scenarios** - Many web pages load content asynchronously
271
+ - **Handle multiple SPA navigation** - Single Page Applications may recreate elements frequently
272
+ - **Monitor for anchor disappearance** - Use `onUnmount` callback to handle cleanup
273
+ - **Use specific selectors** - Avoid overly generic CSS selectors that might match unintended elements
274
+
194
275
  ## License
195
276
 
196
277
  MIT © [molvqingtai](https://github.com/molvqingtai)
@@ -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,jsx,mjs,cjs,ts,tsx}'],
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.4",
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) => {\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,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,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,jsx,mjs,cjs,ts,tsx}'],
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
  ])