@@ -8,6 +8,7 @@ import React, {
8
8
memo ,
9
9
useState ,
10
10
} from 'react' ;
11
+ import ReactDOM from 'react-dom' ;
11
12
import useCloseClickOutside from 'src/components/effects/useCloseClickOutside' ;
12
13
import { ChevronDown , CloseIcon } from 'src/components/icons' ;
13
14
import { PopOverModel } from './popover.model' ;
@@ -78,6 +79,11 @@ const PopOver: FunctionComponent<PopOverModel> = ({
78
79
const [ horizontalPosition , setHorizontalPosition ] = useState <
79
80
'left' | 'right' | 'center'
80
81
> ( 'center' ) ;
82
+ const [ popoverPosition , setPopoverPosition ] = useState ( {
83
+ top : 0 ,
84
+ left : 0 ,
85
+ width : 0
86
+ } ) ;
81
87
82
88
const toggleOpen = useCallback ( ( ) => {
83
89
dispatch ( { type : 'TOGGLE' } ) ;
@@ -95,40 +101,91 @@ const PopOver: FunctionComponent<PopOverModel> = ({
95
101
96
102
useCloseClickOutside ( ref , closePopover ) ;
97
103
98
- // Calculate optimal horizontal positioning
99
- const calculateHorizontalPosition = useCallback ( ( ) => {
100
- if ( ! ref . current || ! popoverRef . current || $isMobile ) return ;
104
+ // Calculate optimal positioning for portal rendering
105
+ const calculatePopoverPosition = useCallback ( ( ) => {
106
+ if ( ! ref . current ) return ;
101
107
102
108
const triggerRect = ref . current . getBoundingClientRect ( ) ;
103
- const popoverWidth = width ;
104
109
const viewportWidth = window . innerWidth ;
105
- const timelineContainer =
106
- ref . current . closest ( '[data-testid="timeline"]' ) || document . body ;
107
- const containerRect = timelineContainer . getBoundingClientRect ( ) ;
108
-
109
- // Calculate space available on each side
110
- const spaceLeft = triggerRect . left - containerRect . left ;
111
- const spaceRight = containerRect . right - triggerRect . right ;
112
- const spaceCenter = Math . min ( spaceLeft , spaceRight ) ;
113
-
114
- // Choose position based on available space
115
- if ( popoverWidth <= spaceCenter * 2 ) {
116
- setHorizontalPosition ( 'center' ) ;
117
- } else if ( spaceLeft >= popoverWidth && spaceLeft > spaceRight ) {
118
- setHorizontalPosition ( 'left' ) ;
119
- } else if ( spaceRight >= popoverWidth ) {
120
- setHorizontalPosition ( 'right' ) ;
110
+ const viewportHeight = window . innerHeight ;
111
+ const popoverWidth = $isMobile ? Math . min ( 320 , viewportWidth * 0.9 ) : width ;
112
+
113
+ // Calculate horizontal position
114
+ let left = triggerRect . left ;
115
+ let horizontalPos : 'left' | 'right' | 'center' = 'left' ;
116
+
117
+ if ( $isMobile ) {
118
+ // Center on mobile
119
+ left = ( viewportWidth - popoverWidth ) / 2 ;
120
+ horizontalPos = 'center' ;
121
121
} else {
122
- setHorizontalPosition ( 'center' ) ; // Fallback to center with potential overflow
122
+ // Calculate space available on each side
123
+ const spaceLeft = triggerRect . left ;
124
+ const spaceRight = viewportWidth - triggerRect . right ;
125
+
126
+ if ( popoverWidth <= spaceRight ) {
127
+ // Align with left edge of trigger
128
+ left = triggerRect . left ;
129
+ horizontalPos = 'left' ;
130
+ } else if ( popoverWidth <= spaceLeft ) {
131
+ // Align with right edge of trigger
132
+ left = triggerRect . right - popoverWidth ;
133
+ horizontalPos = 'right' ;
134
+ } else {
135
+ // Center horizontally
136
+ left = Math . max ( 16 , ( viewportWidth - popoverWidth ) / 2 ) ;
137
+ horizontalPos = 'center' ;
138
+ }
139
+
140
+ // Ensure popover doesn't go off-screen
141
+ left = Math . max ( 16 , Math . min ( left , viewportWidth - popoverWidth - 16 ) ) ;
123
142
}
124
- } , [ width , $isMobile ] ) ;
143
+
144
+ // Calculate vertical position
145
+ let top = triggerRect . bottom + 8 ; // 8px gap below trigger
146
+
147
+ // If position === 'bottom' but there's not enough space below, try above
148
+ if ( position === 'bottom' && top + 400 > viewportHeight ) {
149
+ const topPosition = triggerRect . top - 408 ; // 400px height + 8px gap
150
+ if ( topPosition > 16 ) {
151
+ top = topPosition ;
152
+ }
153
+ } else if ( position === 'top' ) {
154
+ top = Math . max ( 16 , triggerRect . top - 408 ) ;
155
+ }
156
+
157
+ // Ensure popover doesn't go above viewport
158
+ top = Math . max ( 16 , top ) ;
159
+
160
+ setPopoverPosition ( { top, left, width : popoverWidth } ) ;
161
+ setHorizontalPosition ( horizontalPos ) ;
162
+ } , [ width , $isMobile , position ] ) ;
125
163
126
- // Position calculation on layout changes
164
+ // Position calculation on layout changes and scroll/resize
127
165
useLayoutEffect ( ( ) => {
128
166
if ( state . open ) {
129
- calculateHorizontalPosition ( ) ;
167
+ calculatePopoverPosition ( ) ;
130
168
}
131
- } , [ state . open , calculateHorizontalPosition ] ) ;
169
+ } , [ state . open , calculatePopoverPosition ] ) ;
170
+
171
+ // Recalculate position on scroll or resize
172
+ useEffect ( ( ) => {
173
+ if ( ! state . open ) return ;
174
+
175
+ const handlePositionUpdate = ( ) => {
176
+ if ( state . open ) {
177
+ calculatePopoverPosition ( ) ;
178
+ }
179
+ } ;
180
+
181
+ window . addEventListener ( 'scroll' , handlePositionUpdate , { passive : true } ) ;
182
+ window . addEventListener ( 'resize' , handlePositionUpdate , { passive : true } ) ;
183
+
184
+ return ( ) => {
185
+ window . removeEventListener ( 'scroll' , handlePositionUpdate ) ;
186
+ window . removeEventListener ( 'resize' , handlePositionUpdate ) ;
187
+ } ;
188
+ } , [ state . open , calculatePopoverPosition ] ) ;
132
189
133
190
// Use CSS transition instead of setTimeout
134
191
useEffect ( ( ) => {
@@ -164,40 +221,47 @@ const PopOver: FunctionComponent<PopOverModel> = ({
164
221
{ icon || < ChevronDown /> }
165
222
</ span >
166
223
</ button >
167
- { state . open ? (
168
- < div
169
- ref = { popoverRef }
170
- className = { [
171
- popoverHolder ,
172
- popoverHolderRecipe ( {
173
- visible : state . isVisible ,
174
- position : position === 'bottom' ? 'bottom' : 'top' ,
175
- leftMobile : ! ! $isMobile ,
176
- } ) ,
177
- ] . join ( ' ' ) }
178
- style = { {
179
- ...computeCssVarsFromTheme ( theme ) ,
180
- width : $isMobile ? '90%' : `${ width } px` ,
181
- } }
182
- data-position-x = { horizontalPosition }
183
- role = "dialog"
184
- aria-modal = "false"
185
- aria-labelledby = { placeholder ? undefined : 'popover-content' }
186
- >
187
- < div className = { header } >
188
- < button
189
- className = { closeButton }
190
- onClick = { closePopover }
191
- type = "button"
192
- aria-label = "Close menu"
193
- title = "Close menu"
194
- >
195
- < CloseIcon />
196
- </ button >
197
- </ div >
198
- < MemoizedContent > { children } </ MemoizedContent >
199
- </ div >
200
- ) : null }
224
+ { state . open &&
225
+ ReactDOM . createPortal (
226
+ < div
227
+ ref = { popoverRef }
228
+ className = { [
229
+ popoverHolder ,
230
+ popoverHolderRecipe ( {
231
+ visible : state . isVisible ,
232
+ position : position === 'bottom' ? 'bottom' : 'top' ,
233
+ leftMobile : ! ! $isMobile ,
234
+ } ) ,
235
+ ] . join ( ' ' ) }
236
+ style = { {
237
+ ...computeCssVarsFromTheme ( theme ) ,
238
+ width : `${ popoverPosition . width } px` ,
239
+ position : 'fixed' ,
240
+ zIndex : 99999 ,
241
+ left : `${ popoverPosition . left } px` ,
242
+ top : `${ popoverPosition . top } px` ,
243
+ transform : 'none' ,
244
+ } }
245
+ data-position-x = { horizontalPosition }
246
+ role = "dialog"
247
+ aria-modal = "false"
248
+ aria-labelledby = { placeholder ? undefined : 'popover-content' }
249
+ >
250
+ < div className = { header } >
251
+ < button
252
+ className = { closeButton }
253
+ onClick = { closePopover }
254
+ type = "button"
255
+ aria-label = "Close menu"
256
+ title = "Close menu"
257
+ >
258
+ < CloseIcon />
259
+ </ button >
260
+ </ div >
261
+ < MemoizedContent > { children } </ MemoizedContent >
262
+ </ div > ,
263
+ document . body ,
264
+ ) }
201
265
</ div >
202
266
</ >
203
267
) ;
0 commit comments