1- import type { OverlayEventDetail } from '@ionic/core/components' ;
1+ import type { HTMLIonOverlayElement , OverlayEventDetail } from '@ionic/core/components' ;
22import React , { createElement } from 'react' ;
33
44import {
99 mergeRefs ,
1010} from './react-component-lib/utils' ;
1111import { createForwardRef } from './utils' ;
12+ import { detachProps } from './utils/detachProps' ;
1213
1314// TODO(FW-2959): types
1415
@@ -35,7 +36,7 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
3536 }
3637 const displayName = dashToPascalCase ( tagName ) ;
3738 const ReactComponent = class extends React . Component < IonicReactInternalProps < PropType > , InlineOverlayState > {
38- ref : React . RefObject < HTMLElement > ;
39+ ref : React . RefObject < HTMLIonOverlayElement > ;
3940 wrapperRef : React . RefObject < HTMLElement > ;
4041 stableMergedRefs : React . RefCallback < HTMLElement > ;
4142
@@ -54,65 +55,43 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
5455 componentDidMount ( ) {
5556 this . componentDidUpdate ( this . props ) ;
5657
57- /**
58- * Mount the inner component when the
59- * overlay is about to open.
60- *
61- * For ion-popover, this is when `ionMount` is emitted.
62- * For other overlays, this is when `willPresent` is emitted.
63- */
64- this . ref . current ?. addEventListener ( 'ionMount' , ( ) => {
65- this . setState ( { isOpen : true } ) ;
66- } ) ;
67-
68- /**
69- * Mount the inner component
70- * when overlay is about to open.
71- * Also manually call the onWillPresent
72- * handler if present as setState will
73- * cause the event handlers to be
74- * destroyed and re-created.
75- */
76- this . ref . current ?. addEventListener ( 'willPresent' , ( evt : any ) => {
77- this . setState ( { isOpen : true } ) ;
58+ this . ref . current ?. addEventListener ( 'ionMount' , this . handleIonMount ) ;
59+ this . ref . current ?. addEventListener ( 'willPresent' , this . handleWillPresent ) ;
60+ this . ref . current ?. addEventListener ( 'didDismiss' , this . handleDidDismiss ) ;
61+ }
7862
79- this . props . onWillPresent && this . props . onWillPresent ( evt ) ;
80- } ) ;
63+ componentDidUpdate ( prevProps : IonicReactInternalProps < PropType > ) {
64+ const node = this . ref . current ! as HTMLElement ;
65+ attachProps ( node , this . props , prevProps ) ;
66+ }
8167
68+ componentWillUnmount ( ) {
69+ const node = this . ref . current ;
8270 /**
83- * Unmount the inner component.
84- * React will call Node.removeChild
85- * which expects the child to be
86- * a direct descendent of the parent
87- * but due to the presence of
88- * Web Component slots, this is not
89- * always the case. To work around this
90- * we move the inner component to the root
91- * of the Web Component so React can
92- * cleanup properly.
71+ * If the overlay is being unmounted, but is still
72+ * open, this means the unmount was triggered outside
73+ * of the overlay being dismissed.
74+ *
75+ * This can happen with:
76+ * - The parent component being unmounted
77+ * - The overlay being conditionally rendered
78+ * - A route change (push/pop/replace)
79+ *
80+ * Unmounting the overlay at this stage should skip
81+ * the dismiss lifecycle, including skipping the transition.
82+ *
9383 */
94- this . ref . current ?. addEventListener ( 'didDismiss' , ( evt : any ) => {
95- const wrapper = this . wrapperRef . current ;
96- const el = this . ref . current ;
97-
84+ if ( node && this . state . isOpen ) {
9885 /**
99- * This component might be unmounted already, if the containing
100- * element was removed while the popover was still open. (For
101- * example, if an item contains an inline popover with a button
102- * that removes the item.)
86+ * Detach the local event listener that performs the state updates,
87+ * before dismissing the overlay, to prevent the callback handlers
88+ * executing after the component has been unmounted. This is to
89+ * avoid memory leaks.
10390 */
104- if ( wrapper && el ) {
105- el . append ( wrapper ) ;
106- this . setState ( { isOpen : false } ) ;
107- }
108-
109- this . props . onDidDismiss && this . props . onDidDismiss ( evt ) ;
110- } ) ;
111- }
112-
113- componentDidUpdate ( prevProps : IonicReactInternalProps < PropType > ) {
114- const node = this . ref . current ! as HTMLElement ;
115- attachProps ( node , this . props , prevProps ) ;
91+ node . removeEventListener ( 'didDismiss' , this . handleDidDismiss ) ;
92+ node . remove ( ) ;
93+ detachProps ( node , this . props ) ;
94+ }
11695 }
11796
11897 render ( ) {
@@ -172,6 +151,46 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
172151 static get displayName ( ) {
173152 return displayName ;
174153 }
154+
155+ private handleIonMount = ( ) => {
156+ /**
157+ * Mount the inner component when the
158+ * overlay is about to open.
159+ *
160+ * For ion-popover, this is when `ionMount` is emitted.
161+ * For other overlays, this is when `willPresent` is emitted.
162+ */
163+ this . setState ( { isOpen : true } ) ;
164+ } ;
165+
166+ private handleWillPresent = ( evt : any ) => {
167+ this . setState ( { isOpen : true } ) ;
168+ /**
169+ * Manually call the onWillPresent
170+ * handler if present as setState will
171+ * cause the event handlers to be
172+ * destroyed and re-created.
173+ */
174+ this . props . onWillPresent && this . props . onWillPresent ( evt ) ;
175+ } ;
176+
177+ private handleDidDismiss = ( evt : any ) => {
178+ const wrapper = this . wrapperRef . current ;
179+ const el = this . ref . current ;
180+
181+ /**
182+ * This component might be unmounted already, if the containing
183+ * element was removed while the overlay was still open. (For
184+ * example, if an item contains an inline overlay with a button
185+ * that removes the item.)
186+ */
187+ if ( wrapper && el ) {
188+ el . append ( wrapper ) ;
189+ this . setState ( { isOpen : false } ) ;
190+ }
191+
192+ this . props . onDidDismiss && this . props . onDidDismiss ( evt ) ;
193+ } ;
175194 } ;
176195 return createForwardRef < PropType , ElementType > ( ReactComponent , displayName ) ;
177196} ;
0 commit comments