1313import { ariaHideOutside } from './ariaHideOutside' ;
1414import { AriaOverlayProps , useOverlay } from './useOverlay' ;
1515import { DOMAttributes , RefObject } from '@react-types/shared' ;
16+ import { isElementVisible } from '../../utils/src/isElementVisible' ;
1617import { mergeProps } from '@react-aria/utils' ;
1718import { OverlayTriggerState } from '@react-stately/overlays' ;
1819import { useEffect } from 'react' ;
@@ -58,7 +59,7 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
5859
5960 useEffect ( ( ) => {
6061 if ( state . isOpen && ref . current ) {
61- return ariaHideOutside ( [ ref . current ] , { shouldUseInert : true } ) ;
62+ return hideElementsBehind ( ref . current ) ;
6263 }
6364 } , [ state . isOpen , ref ] ) ;
6465
@@ -67,3 +68,120 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
6768 underlayProps
6869 } ;
6970}
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 ( baseElement . compareDocumentPosition ( element ) & 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