react-magic-portal 1.1.1 → 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 +15 -0
- package/README.md +3 -4
- package/__tests__/eslint.config.ts +7 -2
- package/__tests__/src/{MagicPortal.test.tsx → magic-portal.test.tsx} +285 -20
- package/package.json +1 -1
- package/packages/component/dist/index.d.ts +3 -5
- package/packages/component/dist/index.d.ts.map +1 -1
- package/packages/component/dist/index.js +1 -1
- package/packages/component/dist/index.js.map +1 -1
- package/packages/component/eslint.config.ts +8 -2
- package/packages/component/src/index.ts +83 -76
- package/packages/example/dist/assets/index-BXJSx7fv.js +49 -0
- package/packages/example/dist/assets/index-CDQ6J_Ti.css +1 -0
- package/packages/example/dist/index.html +2 -2
- package/packages/example/eslint.config.ts +5 -5
- package/packages/example/src/App.css +9 -22
- package/packages/example/src/App.tsx +5 -5
- package/packages/example/dist/assets/index-BSN9W4mP.js +0 -49
- package/packages/example/dist/assets/index-DWWbQwSg.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
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
|
+
|
|
8
|
+
## [1.1.2](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.1...v1.1.2) (2025-09-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance Improvements
|
|
12
|
+
|
|
13
|
+
* add data-magic-portal attribute ([3c3143b](https://github.com/molvqingtai/react-magic-portal/commit/3c3143be2b5d539fdbbba6bb73a006bfd133e679))
|
|
14
|
+
* add data-magic-portal attribute ([74c132e](https://github.com/molvqingtai/react-magic-portal/commit/74c132ecfc37b56102623e75ad2351b167cad7a0))
|
|
15
|
+
|
|
1
16
|
## [1.1.1](https://github.com/molvqingtai/react-magic-portal/compare/v1.1.0...v1.1.1) (2025-09-24)
|
|
2
17
|
|
|
3
18
|
|
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.
|
|
105
|
-
| `onMount` | `(anchor: Element, container:
|
|
106
|
-
| `onUnmount` | `(anchor: Element, container:
|
|
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
|
-
|
|
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(
|
|
227
|
-
//
|
|
228
|
-
|
|
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(
|
|
243
|
-
//
|
|
244
|
-
|
|
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
|
|
323
|
-
const
|
|
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"
|
|
330
|
-
<div data-testid="portal-content">
|
|
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(
|
|
335
|
-
expect(
|
|
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"
|
|
350
|
-
<div data-testid="portal-content">
|
|
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!.
|
|
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
|
@@ -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
|
|
8
|
-
onMount?: (anchor: Element, container:
|
|
9
|
-
onUnmount?: (anchor: Element, container:
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
26
|
+
rules: {
|
|
27
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
28
|
+
'@typescript-eslint/no-unused-expressions': 'off'
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
])
|