@@ -19,7 +19,7 @@ import { getScrollParent } from 'utils/scrollParent'
19
19
import { standard } from 'utils/transitions'
20
20
21
21
import styles from './Popup.module.css'
22
- import { PopupProps , Position , popupDefaultProps } from './types'
22
+ import { PopupProps , Position , Origin , popupDefaultProps } from './types'
23
23
24
24
const messages = {
25
25
close : 'close popup'
@@ -32,35 +32,82 @@ const messages = {
32
32
const CONTAINER_INSET_PADDING = 16
33
33
34
34
/**
35
- * Gets the css transform origin prop from the display position
36
- * @param {Position } position
37
- * @returns {string } transform origin
35
+ * Used to convert deprecated Position prop to transformOrigin prop
38
36
*/
39
- const getTransformOrigin = ( position : Position ) =>
40
- ( {
41
- [ Position . TOP_LEFT ] : 'bottom right' ,
42
- [ Position . TOP_CENTER ] : 'bottom center' ,
43
- [ Position . TOP_RIGHT ] : 'bottom left' ,
44
- [ Position . BOTTOM_LEFT ] : 'top right' ,
45
- [ Position . BOTTOM_CENTER ] : 'top center' ,
46
- [ Position . BOTTOM_RIGHT ] : 'top left'
47
- } [ position ] ?? 'top center' )
37
+ const positionToTransformOriginMap : Record < Position , Origin > = {
38
+ [ Position . TOP_LEFT ] : {
39
+ horizontal : 'right' ,
40
+ vertical : 'bottom'
41
+ } ,
42
+ [ Position . TOP_CENTER ] : {
43
+ horizontal : 'center' ,
44
+ vertical : 'bottom'
45
+ } ,
46
+ [ Position . TOP_RIGHT ] : {
47
+ horizontal : 'left' ,
48
+ vertical : 'bottom'
49
+ } ,
50
+ [ Position . BOTTOM_LEFT ] : {
51
+ horizontal : 'right' ,
52
+ vertical : 'top'
53
+ } ,
54
+ [ Position . BOTTOM_CENTER ] : {
55
+ horizontal : 'center' ,
56
+ vertical : 'top'
57
+ } ,
58
+ [ Position . BOTTOM_RIGHT ] : {
59
+ horizontal : 'left' ,
60
+ vertical : 'top'
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Used to convert deprecated Position prop to anchorOrigin prop
66
+ */
67
+ const positionToAnchorOriginMap : Record < Position , Origin > = {
68
+ [ Position . TOP_LEFT ] : {
69
+ horizontal : 'left' ,
70
+ vertical : 'top'
71
+ } ,
72
+ [ Position . TOP_CENTER ] : {
73
+ horizontal : 'center' ,
74
+ vertical : 'top'
75
+ } ,
76
+ [ Position . TOP_RIGHT ] : {
77
+ horizontal : 'right' ,
78
+ vertical : 'top'
79
+ } ,
80
+ [ Position . BOTTOM_LEFT ] : {
81
+ horizontal : 'left' ,
82
+ vertical : 'bottom'
83
+ } ,
84
+ [ Position . BOTTOM_CENTER ] : {
85
+ horizontal : 'center' ,
86
+ vertical : 'bottom'
87
+ } ,
88
+ [ Position . BOTTOM_RIGHT ] : {
89
+ horizontal : 'right' ,
90
+ vertical : 'bottom'
91
+ }
92
+ }
48
93
49
94
/**
50
95
* Figures out whether the specified position would overflow the window
51
96
* and picks a better position accordingly
52
- * @param {Position } position
53
- * @param {ClientRect } rect the content
54
- * @param {ClientRect } wrapper the wrapper of the content
55
- * @return {string | null } null if it would not overflow
97
+ * @param {Origin } anchorOrigin where the origin is on the trigger
98
+ * @param {Origin } transformOrigin where the origin is on the popup
99
+ * @param {DOMRect } anchorRect the position and size of the trigger
100
+ * @param {DOMRect } wrapperRect the position and size of the popup
101
+ * @return {{ anchorOrigin: Origin, transformOrigin: Origin } } the new origin after accounting for overflow
56
102
*/
57
- const getComputedPosition = (
58
- position : Position ,
103
+ const getComputedOrigins = (
104
+ anchorOrigin : Origin ,
105
+ transformOrigin : Origin ,
59
106
anchorRect : DOMRect ,
60
107
wrapperRect : DOMRect ,
61
108
containerRef ?: MutableRefObject < HTMLDivElement | undefined >
62
- ) : Position => {
63
- if ( ! anchorRect || ! wrapperRect ) return position
109
+ ) => {
110
+ if ( ! anchorRect || ! wrapperRect ) return { anchorOrigin , transformOrigin }
64
111
65
112
let containerWidth , containerHeight
66
113
if ( containerRef && containerRef . current ) {
@@ -75,24 +122,36 @@ const getComputedPosition = (
75
122
containerHeight = window . innerHeight - CONTAINER_INSET_PADDING
76
123
}
77
124
78
- const overflowRight = anchorRect . x + wrapperRect . width > containerWidth
79
- const overflowLeft = anchorRect . x - wrapperRect . width < 0
80
- const overflowBottom = anchorRect . y + wrapperRect . height > containerHeight
81
- const overflowTop = anchorRect . y - wrapperRect . height < 0
125
+ // Get new wrapper position
126
+ const anchorTranslation = getOriginTranslation ( anchorOrigin , anchorRect )
127
+ const wrapperTranslation = getOriginTranslation ( transformOrigin , wrapperRect )
128
+ const wrapperX = anchorRect . x + anchorTranslation . x - wrapperTranslation . x
129
+ const wrapperY = anchorRect . y + anchorTranslation . y - wrapperTranslation . y
130
+
131
+ // Check bounds of the wrapper in new position are inside container
132
+ const overflowRight = wrapperX + wrapperRect . width > containerWidth
133
+ const overflowLeft = wrapperX < 0
134
+ const overflowBottom = wrapperY + wrapperRect . height > containerHeight
135
+ const overflowTop = wrapperY < 0
82
136
137
+ // For all overflows, flip the position
83
138
if ( overflowRight ) {
84
- position = position . replace ( 'Right' , 'Left' ) as Position
139
+ anchorOrigin . horizontal = 'left'
140
+ transformOrigin . horizontal = 'right'
85
141
}
86
142
if ( overflowLeft ) {
87
- position = position . replace ( 'Left' , 'Right' ) as Position
143
+ anchorOrigin . horizontal = 'right'
144
+ transformOrigin . horizontal = 'left'
88
145
}
89
146
if ( overflowTop ) {
90
- position = position . replace ( 'top' , 'bottom' ) as Position
147
+ anchorOrigin . vertical = 'bottom'
148
+ transformOrigin . vertical = 'top'
91
149
}
92
150
if ( overflowBottom ) {
93
- position = position . replace ( 'bottom' , 'top' ) as Position
151
+ anchorOrigin . vertical = 'top'
152
+ transformOrigin . vertical = 'bottom'
94
153
}
95
- return position
154
+ return { anchorOrigin , transformOrigin }
96
155
}
97
156
98
157
/**
@@ -132,22 +191,63 @@ const getAdjustedPosition = (
132
191
return adjusted
133
192
}
134
193
194
+ /**
195
+ * Gets the x, y offsets for the given origin using the dimensions
196
+ * @param origin the relative origin
197
+ * @param dimensions the dimensions to use with the relative origin
198
+ * @returns the x and y coordinates of the new origin relative to the old one
199
+ */
200
+ const getOriginTranslation = (
201
+ origin : Origin ,
202
+ dimensions : { width : number ; height : number }
203
+ ) => {
204
+ let x = 0
205
+ let y = 0
206
+ const { width, height } = dimensions
207
+ if ( origin . horizontal === 'center' ) {
208
+ x += width / 2
209
+ } else if ( origin . horizontal === 'right' ) {
210
+ x += width
211
+ }
212
+ if ( origin . vertical === 'center' ) {
213
+ y += height / 2
214
+ } else if ( origin . vertical === 'bottom' ) {
215
+ y += height
216
+ }
217
+ return { x, y }
218
+ }
219
+
220
+ const defaultAnchorOrigin : Origin = {
221
+ horizontal : 'center' ,
222
+ vertical : 'bottom'
223
+ }
224
+
225
+ const defaultTransformOrigin : Origin = {
226
+ horizontal : 'center' ,
227
+ vertical : 'top'
228
+ }
229
+
135
230
/**
136
231
* A popup is an in-place container that shows on top of the UI. A popup does
137
232
* not impact the rest of the UI (e.g. graying it out). It differs
138
233
* from modals, which do take over the whole UI and are usually
139
234
* center-screened.
140
235
*/
141
236
export const Popup = forwardRef < HTMLDivElement , PopupProps > ( function Popup (
142
- {
237
+ props ,
238
+ ref
239
+ ) {
240
+ const {
143
241
anchorRef,
144
- animationDuration,
242
+ animationDuration = 90 ,
145
243
checkIfClickInside,
146
244
children,
147
245
isVisible,
148
246
onAfterClose,
149
247
onClose,
150
- position = Position . BOTTOM_CENTER ,
248
+ position,
249
+ anchorOrigin : anchorOriginProp = defaultAnchorOrigin ,
250
+ transformOrigin : transformOriginProp = defaultTransformOrigin ,
151
251
hideCloseButton = false ,
152
252
showHeader,
153
253
title,
@@ -156,9 +256,7 @@ export const Popup = forwardRef<HTMLDivElement, PopupProps>(function Popup(
156
256
wrapperClassName,
157
257
zIndex,
158
258
containerRef
159
- } ,
160
- ref
161
- ) {
259
+ } = props
162
260
const handleClose = useCallback ( ( ) => {
163
261
onClose ( )
164
262
setTimeout ( ( ) => {
@@ -168,6 +266,13 @@ export const Popup = forwardRef<HTMLDivElement, PopupProps>(function Popup(
168
266
} , animationDuration )
169
267
} , [ onClose , onAfterClose , animationDuration ] )
170
268
269
+ const [ anchorOrigin , transformOrigin ] = position
270
+ ? [
271
+ positionToAnchorOriginMap [ position ] ,
272
+ positionToTransformOriginMap [ position ]
273
+ ]
274
+ : [ anchorOriginProp , transformOriginProp ]
275
+
171
276
const popupRef : React . MutableRefObject < HTMLDivElement > = useClickOutside (
172
277
handleClose ,
173
278
checkIfClickInside ,
@@ -177,57 +282,41 @@ export const Popup = forwardRef<HTMLDivElement, PopupProps>(function Popup(
177
282
178
283
const wrapperRef = useRef < HTMLDivElement > ( null )
179
284
const originalTopPosition = useRef < number > ( 0 )
180
- const [ computedPosition , setComputedPosition ] = useState ( position )
181
-
182
- const getRects = useCallback (
183
- ( ) =>
184
- [ anchorRef , wrapperRef ] . map ( ( r ) => r ?. current ?. getBoundingClientRect ( ) ) ,
185
- [ anchorRef , wrapperRef ]
186
- )
285
+ const [ computedTransformOrigin , setComputedTransformOrigin ] =
286
+ useState ( anchorOrigin )
187
287
188
288
// On visible, set the position
189
289
useEffect ( ( ) => {
190
290
if ( isVisible ) {
191
- const [ anchorRect , wrapperRect ] = getRects ( )
291
+ const [ anchorRect , wrapperRect ] = [ anchorRef , wrapperRef ] . map ( ( r ) =>
292
+ r ?. current ?. getBoundingClientRect ( )
293
+ )
192
294
if ( ! anchorRect || ! wrapperRect ) return
193
295
194
- const computed = getComputedPosition (
195
- position ,
296
+ const {
297
+ anchorOrigin : anchorOriginComputed ,
298
+ transformOrigin : transformOriginComputed
299
+ } = getComputedOrigins (
300
+ anchorOrigin ,
301
+ transformOrigin ,
196
302
anchorRect ,
197
303
wrapperRect ,
198
304
containerRef
199
305
)
200
- setComputedPosition ( computed )
201
-
202
- const positionMap = {
203
- [ Position . TOP_LEFT ] : [
204
- anchorRect . y - wrapperRect . height ,
205
- anchorRect . x - wrapperRect . width
206
- ] ,
207
- [ Position . TOP_CENTER ] : [
208
- anchorRect . y - wrapperRect . height ,
209
- anchorRect . x - wrapperRect . width / 2 + anchorRect . width / 2
210
- ] ,
211
- [ Position . TOP_RIGHT ] : [
212
- anchorRect . y - wrapperRect . height ,
213
- anchorRect . x + anchorRect . width
214
- ] ,
215
- [ Position . BOTTOM_LEFT ] : [
216
- anchorRect . y + anchorRect . height ,
217
- anchorRect . x - wrapperRect . width
218
- ] ,
219
- [ Position . BOTTOM_CENTER ] : [
220
- anchorRect . y + anchorRect . height ,
221
- anchorRect . x - wrapperRect . width / 2 + anchorRect . width / 2
222
- ] ,
223
- [ Position . BOTTOM_RIGHT ] : [
224
- anchorRect . y + anchorRect . height ,
225
- anchorRect . x + anchorRect . width
226
- ]
227
- }
306
+ setComputedTransformOrigin ( transformOriginComputed )
307
+
308
+ const anchorTranslation = getOriginTranslation (
309
+ anchorOriginComputed ,
310
+ anchorRect
311
+ )
312
+ const wrapperTranslation = getOriginTranslation (
313
+ transformOriginComputed ,
314
+ wrapperRect
315
+ )
316
+
317
+ const top = anchorRect . y + anchorTranslation . y - wrapperTranslation . y
318
+ const left = anchorRect . x + anchorTranslation . x - wrapperTranslation . x
228
319
229
- const [ top , left ] =
230
- positionMap [ computed ] ?? positionMap [ Position . BOTTOM_CENTER ]
231
320
const { adjustedTop, adjustedLeft } = getAdjustedPosition (
232
321
top ,
233
322
left ,
@@ -242,13 +331,12 @@ export const Popup = forwardRef<HTMLDivElement, PopupProps>(function Popup(
242
331
originalTopPosition . current = top
243
332
}
244
333
} , [
245
- position ,
246
334
isVisible ,
247
335
wrapperRef ,
248
336
anchorRef ,
249
- computedPosition ,
250
- setComputedPosition ,
251
- getRects ,
337
+ anchorOrigin ,
338
+ transformOrigin ,
339
+ setComputedTransformOrigin ,
252
340
originalTopPosition ,
253
341
containerRef
254
342
] )
@@ -337,7 +425,7 @@ export const Popup = forwardRef<HTMLDivElement, PopupProps>(function Popup(
337
425
key = { key }
338
426
style = { {
339
427
...props ,
340
- transformOrigin : getTransformOrigin ( computedPosition )
428
+ transformOrigin : ` ${ computedTransformOrigin . horizontal } ${ computedTransformOrigin . vertical } `
341
429
} }
342
430
>
343
431
{ showHeader && (
0 commit comments