13
13
import { ariaHideOutside } from './ariaHideOutside' ;
14
14
import { AriaOverlayProps , useOverlay } from './useOverlay' ;
15
15
import { DOMAttributes , RefObject } from '@react-types/shared' ;
16
+ import { isElementVisible } from '../../utils/src/isElementVisible' ;
16
17
import { mergeProps } from '@react-aria/utils' ;
17
18
import { OverlayTriggerState } from '@react-stately/overlays' ;
18
19
import { useEffect } from 'react' ;
@@ -58,7 +59,7 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
58
59
59
60
useEffect ( ( ) => {
60
61
if ( state . isOpen && ref . current ) {
61
- return ariaHideOutside ( [ ref . current ] , { shouldUseInert : true } ) ;
62
+ return hideElementsBehind ( ref . current ) ;
62
63
}
63
64
} , [ state . isOpen , ref ] ) ;
64
65
@@ -67,3 +68,120 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
67
68
underlayProps
68
69
} ;
69
70
}
71
+
72
+ function hideElementsBehind ( element : Element , root = document . body ) {
73
+ // TODO: automatically determine root based on parent stacking context of element?
74
+ let roots = getStackingContextRoots ( root ) ;
75
+ let rootStackingContext = roots . find ( r => r . contains ( element ) ) || document . documentElement ;
76
+ let elementZIndex = getZIndex ( rootStackingContext ) ;
77
+
78
+ return ariaHideOutside ( [ element ] , {
79
+ shouldUseInert : true ,
80
+ getVisibleNodes : el => {
81
+ let node : Element | null = el ;
82
+ let ancestors : Element [ ] = [ ] ;
83
+ while ( node && node !== root ) {
84
+ ancestors . unshift ( node ) ;
85
+ node = node . parentElement ;
86
+ }
87
+
88
+ // If an ancestor element of the added target is a stacking context root,
89
+ // use that to determine if the element should be preserved.
90
+ let stackingContext = ancestors . find ( el => isStackingContext ( el ) ) ;
91
+ if ( stackingContext ) {
92
+ if ( shouldPreserve ( element , elementZIndex , stackingContext ) ) {
93
+ return [ el ] ;
94
+ }
95
+ return [ ] ;
96
+ } else {
97
+ // Otherwise, find stacking context roots within the added element, and compare with the modal element.
98
+ let roots = getStackingContextRoots ( el ) ;
99
+ let preservedElements : Element [ ] = [ ] ;
100
+ for ( let root of roots ) {
101
+ if ( shouldPreserve ( element , elementZIndex , root ) ) {
102
+ preservedElements . push ( root ) ;
103
+ }
104
+ }
105
+ return preservedElements ;
106
+ }
107
+ }
108
+ } ) ;
109
+ }
110
+
111
+ function shouldPreserve ( baseElement : Element , baseZIndex : number , element : Element ) {
112
+ if ( baseElement . contains ( element ) ) {
113
+ return true ;
114
+ }
115
+
116
+ let zIndex = getZIndex ( element ) ;
117
+ if ( zIndex === baseZIndex ) {
118
+ // If two elements have the same z-index, compare their document order.
119
+ if ( element . compareDocumentPosition ( baseElement ) & Node . DOCUMENT_POSITION_FOLLOWING ) {
120
+ return true ;
121
+ }
122
+ } else if ( zIndex > baseZIndex ) {
123
+ return true ;
124
+ }
125
+
126
+ return false ;
127
+ }
128
+
129
+ function isStackingContext ( el : Element ) {
130
+ let style = getComputedStyle ( el ) ;
131
+
132
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context#features_creating_stacking_contexts
133
+ return (
134
+ el === document . documentElement ||
135
+ ( style . position !== 'static' && style . zIndex !== 'auto' ) ||
136
+ ( 'containerType' in style && style . containerType . includes ( 'size' ) ) ||
137
+ ( style . zIndex !== 'auto' && isFlexOrGridItem ( el ) ) ||
138
+ parseFloat ( style . opacity ) < 1 ||
139
+ ( 'mixBlendMode' in style && style . mixBlendMode !== 'normal' ) ||
140
+ ( 'transform' in style && style . transform !== 'none' ) ||
141
+ ( 'webkitTransform' in style && style . webkitTransform !== 'none' ) ||
142
+ ( 'scale' in style && style . scale !== 'none' ) ||
143
+ ( 'rotate' in style && style . rotate !== 'none' ) ||
144
+ ( 'translate' in style && style . translate !== 'none' ) ||
145
+ ( 'filter' in style && style . filter !== 'none' ) ||
146
+ ( 'webkitFilter' in style && style . webkitFilter !== 'none' ) ||
147
+ ( 'backdropFilter' in style && style . backdropFilter !== 'none' ) ||
148
+ ( 'perspective' in style && style . perspective !== 'none' ) ||
149
+ ( 'clipPath' in style && style . clipPath !== 'none' ) ||
150
+ ( 'mask' in style && style . mask !== 'none' ) ||
151
+ ( 'maskImage' in style && style . maskImage !== 'none' ) ||
152
+ ( 'maskBorder' in style && style . maskBorder !== 'none' ) ||
153
+ style . isolation === 'isolate' ||
154
+ / p o s i t i o n | z - i n d e x | o p a c i t y | m i x - b l e n d - m o d e | t r a n s f o r m | w e b k i t - t r a n s f o r m | s c a l e | r o t a t e | t r a n s l a t e | f i l t e r | w e b k i t - f i l t e r | b a c k d r o p - f i l t e r | p e r s p e c t i v e | c l i p - p a t h | m a s k | m a s k - i m a g e | m a s k - b o r d e r | i s o l a t i o n / . test ( style . willChange ) ||
155
+ / l a y o u t | p a i n t | s t r i c t | c o n t e n t / . test ( style . contain )
156
+ ) ;
157
+ }
158
+
159
+ function getStackingContextRoots ( root : Element = document . body ) {
160
+ let roots : Element [ ] = [ ] ;
161
+
162
+ function walk ( el : Element ) {
163
+ if ( ! isElementVisible ( el ) ) {
164
+ return ;
165
+ }
166
+
167
+ if ( isStackingContext ( el ) ) {
168
+ roots . push ( el ) ;
169
+ } else {
170
+ for ( const child of el . children ) {
171
+ walk ( child ) ;
172
+ }
173
+ }
174
+ }
175
+
176
+ walk ( root ) ;
177
+ return roots ;
178
+ }
179
+
180
+ function getZIndex ( element : Element ) {
181
+ return Number ( getComputedStyle ( element ) . zIndex ) || 0 ;
182
+ }
183
+
184
+ function isFlexOrGridItem ( element : Element ) {
185
+ let parent = element . parentElement ;
186
+ return parent && / f l e x | g r i d / . test ( getComputedStyle ( parent ) . display ) ;
187
+ }
0 commit comments