@@ -23,6 +23,8 @@ interface Position {
23
23
interface Dimensions {
24
24
width : number ,
25
25
height : number ,
26
+ totalWidth : number ,
27
+ totalHeight : number ,
26
28
top : number ,
27
29
left : number ,
28
30
scroll : Position
@@ -91,29 +93,38 @@ const AXIS_SIZE = {
91
93
left : 'width'
92
94
} ;
93
95
96
+ const TOTAL_SIZE = {
97
+ width : 'totalWidth' ,
98
+ height : 'totalHeight'
99
+ } ;
100
+
94
101
const PARSED_PLACEMENT_CACHE = { } ;
95
102
96
103
// @ts -ignore
97
104
let visualViewport = typeof window !== 'undefined' && window . visualViewport ;
98
105
99
106
function getContainerDimensions ( containerNode : Element ) : Dimensions {
100
- let width = 0 , height = 0 , top = 0 , left = 0 ;
107
+ let width = 0 , height = 0 , totalWidth = 0 , totalHeight = 0 , top = 0 , left = 0 ;
101
108
let scroll : Position = { } ;
102
109
103
110
if ( containerNode . tagName === 'BODY' ) {
104
111
let documentElement = document . documentElement ;
105
- width = visualViewport ?. width ?? documentElement . clientWidth ;
106
- height = visualViewport ?. height ?? documentElement . clientHeight ;
112
+ totalWidth = documentElement . clientWidth ;
113
+ totalHeight = documentElement . clientHeight ;
114
+ width = visualViewport ?. width ?? totalWidth ;
115
+ height = visualViewport ?. height ?? totalHeight ;
107
116
108
117
scroll . top = documentElement . scrollTop || containerNode . scrollTop ;
109
118
scroll . left = documentElement . scrollLeft || containerNode . scrollLeft ;
110
119
} else {
111
120
( { width, height, top, left} = getOffset ( containerNode ) ) ;
112
121
scroll . top = containerNode . scrollTop ;
113
122
scroll . left = containerNode . scrollLeft ;
123
+ totalWidth = width ;
124
+ totalHeight = height ;
114
125
}
115
126
116
- return { width, height, scroll, top, left} ;
127
+ return { width, height, totalWidth , totalHeight , scroll, top, left} ;
117
128
}
118
129
119
130
function getScroll ( node : Element ) : Offset {
@@ -219,7 +230,7 @@ function computePosition(
219
230
// height, as `bottom` will be relative to this height. But if the container is static,
220
231
// then it can only be the `document.body`, and `bottom` will be relative to _its_
221
232
// container, which should be as large as boundaryDimensions.
222
- const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary [ size ] : boundaryDimensions [ size ] ) ;
233
+ const containerHeight = ( isContainerPositioned ? containerOffsetWithBoundary [ size ] : boundaryDimensions [ TOTAL_SIZE [ size ] ] ) ;
223
234
position [ FLIPPED_DIRECTION [ axis ] ] = Math . floor ( containerHeight - childOffset [ axis ] + offset ) ;
224
235
} else {
225
236
position [ axis ] = Math . floor ( childOffset [ axis ] + childOffset [ size ] + offset ) ;
@@ -386,13 +397,13 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
386
397
arrowBoundaryOffset = 0
387
398
} = opts ;
388
399
389
- let container = ( ( overlayNode instanceof HTMLElement && overlayNode . offsetParent ) || document . body ) as Element ;
390
- let isBodyContainer = container . tagName === 'BODY' ;
400
+ let container = overlayNode instanceof HTMLElement ? getContainingBlock ( overlayNode ) : document . documentElement ;
401
+ let isViewportContainer = container === document . documentElement ;
391
402
const containerPositionStyle = window . getComputedStyle ( container ) . position ;
392
403
let isContainerPositioned = ! ! containerPositionStyle && containerPositionStyle !== 'static' ;
393
- let childOffset : Offset = isBodyContainer ? getOffset ( targetNode ) : getPosition ( targetNode , container ) ;
404
+ let childOffset : Offset = isViewportContainer ? getOffset ( targetNode ) : getPosition ( targetNode , container ) ;
394
405
395
- if ( ! isBodyContainer ) {
406
+ if ( ! isViewportContainer ) {
396
407
let { marginTop, marginLeft} = window . getComputedStyle ( targetNode ) ;
397
408
childOffset . top += parseInt ( marginTop , 10 ) || 0 ;
398
409
childOffset . left += parseInt ( marginLeft , 10 ) || 0 ;
@@ -457,3 +468,54 @@ function getPosition(node: Element, parent: Element): Offset {
457
468
offset . left -= parseInt ( style . marginLeft , 10 ) || 0 ;
458
469
return offset ;
459
470
}
471
+
472
+ // Returns the containing block of an element, which is the element that
473
+ // this element will be positioned relative to.
474
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block
475
+ function getContainingBlock ( node : HTMLElement ) : Element {
476
+ // The offsetParent of an element in most cases equals the containing block.
477
+ // https://w3c.github.io/csswg-drafts/cssom-view/#dom-htmlelement-offsetparent
478
+ let offsetParent = node . offsetParent ;
479
+
480
+ // The offsetParent algorithm terminates at the document body,
481
+ // even if the body is not a containing block. Double check that
482
+ // and use the documentElement if so.
483
+ if (
484
+ offsetParent &&
485
+ offsetParent === document . body &&
486
+ window . getComputedStyle ( offsetParent ) . position === 'static' &&
487
+ ! isContainingBlock ( offsetParent )
488
+ ) {
489
+ offsetParent = document . documentElement ;
490
+ }
491
+
492
+ // TODO(later): handle table elements?
493
+
494
+ // The offsetParent can be null if the element has position: fixed, or a few other cases.
495
+ // We have to walk up the tree manually in this case because fixed positioned elements
496
+ // are still positioned relative to their containing block, which is not always the viewport.
497
+ if ( offsetParent == null ) {
498
+ offsetParent = node . parentElement ;
499
+ while ( offsetParent && ! isContainingBlock ( offsetParent ) ) {
500
+ offsetParent = offsetParent . parentElement ;
501
+ }
502
+ }
503
+
504
+ // Fall back to the viewport.
505
+ return offsetParent || document . documentElement ;
506
+ }
507
+
508
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
509
+ function isContainingBlock ( node : Element ) : boolean {
510
+ let style = window . getComputedStyle ( node ) ;
511
+ return (
512
+ style . transform !== 'none' ||
513
+ / t r a n s f o r m | p e r s p e c t i v e / . test ( style . willChange ) ||
514
+ style . filter !== 'none' ||
515
+ style . contain === 'paint' ||
516
+ // @ts -ignore
517
+ ( 'backdropFilter' in style && style . backdropFilter !== 'none' ) ||
518
+ // @ts -ignore
519
+ ( 'WebkitBackdropFilter' in style && style . WebkitBackdropFilter !== 'none' )
520
+ ) ;
521
+ }
0 commit comments