react-magic-portal 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.commitlintrc +5 -0
  2. package/.github/dependabot.yml +11 -0
  3. package/.github/workflows/cd.yml +85 -0
  4. package/.github/workflows/ci.yml +65 -0
  5. package/.husky/commit-msg +1 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.prettierrc +6 -0
  8. package/.releaserc +13 -0
  9. package/CHANGELOG.md +13 -0
  10. package/LICENSE +21 -0
  11. package/README.md +186 -0
  12. package/__tests__/eslint.config.ts +25 -0
  13. package/__tests__/package.json +42 -0
  14. package/__tests__/src/MagicPortal.test.tsx +506 -0
  15. package/__tests__/tsconfig.json +23 -0
  16. package/__tests__/vite.config.ts +11 -0
  17. package/eslint.config.mts +16 -0
  18. package/package.json +64 -0
  19. package/packages/component/.prettierrc +6 -0
  20. package/packages/component/README.md +6 -0
  21. package/packages/component/dist/index.d.ts +27 -0
  22. package/packages/component/dist/index.d.ts.map +1 -0
  23. package/packages/component/dist/index.js +2 -0
  24. package/packages/component/dist/index.js.map +1 -0
  25. package/packages/component/eslint.config.ts +25 -0
  26. package/packages/component/package.json +70 -0
  27. package/packages/component/src/index.ts +123 -0
  28. package/packages/component/tsconfig.json +27 -0
  29. package/packages/example/.prettierrc +6 -0
  30. package/packages/example/README.md +6 -0
  31. package/packages/example/eslint.config.ts +25 -0
  32. package/packages/example/index.html +13 -0
  33. package/packages/example/package.json +32 -0
  34. package/packages/example/pnpm-lock.yaml +2098 -0
  35. package/packages/example/public/vite.svg +1 -0
  36. package/packages/example/src/App.css +332 -0
  37. package/packages/example/src/App.tsx +82 -0
  38. package/packages/example/src/assets/react.svg +1 -0
  39. package/packages/example/src/components/portal-content.tsx +33 -0
  40. package/packages/example/src/index.css +68 -0
  41. package/packages/example/src/main.tsx +13 -0
  42. package/packages/example/src/vite-env.d.ts +1 -0
  43. package/packages/example/tsconfig.json +25 -0
  44. package/packages/example/vite.config.ts +7 -0
  45. package/pnpm-workspace.yaml +3 -0
