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.
@@ -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: 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
 
14
- const MagicPortal = ({ anchor, position = 'append', children, onMount, onUnmount, ref, key }: MagicPortalProps) => {
15
- const [container, setContainer] = useState<HTMLDivElement | null>(null)
16
- const anchorRef = useRef<Element | null>(null)
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
- const updateRef = useCallback(
19
- (element: HTMLDivElement | null) => {
20
- if (ref) {
21
- if (typeof ref === 'function') {
22
- ref(element)
23
- } else {
24
- ref.current = element
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
- [ref]
29
- )
30
-
31
- const createContainer = useCallback(
32
- (anchorElement: Element): HTMLDivElement | null => {
33
- const container = document.createElement('div')
34
- container.style = 'display: contents !important;'
35
-
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
- const updateAnchor = useCallback(() => {
63
- const newAnchor = resolveAnchor()
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
- setContainer((prevContainer) => {
66
- prevContainer?.remove()
67
- anchorRef.current = newAnchor
68
- const newContainer = newAnchor ? createContainer(newAnchor) : null
69
- updateRef(newContainer)
70
- return newContainer
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: Element | null) => {
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
- }, [resolveAnchor, createContainer, updateRef])
77
+ })
73
78
 
74
- useEffect(() => {
75
- updateAnchor()
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 (typeof anchor === 'string') {
88
- return Array.from(addedNodes).some(
89
- (node) => node.nodeType === Node.ELEMENT_NODE && node instanceof Element && node.matches?.(anchor)
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
- }, [updateAnchor, anchor])
114
+ }, [update, anchor])
108
115
 
109
116
  useEffect(() => {
110
- if (anchorRef.current && container) {
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
- }, [container, onMount, onUnmount])
123
+ }, [onMount, onUnmount, container])
117
124
 
118
- return container ? ReactDOM.createPortal(children, container, key) : null
125
+ return container && ReactDOM.createPortal(nodes, container, key)
119
126
  }
120
127
 
121
128
  MagicPortal.displayName = 'MagicPortal'