@@ -96,6 +96,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
9696 private viewTransitionAnimation ?: Animation ;
9797 private resizeTimeout ?: any ;
9898
99+ // Mutation observer to watch for parent removal
100+ private parentRemovalObserver ?: MutationObserver ;
101+ // Cached original parent from before modal is moved to body during presentation
102+ private cachedOriginalParent ?: HTMLElement ;
103+
99104 lastFocus ?: HTMLElement ;
100105 animation ?: Animation ;
101106
@@ -398,6 +403,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
398403 disconnectedCallback ( ) {
399404 this . triggerController . removeClickListener ( ) ;
400405 this . cleanupViewTransitionListener ( ) ;
406+ this . cleanupParentRemovalObserver ( ) ;
401407 }
402408
403409 componentWillLoad ( ) {
@@ -407,6 +413,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
407413 const attributesToInherit = [ 'aria-label' , 'role' ] ;
408414 this . inheritedAttributes = inheritAttributes ( el , attributesToInherit ) ;
409415
416+ // Cache original parent before modal gets moved to body during presentation
417+ if ( el . parentNode ) {
418+ this . cachedOriginalParent = el . parentNode as HTMLElement ;
419+ }
420+
410421 /**
411422 * When using a controller modal you can set attributes
412423 * using the htmlAttributes property. Since the above attributes
@@ -642,6 +653,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
642653 // Initialize view transition listener for iOS card modals
643654 this . initViewTransitionListener ( ) ;
644655
656+ // Initialize parent removal observer
657+ this . initParentRemovalObserver ( ) ;
658+
645659 unlock ( ) ;
646660 }
647661
@@ -847,6 +861,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
847861 this . gesture . destroy ( ) ;
848862 }
849863 this . cleanupViewTransitionListener ( ) ;
864+ this . cleanupParentRemovalObserver ( ) ;
850865 }
851866 this . currentBreakpoint = undefined ;
852867 this . animation = undefined ;
@@ -1150,6 +1165,58 @@ export class Modal implements ComponentInterface, OverlayInterface {
11501165 } ) ;
11511166 }
11521167
1168+ private initParentRemovalObserver ( ) {
1169+ // Only observe if we have a cached parent and are in browser environment
1170+ if ( typeof window === 'undefined' || ! this . cachedOriginalParent ) {
1171+ return ;
1172+ }
1173+
1174+ // Don't observe document or fragment nodes as they can't be "removed"
1175+ if ( this . cachedOriginalParent . nodeType === Node . DOCUMENT_NODE || this . cachedOriginalParent . nodeType === Node . DOCUMENT_FRAGMENT_NODE ) {
1176+ return ;
1177+ }
1178+
1179+ const grandParent = this . cachedOriginalParent . parentNode ;
1180+ if ( ! grandParent ) {
1181+ return ;
1182+ }
1183+
1184+ this . parentRemovalObserver = new MutationObserver ( ( mutations ) => {
1185+ mutations . forEach ( ( mutation ) => {
1186+ if ( mutation . type === 'childList' && mutation . removedNodes . length > 0 ) {
1187+ // Check if our cached original parent was removed
1188+ const cachedParentWasRemoved = Array . from ( mutation . removedNodes ) . some (
1189+ ( node ) => {
1190+ const isDirectMatch = node === this . cachedOriginalParent ;
1191+ const isContainedMatch = this . cachedOriginalParent ? ( node as HTMLElement ) . contains ?.( this . cachedOriginalParent ) : false ;
1192+ return isDirectMatch || isContainedMatch ;
1193+ }
1194+ ) ;
1195+
1196+ // Also check if parent is no longer connected to DOM
1197+ const cachedParentDisconnected = this . cachedOriginalParent && ! this . cachedOriginalParent . isConnected ;
1198+
1199+ if ( cachedParentWasRemoved || cachedParentDisconnected ) {
1200+ this . dismiss ( undefined , 'parent-removed' ) ;
1201+ }
1202+ }
1203+ } ) ;
1204+ } ) ;
1205+
1206+ // Observe with subtree to catch nested removals
1207+ this . parentRemovalObserver . observe ( grandParent , {
1208+ childList : true ,
1209+ subtree : true ,
1210+ } ) ;
1211+ }
1212+
1213+ private cleanupParentRemovalObserver ( ) {
1214+ if ( this . parentRemovalObserver ) {
1215+ this . parentRemovalObserver . disconnect ( ) ;
1216+ this . parentRemovalObserver = undefined ;
1217+ }
1218+ }
1219+
11531220 render ( ) {
11541221 const {
11551222 handle,
0 commit comments