@@ -0,0 +1,506 @@
1
+ import { useRef, useState } from 'react'
2
+ import { render, screen, waitFor } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
5
+ import MagicPortal from 'react-magic-portal'
6
+
7
+ describe('MagicPortal', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = ''
10
+ })
11
+
12
+ afterEach(() => {
13
+ vi.clearAllMocks()
14
+ })
15
+
16
+ describe('Basic Functionality', () => {
17
+ it('should render children in portal when anchor exists', () => {
18
+ // Setup anchor element
19
+ const anchor = document.createElement('div')
20
+ anchor.id = 'test-anchor'
21
+ document.body.appendChild(anchor)
22
+
23
+ render(
24
+ <MagicPortal anchor="#test-anchor">
25
+ <div data-testid="portal-content">Portal Content</div>
26
+ </MagicPortal>
27
+ )
28
+
29
+ const portalContent = screen.getByTestId('portal-content')
30
+ expect(portalContent).toBeTruthy()
31
+ expect(anchor.contains(portalContent)).toBe(true)
32
+ })
33
+
34
+ it('should not render children when anchor does not exist', () => {
35
+ render(
36
+ <MagicPortal anchor="#non-existent">
37
+ <div data-testid="portal-content">Portal Content</div>
38
+ </MagicPortal>
39
+ )
40
+
41
+ expect(screen.queryByTestId('portal-content')).toBeNull()
42
+ })
43
+
44
+ it('should render children when anchor appears later', async () => {
45
+ function TestComponent() {
46
+ const [showAnchor, setShowAnchor] = useState(false)
47
+
48
+ return (
49
+ <div>
50
+ <button onClick={() => setShowAnchor(true)}>Show Anchor</button>
51
+ {showAnchor && <div id="magic-anchor">Anchor</div>}
52
+ <MagicPortal anchor="#magic-anchor">
53
+ <div data-testid="portal-content">Portal Content</div>
54
+ </MagicPortal>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ const user = userEvent.setup()
60
+ render(<TestComponent />)
61
+
62
+ expect(screen.queryByTestId('portal-content')).toBeNull()
63
+
64
+ await user.click(screen.getByText('Show Anchor'))
65
+
66
+ await waitFor(() => {
67
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
68
+ })
69
+ })
70
+
71
+ it('should clean up when anchor is removed', async () => {
72
+ function TestComponent() {
73
+ const [showAnchor, setShowAnchor] = useState(true)
74
+
75
+ return (
76
+ <div>
77
+ <button onClick={() => setShowAnchor(false)}>Hide Anchor</button>
78
+ {showAnchor && <div id="magic-anchor">Anchor</div>}
79
+ <MagicPortal anchor="#magic-anchor">
80
+ <div data-testid="portal-content">Portal Content</div>
81
+ </MagicPortal>
82
+ </div>
83
+ )
84
+ }
85
+
86
+ const user = userEvent.setup()
87
+ render(<TestComponent />)
88
+
89
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
90
+
91
+ await user.click(screen.getByText('Hide Anchor'))
92
+
93
+ await waitFor(() => {
94
+ expect(screen.queryByTestId('portal-content')).toBeNull()
95
+ })
96
+ })
97
+ })
98
+
99
+ describe('Anchor Types', () => {
100
+ it('should work with CSS selector string', () => {
101
+ const anchor = document.createElement('div')
102
+ anchor.className = 'test-class'
103
+ document.body.appendChild(anchor)
104
+
105
+ render(
106
+ <MagicPortal anchor=".test-class">
107
+ <div data-testid="portal-content">Portal Content</div>
108
+ </MagicPortal>
109
+ )
110
+
111
+ const portalContent = screen.getByTestId('portal-content')
112
+ expect(portalContent).toBeTruthy()
113
+ expect(anchor.contains(portalContent)).toBe(true)
114
+ })
115
+
116
+ it('should work with element reference', () => {
117
+ function TestComponent() {
118
+ const anchorRef = useRef<HTMLDivElement>(null)
119
+
120
+ return (
121
+ <div>
122
+ <div ref={anchorRef} data-testid="anchor">
123
+ Anchor
124
+ </div>
125
+ <MagicPortal anchor={anchorRef}>
126
+ <div data-testid="portal-content">Portal Content</div>
127
+ </MagicPortal>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ render(<TestComponent />)
133
+
134
+ const portalContent = screen.getByTestId('portal-content')
135
+ const anchor = screen.getByTestId('anchor')
136
+ expect(portalContent).toBeTruthy()
137
+ expect(anchor.contains(portalContent)).toBe(true)
138
+ })
139
+
140
+ it('should work with function returning element', () => {
141
+ const anchor = document.createElement('div')
142
+ anchor.id = 'function-anchor'
143
+ document.body.appendChild(anchor)
144
+
145
+ render(
146
+ <MagicPortal anchor={() => document.getElementById('function-anchor')}>
147
+ <div data-testid="portal-content">Portal Content</div>
148
+ </MagicPortal>
149
+ )
150
+
151
+ const portalContent = screen.getByTestId('portal-content')
152
+ expect(portalContent).toBeTruthy()
153
+ expect(anchor.contains(portalContent)).toBe(true)
154
+ })
155
+
156
+ it('should work with direct element', () => {
157
+ const anchor = document.createElement('div')
158
+ document.body.appendChild(anchor)
159
+
160
+ render(
161
+ <MagicPortal anchor={anchor}>
162
+ <div data-testid="portal-content">Portal Content</div>
163
+ </MagicPortal>
164
+ )
165
+
166
+ const portalContent = screen.getByTestId('portal-content')
167
+ expect(portalContent).toBeTruthy()
168
+ expect(anchor.contains(portalContent)).toBe(true)
169
+ })
170
+
171
+ it('should handle null anchor gracefully', () => {
172
+ render(
173
+ <MagicPortal anchor={null}>
174
+ <div data-testid="portal-content">Portal Content</div>
175
+ </MagicPortal>
176
+ )
177
+
178
+ expect(screen.queryByTestId('portal-content')).toBeNull()
179
+ })
180
+ })
181
+
182
+ describe('Position Options', () => {
183
+ beforeEach(() => {
184
+ const anchor = document.createElement('div')
185
+ anchor.id = 'position-anchor'
186
+ anchor.innerHTML = '<span>Existing Content</span>'
187
+ document.body.appendChild(anchor)
188
+ })
189
+
190
+ it('should append by default', () => {
191
+ render(
192
+ <MagicPortal anchor="#position-anchor">
193
+ <div data-testid="portal-content">Portal Content</div>
194
+ </MagicPortal>
195
+ )
196
+
197
+ const anchor = document.getElementById('position-anchor')!
198
+ const portalContent = screen.getByTestId('portal-content')
199
+
200
+ expect(anchor.contains(portalContent)).toBe(true)
201
+ })
202
+
203
+ it('should prepend when position is prepend', () => {
204
+ render(
205
+ <MagicPortal anchor="#position-anchor" position="prepend">
206
+ <div data-testid="portal-content">Portal Content</div>
207
+ </MagicPortal>
208
+ )
209
+
210
+ const anchor = document.getElementById('position-anchor')!
211
+ const portalContent = screen.getByTestId('portal-content')
212
+
213
+ expect(anchor.contains(portalContent)).toBe(true)
214
+ })
215
+
216
+ it('should position before when position is before', () => {
217
+ render(
218
+ <MagicPortal anchor="#position-anchor" position="before">
219
+ <div data-testid="portal-content">Portal Content</div>
220
+ </MagicPortal>
221
+ )
222
+
223
+ const anchor = document.getElementById('position-anchor')!
224
+ const portalContent = screen.getByTestId('portal-content')
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)
230
+ })
231
+
232
+ it('should position after when position is after', () => {
233
+ render(
234
+ <MagicPortal anchor="#position-anchor" position="after">
235
+ <div data-testid="portal-content">Portal Content</div>
236
+ </MagicPortal>
237
+ )
238
+
239
+ const anchor = document.getElementById('position-anchor')!
240
+ const portalContent = screen.getByTestId('portal-content')
241
+
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)
246
+ })
247
+ })
248
+
249
+ describe('Lifecycle Callbacks', () => {
250
+ it('should call onMount when portal is mounted', async () => {
251
+ const onMount = vi.fn()
252
+ const anchor = document.createElement('div')
253
+ anchor.id = 'mount-anchor'
254
+ document.body.appendChild(anchor)
255
+
256
+ render(
257
+ <MagicPortal anchor="#mount-anchor" onMount={onMount}>
258
+ <div data-testid="portal-content">Portal Content</div>
259
+ </MagicPortal>
260
+ )
261
+
262
+ await waitFor(() => {
263
+ expect(onMount).toHaveBeenCalledTimes(1)
264
+ expect(onMount).toHaveBeenCalledWith(anchor, expect.any(HTMLDivElement))
265
+ })
266
+ })
267
+
268
+ it('should call onUnmount when component is unmounted', async () => {
269
+ const onUnmount = vi.fn()
270
+ const anchor = document.createElement('div')
271
+ anchor.id = 'unmount-anchor'
272
+ document.body.appendChild(anchor)
273
+
274
+ const { unmount } = render(
275
+ <MagicPortal anchor="#unmount-anchor" onUnmount={onUnmount}>
276
+ <div data-testid="portal-content">Portal Content</div>
277
+ </MagicPortal>
278
+ )
279
+
280
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
281
+
282
+ unmount()
283
+
284
+ expect(onUnmount).toHaveBeenCalledTimes(1)
285
+ expect(onUnmount).toHaveBeenCalledWith(anchor, expect.any(HTMLDivElement))
286
+ })
287
+
288
+ it('should call both onMount and onUnmount during anchor changes', async () => {
289
+ const onMount = vi.fn()
290
+ const onUnmount = vi.fn()
291
+
292
+ // Create anchor element that will persist
293
+ const anchor = document.createElement('div')
294
+ anchor.id = 'toggle-anchor'
295
+
296
+ const { unmount } = render(
297
+ <MagicPortal anchor="#toggle-anchor" onMount={onMount} onUnmount={onUnmount}>
298
+ <div data-testid="portal-content">Portal Content</div>
299
+ </MagicPortal>
300
+ )
301
+
302
+ // No anchor exists initially
303
+ expect(screen.queryByTestId('portal-content')).toBeNull()
304
+ expect(onMount).not.toHaveBeenCalled()
305
+
306
+ // Add anchor to DOM
307
+ document.body.appendChild(anchor)
308
+
309
+ // Wait for portal to mount
310
+ await waitFor(() => {
311
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
312
+ expect(onMount).toHaveBeenCalledTimes(1)
313
+ })
314
+
315
+ // Unmount component to trigger onUnmount
316
+ unmount()
317
+ expect(onUnmount).toHaveBeenCalledTimes(1)
318
+ })
319
+ })
320
+
321
+ describe('Ref Handling', () => {
322
+ it('should forward ref to portal container', () => {
323
+ const ref: { current: HTMLDivElement | null } = { current: null }
324
+ const anchor = document.createElement('div')
325
+ anchor.id = 'ref-anchor'
326
+ document.body.appendChild(anchor)
327
+
328
+ render(
329
+ <MagicPortal anchor="#ref-anchor" ref={ref}>
330
+ <div data-testid="portal-content">Portal Content</div>
331
+ </MagicPortal>
332
+ )
333
+
334
+ expect(ref.current).toBeInstanceOf(HTMLDivElement)
335
+ expect(ref.current?.style.display).toBe('contents')
336
+ })
337
+
338
+ it('should handle function refs', () => {
339
+ let refElement: HTMLDivElement | null = null
340
+ const refCallback = (element: HTMLDivElement | null) => {
341
+ refElement = element
342
+ }
343
+
344
+ const anchor = document.createElement('div')
345
+ anchor.id = 'func-ref-anchor'
346
+ document.body.appendChild(anchor)
347
+
348
+ render(
349
+ <MagicPortal anchor="#func-ref-anchor" ref={refCallback}>
350
+ <div data-testid="portal-content">Portal Content</div>
351
+ </MagicPortal>
352
+ )
353
+
354
+ expect(refElement).toBeInstanceOf(HTMLDivElement)
355
+ expect(refElement!.style.display).toBe('contents')
356
+ })
357
+ })
358
+
359
+ describe('Multiple Portals', () => {
360
+ it('should support multiple portals on the same anchor', () => {
361
+ const anchor = document.createElement('div')
362
+ anchor.id = 'multi-anchor'
363
+ document.body.appendChild(anchor)
364
+
365
+ render(
366
+ <div>
367
+ <MagicPortal anchor="#multi-anchor" position="prepend">
368
+ <div data-testid="portal-1">Portal 1</div>
369
+ </MagicPortal>
370
+ <MagicPortal anchor="#multi-anchor" position="append">
371
+ <div data-testid="portal-2">Portal 2</div>
372
+ </MagicPortal>
373
+ </div>
374
+ )
375
+
376
+ const portal1 = screen.getByTestId('portal-1')
377
+ const portal2 = screen.getByTestId('portal-2')
378
+ expect(portal1).toBeTruthy()
379
+ expect(portal2).toBeTruthy()
380
+ expect(anchor.contains(portal1)).toBe(true)
381
+ expect(anchor.contains(portal2)).toBe(true)
382
+ })
383
+ })
384
+
385
+ describe('Magic Content Updates', () => {
386
+ it('should detect when matching elements are added to DOM', async () => {
387
+ function TestComponent() {
388
+ const [elementCount, setElementCount] = useState(0)
389
+
390
+ const addElement = () => {
391
+ const newElement = document.createElement('div')
392
+ newElement.className = 'magic-target'
393
+ newElement.textContent = `Target ${elementCount + 1}`
394
+ document.body.appendChild(newElement)
395
+ setElementCount((prev) => prev + 1)
396
+ }
397
+
398
+ return (
399
+ <div>
400
+ <button onClick={addElement}>Add Target Element</button>
401
+ <MagicPortal anchor=".magic-target">
402
+ <div data-testid="portal-content">Portal Content</div>
403
+ </MagicPortal>
404
+ </div>
405
+ )
406
+ }
407
+
408
+ const user = userEvent.setup()
409
+ render(<TestComponent />)
410
+
411
+ expect(screen.queryByTestId('portal-content')).toBeNull()
412
+
413
+ await user.click(screen.getByText('Add Target Element'))
414
+
415
+ await waitFor(() => {
416
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
417
+ })
418
+ })
419
+ })
420
+
421
+ describe('Error Handling', () => {
422
+ it('should handle invalid selectors gracefully', () => {
423
+ expect(() => {
424
+ render(
425
+ <MagicPortal anchor="invalid>>>selector">
426
+ <div data-testid="portal-content">Portal Content</div>
427
+ </MagicPortal>
428
+ )
429
+ }).not.toThrow()
430
+
431
+ expect(screen.queryByTestId('portal-content')).toBeNull()
432
+ })
433
+
434
+ it('should handle function that returns null', () => {
435
+ render(
436
+ <MagicPortal anchor={() => null}>
437
+ <div data-testid="portal-content">Portal Content</div>
438
+ </MagicPortal>
439
+ )
440
+
441
+ expect(screen.queryByTestId('portal-content')).toBeNull()
442
+ })
443
+
444
+ it('should handle function that throws error', () => {
445
+ const errorFunction = () => {
446
+ throw new Error('Test error')
447
+ }
448
+
449
+ // The component should handle the error gracefully and not crash
450
+ expect(() => {
451
+ render(
452
+ <MagicPortal anchor={errorFunction}>
453
+ <div data-testid="portal-content">Portal Content</div>
454
+ </MagicPortal>
455
+ )
456
+ }).toThrow('Test error')
457
+
458
+ // Portal content should not be rendered when anchor function throws
459
+ expect(screen.queryByTestId('portal-content')).toBeNull()
460
+ })
461
+ })
462
+
463
+ describe('Key Prop', () => {
464
+ it('should pass key to ReactDOM.createPortal', () => {
465
+ const anchor = document.createElement('div')
466
+ anchor.id = 'key-anchor'
467
+ document.body.appendChild(anchor)
468
+
469
+ const { rerender } = render(
470
+ <MagicPortal anchor="#key-anchor" key="test-key-1">
471
+ <div data-testid="portal-content-1">Portal Content 1</div>
472
+ </MagicPortal>
473
+ )
474
+
475
+ expect(screen.getByTestId('portal-content-1')).toBeTruthy()
476
+
477
+ rerender(
478
+ <MagicPortal anchor="#key-anchor" key="test-key-2">
479
+ <div data-testid="portal-content-2">Portal Content 2</div>
480
+ </MagicPortal>
481
+ )
482
+
483
+ expect(screen.queryByTestId('portal-content-1')).toBeNull()
484
+ expect(screen.getByTestId('portal-content-2')).toBeTruthy()
485
+ })
486
+ })
487
+
488
+ describe('Cleanup', () => {
489
+ it('should clean up MutationObserver on unmount', () => {
490
+ const anchor = document.createElement('div')
491
+ anchor.id = 'cleanup-anchor'
492
+ document.body.appendChild(anchor)
493
+
494
+ const { unmount } = render(
495
+ <MagicPortal anchor="#cleanup-anchor">
496
+ <div data-testid="portal-content">Portal Content</div>
497
+ </MagicPortal>
498
+ )
499
+
500
+ expect(screen.getByTestId('portal-content')).toBeTruthy()
501
+
502
+ expect(() => unmount()).not.toThrow()
503
+ expect(screen.queryByTestId('portal-content')).toBeNull()
504
+ })
505
+ })
506
+ })
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "ES2020",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "allowJs": true,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "module": "ESNext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": true,
18
+ "jsx": "react-jsx",
19
+ "types": ["vitest/globals"]
20
+ },
21
+ "include": ["src/**/*", "vite.config.ts", "eslint.config.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference types="vitest" />
2
+ import { defineConfig } from 'vite'
3
+ import react from '@vitejs/plugin-react'
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ globals: true,
9
+ environment: 'happy-dom'
10
+ }
11
+ })
@@ -0,0 +1,16 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ import pluginReact from "eslint-plugin-react";
5
+ import { defineConfig } from "eslint/config";
6
+
7
+ export default defineConfig([
8
+ {
9
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
10
+ plugins: { js },
11
+ extends: ["js/recommended"],
12
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
13
+ },
14
+ tseslint.configs.recommended,
15
+ pluginReact.configs.flat.recommended,
16
+ ]);
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "react-magic-portal",
3
+ "version": "1.1.0",
4
+ "description": "React Portal with dynamic mounting support",
5
+ "main": "packages/component/dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "pnpm --filter react-magic-portal dev",
9
+ "build": "pnpm --filter react-magic-portal build",
10
+ "dev:example": "pnpm --filter example dev",
11
+ "lint": "npm-run-all -p lint:*",
12
+ "check": "npm-run-all -p check:*",
13
+ "test": "pnpm --filter __tests__ test",
14
+ "test:ui": "pnpm --filter __tests__ test:ui",
15
+ "lint:component": "pnpm --filter react-magic-portal lint",
16
+ "lint:example": "pnpm --filter example lint",
17
+ "lint:tests": "pnpm --filter __tests__ lint",
18
+ "check:component": "pnpm --filter react-magic-portal check",
19
+ "check:example": "pnpm --filter example check",
20
+ "check:tests": "pnpm --filter __tests__ check",
21
+ "prepare": "husky"
22
+ },
23
+ "keywords": [
24
+ "react",
25
+ "portal",
26
+ "dynamic",
27
+ "browser-extension",
28
+ "content-script",
29
+ "mutation-observer",
30
+ "inject",
31
+ "mount",
32
+ "anchor",
33
+ "reactdom",
34
+ "createportal",
35
+ "web-extension",
36
+ "chrome-extension",
37
+ "firefox-extension",
38
+ "dynamic-content",
39
+ "dom-manipulation",
40
+ "typescript"
41
+ ],
42
+ "author": "molvqingtai",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/molvqingtai/react-magic-portal.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/molvqingtai/react-magic-portal/issues"
50
+ },
51
+ "homepage": "https://github.com/molvqingtai/react-magic-portal#readme",
52
+ "dependencies": {
53
+ "@commitlint/cli": "^19.8.1",
54
+ "@commitlint/config-conventional": "^19.8.1",
55
+ "@semantic-release/changelog": "^6.0.3",
56
+ "@semantic-release/git": "^10.0.1",
57
+ "husky": "^9.1.7",
58
+ "npm-run-all": "^4.1.5",
59
+ "semantic-release": "^24.2.9"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 120
6
+ }
@@ -0,0 +1,6 @@
1
+ # Run
2
+
3
+ ```bash
4
+ pnpm install
5
+ pnpm dev
6
+ ```
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+
3
+ //#region src/index.d.ts
4
+ interface MagicPortalProps {
5
+ anchor: string | (() => Element | null) | Element | React.RefObject<Element | null> | null;
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>;
11
+ key?: React.Key;
12
+ }
13
+ declare const MagicPortal: {
14
+ ({
15
+ anchor,
16
+ position,
17
+ children,
18
+ onMount,
19
+ onUnmount,
20
+ ref,
21
+ key
22
+ }: MagicPortalProps): React.ReactPortal | null;
23
+ displayName: string;
24
+ };
25
+ //#endregion
26
+ export { MagicPortalProps, MagicPortal as default };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +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`,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};
2
+ //# sourceMappingURL=index.js.map