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 +14 -0
- package/README.md +85 -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/src/components/portal-content.tsx +7 -2
- package/packages/example/dist/assets/index-BSN9W4mP.js +0 -49
- package/packages/example/dist/assets/index-DWWbQwSg.css +0 -1
|
@@ -1,101 +1,108 @@
|
|
|
1
|
-
import React, { useEffect, useState, useRef, useCallback } from 'react'
|
|
1
|
+
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect } from 'react'
|
|
2
2
|
import ReactDOM from 'react-dom'
|
|
3
3
|
|
|
4
4
|
export 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
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* https://github.com/radix-ui/primitives/blob/36d954d3c1b41c96b1d2e875b93fc9362c8c09e6/packages/react/slot/src/slot.tsx#L166
|
|
15
|
+
*/
|
|
16
|
+
const getElementRef = (element: React.ReactElement) => {
|
|
17
|
+
// React <=18 in DEV
|
|
18
|
+
let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get
|
|
19
|
+
let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning
|
|
20
|
+
if (mayWarn) {
|
|
21
|
+
return (element as any).ref as React.Ref<Element>
|
|
22
|
+
}
|
|
23
|
+
// React 19 in DEV
|
|
24
|
+
getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get
|
|
25
|
+
mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning
|
|
26
|
+
if (mayWarn) {
|
|
27
|
+
return (element.props as { ref?: React.Ref<Element> }).ref
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Not DEV
|
|
31
|
+
return (element.props as { ref?: React.Ref<Element> }).ref || ((element as any).ref as React.Ref<Element>)
|
|
32
|
+
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
const resolveAnchor = (anchor: MagicPortalProps['anchor']) => {
|
|
35
|
+
if (typeof anchor === 'string') {
|
|
36
|
+
return document.querySelector(anchor)
|
|
37
|
+
} else if (typeof anchor === 'function') {
|
|
38
|
+
return anchor()
|
|
39
|
+
} else if (anchor && 'current' in anchor) {
|
|
40
|
+
return anchor.current
|
|
41
|
+
} else {
|
|
42
|
+
return anchor
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mergeRef = <T extends Element | null>(...refs: (React.Ref<T> | undefined)[]) => {
|
|
47
|
+
return (node: T) =>
|
|
48
|
+
refs.forEach((ref) => {
|
|
49
|
+
if (typeof ref === 'function') {
|
|
50
|
+
ref(node)
|
|
51
|
+
} else if (ref) {
|
|
52
|
+
ref.current = node
|
|
26
53
|
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
const createContainer = useCallback(
|
|
32
|
-
(anchorElement: Element): HTMLDivElement | null => {
|
|
33
|
-
const container = document.createElement('div')
|
|
34
|
-
container.style = 'display: contents !important;'
|
|
35
|
-
container.dataset.magicPortal = 'true'
|
|
36
|
-
const positionMap = {
|
|
37
|
-
before: 'beforebegin',
|
|
38
|
-
prepend: 'afterbegin',
|
|
39
|
-
append: 'beforeend',
|
|
40
|
-
after: 'afterend'
|
|
41
|
-
} as const
|
|
42
|
-
|
|
43
|
-
const result = anchorElement.insertAdjacentElement(positionMap[position], container)
|
|
44
|
-
|
|
45
|
-
return result as HTMLDivElement | null
|
|
46
|
-
},
|
|
47
|
-
[position]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const resolveAnchor = useCallback((): Element | null => {
|
|
51
|
-
if (typeof anchor === 'string') {
|
|
52
|
-
return document.querySelector(anchor)
|
|
53
|
-
} else if (typeof anchor === 'function') {
|
|
54
|
-
return anchor()
|
|
55
|
-
} else if (anchor && 'current' in anchor) {
|
|
56
|
-
return anchor.current
|
|
57
|
-
} else {
|
|
58
|
-
return anchor
|
|
59
|
-
}
|
|
60
|
-
}, [anchor])
|
|
54
|
+
})
|
|
55
|
+
}
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, key }: MagicPortalProps) => {
|
|
58
|
+
const anchorRef = useRef<Element | null>(null)
|
|
59
|
+
const [container, setContainer] = useState<Element | null>(null)
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
const nodes = React.Children.map(children, (item) => {
|
|
62
|
+
if (!React.isValidElement(item)) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
const originalRef = getElementRef(item)
|
|
66
|
+
return React.cloneElement(item as React.ReactElement<any>, {
|
|
67
|
+
ref: mergeRef(originalRef, (node) => {
|
|
68
|
+
const positionMap = {
|
|
69
|
+
before: 'beforebegin',
|
|
70
|
+
prepend: 'afterbegin',
|
|
71
|
+
append: 'beforeend',
|
|
72
|
+
after: 'afterend'
|
|
73
|
+
} as const
|
|
74
|
+
node && anchorRef.current?.insertAdjacentElement(positionMap[position], node)
|
|
75
|
+
})
|
|
71
76
|
})
|
|
72
|
-
}
|
|
77
|
+
})
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
const update = useCallback(() => {
|
|
80
|
+
anchorRef.current = resolveAnchor(anchor)
|
|
81
|
+
const container =
|
|
82
|
+
position === 'prepend' || position === 'append' ? anchorRef.current : (anchorRef.current?.parentElement ?? null)
|
|
83
|
+
setContainer(container)
|
|
84
|
+
}, [anchor, position])
|
|
85
|
+
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
update()
|
|
76
88
|
|
|
77
89
|
const observer = new MutationObserver((mutations) => {
|
|
78
90
|
const shouldUpdate = mutations.some((mutation) => {
|
|
79
91
|
const { addedNodes, removedNodes } = mutation
|
|
80
|
-
|
|
81
92
|
// Check if current anchor is removed
|
|
82
93
|
if (anchorRef.current && Array.from(removedNodes).includes(anchorRef.current)) {
|
|
83
94
|
return true
|
|
84
95
|
}
|
|
85
|
-
|
|
86
96
|
// Only check added nodes when anchor is a string selector
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
if (
|
|
98
|
+
typeof anchor === 'string' &&
|
|
99
|
+
[...addedNodes].some((node) => node instanceof Element && node.matches?.(anchor))
|
|
100
|
+
) {
|
|
101
|
+
return true
|
|
91
102
|
}
|
|
92
|
-
|
|
93
103
|
return false
|
|
94
104
|
})
|
|
95
|
-
|
|
96
|
-
if (shouldUpdate) {
|
|
97
|
-
updateAnchor()
|
|
98
|
-
}
|
|
105
|
+
shouldUpdate && update()
|
|
99
106
|
})
|
|
100
107
|
|
|
101
108
|
observer.observe(document.body, {
|
|
@@ -104,18 +111,18 @@ const MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount
|
|
|
104
111
|
})
|
|
105
112
|
|
|
106
113
|
return () => observer.disconnect()
|
|
107
|
-
}, [
|
|
114
|
+
}, [update, anchor])
|
|
108
115
|
|
|
109
116
|
useEffect(() => {
|
|
110
|
-
if (anchorRef.current
|
|
117
|
+
if (container && anchorRef.current) {
|
|
111
118
|
onMount?.(anchorRef.current, container)
|
|
112
119
|
return () => {
|
|
113
120
|
onUnmount?.(anchorRef.current!, container)
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
}, [
|
|
123
|
+
}, [onMount, onUnmount, container])
|
|
117
124
|
|
|
118
|
-
return container
|
|
125
|
+
return container && ReactDOM.createPortal(nodes, container, key)
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
MagicPortal.displayName = 'MagicPortal'
|