@@ -110,7 +110,11 @@ function getContainerDimensions(containerNode: Element): Dimensions {
110
110
let scroll : Position = { } ;
111
111
let isPinchZoomedIn = ( visualViewport ?. scale ?? 1 ) > 1 ;
112
112
113
- if ( containerNode . tagName === 'BODY' ) {
113
+ // In the case where the container is `html` or `body` and the container doesn't have something like `position: relative`,
114
+ // then position absolute will be positioned relative to the viewport, also known as the `initial containing block`.
115
+ // That's why we use the visual viewport instead.
116
+
117
+ if ( containerNode . tagName === 'BODY' || containerNode . tagName === 'HTML' ) {
114
118
let documentElement = document . documentElement ;
115
119
totalWidth = documentElement . clientWidth ;
116
120
totalHeight = documentElement . clientHeight ;
@@ -179,10 +183,13 @@ function getDelta(
179
183
let boundarySize = boundaryDimensions [ AXIS_SIZE [ axis ] ] ;
180
184
// Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay.
181
185
// Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)
182
- let boundaryStartEdge = boundaryDimensions . scroll [ AXIS [ axis ] ] + padding ;
183
- let boundaryEndEdge = boundarySize + boundaryDimensions . scroll [ AXIS [ axis ] ] - padding ;
184
- let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary [ axis ] - boundaryDimensions [ AXIS [ axis ] ] ;
185
- let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary [ axis ] - boundaryDimensions [ AXIS [ axis ] ] ;
186
+
187
+ let boundaryStartEdge = containerOffsetWithBoundary [ axis ] + boundaryDimensions . scroll [ AXIS [ axis ] ] + padding ;
188
+ let boundaryEndEdge = containerOffsetWithBoundary [ axis ] + boundaryDimensions . scroll [ AXIS [ axis ] ] + boundarySize - padding ;
189
+ // transformed value of the left edge of the overlay
190
+ let startEdgeOffset = offset - containerScroll + boundaryDimensions . scroll [ AXIS [ axis ] ] + containerOffsetWithBoundary [ axis ] - boundaryDimensions [ AXIS [ axis ] ] ;
191
+ // transformed value of the right edge of the overlay
192
+ let endEdgeOffset = offset - containerScroll + size + boundaryDimensions . scroll [ AXIS [ axis ] ] + containerOffsetWithBoundary [ axis ] - boundaryDimensions [ AXIS [ axis ] ] ;
186
193
187
194
// If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's
188
195
// edges with the closest boundary edge.
@@ -234,7 +241,8 @@ function computePosition(
234
241
containerOffsetWithBoundary : Offset ,
235
242
isContainerPositioned : boolean ,
236
243
arrowSize : number ,
237
- arrowBoundaryOffset : number
244
+ arrowBoundaryOffset : number ,
245
+ containerDimensions : Dimensions
238
246
) {
239
247
let { placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo ;
240
248
let position : Position = { } ;
@@ -255,9 +263,9 @@ function computePosition(
255
263
256
264
position [ crossAxis ] ! += crossOffset ;
257
265
258
- // overlay top overlapping arrow with button bottom
266
+ // overlay top or left overlapping arrow with button bottom or right
259
267
const minPosition = childOffset [ crossAxis ] - overlaySize [ crossSize ] + arrowSize + arrowBoundaryOffset ;
260
- // overlay bottom overlapping arrow with button top
268
+ // overlay bottom or right overlapping arrow with button top or left
261
269
const maxPosition = childOffset [ crossAxis ] + childOffset [ crossSize ] - arrowSize - arrowBoundaryOffset ;
262
270
position [ crossAxis ] = clamp ( position [ crossAxis ] ! , minPosition , maxPosition ) ;
263
271
@@ -266,8 +274,8 @@ function computePosition(
266
274
// If the container is positioned (non-static), then we use the container's actual
267
275
// height, as `bottom` will be relative to this height. But if the container is static,
268
276
// then it can only be the `document.body`, and `bottom` will be relative to _its_
269
- // container, which should be as large as boundaryDimensions .
270
- const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary [ size ] : boundaryDimensions [ TOTAL_SIZE [ size ] ] ) ;
277
+ // container.
278
+ const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary [ size ] : containerDimensions [ TOTAL_SIZE [ size ] ] ) ;
271
279
position [ FLIPPED_DIRECTION [ axis ] ] = Math . floor ( containerHeight - childOffset [ axis ] + offset ) ;
272
280
} else {
273
281
position [ axis ] = Math . floor ( childOffset [ axis ] + childOffset [ size ] + offset ) ;
@@ -283,42 +291,55 @@ function getMaxHeight(
283
291
margins : Position ,
284
292
padding : number ,
285
293
overlayHeight : number ,
286
- heightGrowthDirection : HeightGrowthDirection
294
+ heightGrowthDirection : HeightGrowthDirection ,
295
+ containerDimensions : Dimensions
287
296
) {
288
- const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary . height : boundaryDimensions [ TOTAL_SIZE . height ] ) ;
289
- // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method
290
- // used in computePosition .
291
- let overlayTop = position . top != null ? containerOffsetWithBoundary . top + position . top : containerOffsetWithBoundary . top + ( containerHeight - ( position . bottom ?? 0 ) - overlayHeight ) ;
297
+ const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary . height : containerDimensions [ TOTAL_SIZE . height ] ) ;
298
+ // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top
299
+ // with respect to the container .
300
+ let overlayTop = position . top != null ? position . top : ( containerHeight - ( position . bottom ?? 0 ) - overlayHeight ) ;
292
301
let maxHeight = heightGrowthDirection !== 'top' ?
293
302
// We want the distance between the top of the overlay to the bottom of the boundary
294
303
Math . max ( 0 ,
295
- ( boundaryDimensions . height + boundaryDimensions . top + ( boundaryDimensions . scroll . top ?? 0 ) ) // this is the bottom of the boundary
304
+ ( boundaryDimensions . height + boundaryDimensions . top ) // this is the bottom of the boundary
296
305
- overlayTop // this is the top of the overlay
297
306
- ( ( margins . top ?? 0 ) + ( margins . bottom ?? 0 ) + padding ) // save additional space for margin and padding
298
307
)
299
308
// We want the distance between the bottom of the overlay to the top of the boundary
300
309
: Math . max ( 0 ,
301
310
( overlayTop + overlayHeight ) // this is the bottom of the overlay
302
- - ( boundaryDimensions . top + ( boundaryDimensions . scroll . top ?? 0 ) ) // this is the top of the boundary
311
+ - boundaryDimensions . top // this is the top of the boundary
303
312
- ( ( margins . top ?? 0 ) + ( margins . bottom ?? 0 ) + padding ) // save additional space for margin and padding
304
313
) ;
305
314
return Math . min ( boundaryDimensions . height - ( padding * 2 ) , maxHeight ) ;
306
315
}
307
316
308
317
function getAvailableSpace (
309
- boundaryDimensions : Dimensions ,
318
+ boundaryDimensions : Dimensions , // boundary
310
319
containerOffsetWithBoundary : Offset ,
311
- childOffset : Offset ,
312
- margins : Position ,
313
- padding : number ,
320
+ childOffset : Offset , // trigger
321
+ margins : Position , // overlay
322
+ padding : number , // overlay <-> boundary
314
323
placementInfo : ParsedPlacement
315
324
) {
316
325
let { placement, axis, size} = placementInfo ;
317
326
if ( placement === axis ) {
318
- return Math . max ( 0 , childOffset [ axis ] - boundaryDimensions [ axis ] - ( boundaryDimensions . scroll [ axis ] ?? 0 ) + containerOffsetWithBoundary [ axis ] - ( margins [ axis ] ?? 0 ) - margins [ FLIPPED_DIRECTION [ axis ] ] - padding ) ;
327
+ return Math . max ( 0 ,
328
+ childOffset [ axis ] // trigger start
329
+ - boundaryDimensions [ axis ] // boundary start
330
+ - ( margins [ axis ] ?? 0 ) // margins usually for arrows or other decorations
331
+ - margins [ FLIPPED_DIRECTION [ axis ] ]
332
+ - padding ) ; // padding between overlay and boundary
319
333
}
320
334
321
- return Math . max ( 0 , boundaryDimensions [ size ] + boundaryDimensions [ axis ] + boundaryDimensions . scroll [ axis ] - containerOffsetWithBoundary [ axis ] - childOffset [ axis ] - childOffset [ size ] - ( margins [ axis ] ?? 0 ) - margins [ FLIPPED_DIRECTION [ axis ] ] - padding ) ;
335
+ return Math . max ( 0 ,
336
+ boundaryDimensions [ size ]
337
+ + boundaryDimensions [ axis ]
338
+ - childOffset [ axis ]
339
+ - childOffset [ size ]
340
+ - ( margins [ axis ] ?? 0 )
341
+ - margins [ FLIPPED_DIRECTION [ axis ] ]
342
+ - padding ) ;
322
343
}
323
344
324
345
export function calculatePositionInternal (
@@ -341,7 +362,7 @@ export function calculatePositionInternal(
341
362
) : PositionResult {
342
363
let placementInfo = parsePlacement ( placementInput ) ;
343
364
let { size, crossAxis, crossSize, placement, crossPlacement} = placementInfo ;
344
- let position = computePosition ( childOffset , boundaryDimensions , overlaySize , placementInfo , offset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset ) ;
365
+ let position = computePosition ( childOffset , boundaryDimensions , overlaySize , placementInfo , offset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset , containerDimensions ) ;
345
366
let normalizedOffset = offset ;
346
367
let space = getAvailableSpace (
347
368
boundaryDimensions ,
@@ -355,7 +376,8 @@ export function calculatePositionInternal(
355
376
// Check if the scroll size of the overlay is greater than the available space to determine if we need to flip
356
377
if ( flip && scrollSize [ size ] > space ) {
357
378
let flippedPlacementInfo = parsePlacement ( `${ FLIPPED_DIRECTION [ placement ] } ${ crossPlacement } ` as Placement ) ;
358
- let flippedPosition = computePosition ( childOffset , boundaryDimensions , overlaySize , flippedPlacementInfo , offset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset ) ;
379
+ let flippedPosition = computePosition ( childOffset , boundaryDimensions , overlaySize , flippedPlacementInfo , offset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset , containerDimensions ) ;
380
+
359
381
let flippedSpace = getAvailableSpace (
360
382
boundaryDimensions ,
361
383
containerOffsetWithBoundary ,
@@ -400,7 +422,8 @@ export function calculatePositionInternal(
400
422
margins ,
401
423
padding ,
402
424
overlaySize . height ,
403
- heightGrowthDirection
425
+ heightGrowthDirection ,
426
+ containerDimensions
404
427
) ;
405
428
406
429
if ( userSetMaxHeight && userSetMaxHeight < maxHeight ) {
@@ -409,7 +432,7 @@ export function calculatePositionInternal(
409
432
410
433
overlaySize . height = Math . min ( overlaySize . height , maxHeight ) ;
411
434
412
- position = computePosition ( childOffset , boundaryDimensions , overlaySize , placementInfo , normalizedOffset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset ) ;
435
+ position = computePosition ( childOffset , boundaryDimensions , overlaySize , placementInfo , normalizedOffset , crossOffset , containerOffsetWithBoundary , isContainerPositioned , arrowSize , arrowBoundaryOffset , containerDimensions ) ;
413
436
delta = getDelta ( crossAxis , position [ crossAxis ] ! , overlaySize [ crossSize ] , boundaryDimensions , containerDimensions , padding , containerOffsetWithBoundary ) ;
414
437
position [ crossAxis ] ! += delta ;
415
438
@@ -507,7 +530,7 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
507
530
// If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the
508
531
// body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset
509
532
// by the container scroll since they are essentially the same containing element and thus in the same coordinate system
510
- let containerOffsetWithBoundary : Offset = boundaryElement . tagName === 'BODY' ? getOffset ( container , false ) : getPosition ( container , boundaryElement , false ) ;
533
+ let containerOffsetWithBoundary : Offset = boundaryElement . tagName === 'BODY' ? getOffset ( container , false ) : getPosition ( boundaryElement , container , false ) ;
511
534
if ( container . tagName === 'HTML' && boundaryElement . tagName === 'BODY' ) {
512
535
containerDimensions . scroll . top = 0 ;
513
536
containerDimensions . scroll . left = 0 ;
@@ -536,7 +559,7 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
536
559
export function getRect ( node : Element , ignoreScale : boolean ) {
537
560
let { top, left, width, height} = node . getBoundingClientRect ( ) ;
538
561
539
- // Use offsetWidth and offsetHeight if this is an HTML element, so that
562
+ // Use offsetWidth and offsetHeight if this is an HTML element, so that
540
563
// the size is not affected by scale transforms.
541
564
if ( ignoreScale && node instanceof node . ownerDocument . defaultView ! . HTMLElement ) {
542
565
width = node . offsetWidth ;
0 commit comments