mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 17:42:15 +08:00
chore(): sync with main
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import type { OverlayEventDetail } from '@ionic/core/components';
|
||||
import type { HTMLIonOverlayElement, OverlayEventDetail } from '@ionic/core/components';
|
||||
import React, { createElement } from 'react';
|
||||
|
||||
import {
|
||||
@ -9,6 +9,7 @@ import {
|
||||
mergeRefs,
|
||||
} from './react-component-lib/utils';
|
||||
import { createForwardRef } from './utils';
|
||||
import { detachProps } from './utils/detachProps';
|
||||
|
||||
// TODO(FW-2959): types
|
||||
|
||||
@ -35,7 +36,7 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
||||
}
|
||||
const displayName = dashToPascalCase(tagName);
|
||||
const ReactComponent = class extends React.Component<IonicReactInternalProps<PropType>, InlineOverlayState> {
|
||||
ref: React.RefObject<HTMLElement>;
|
||||
ref: React.RefObject<HTMLIonOverlayElement>;
|
||||
wrapperRef: React.RefObject<HTMLElement>;
|
||||
stableMergedRefs: React.RefCallback<HTMLElement>;
|
||||
|
||||
@ -54,60 +55,9 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
||||
componentDidMount() {
|
||||
this.componentDidUpdate(this.props);
|
||||
|
||||
/**
|
||||
* Mount the inner component when the
|
||||
* overlay is about to open.
|
||||
*
|
||||
* For ion-popover, this is when `ionMount` is emitted.
|
||||
* For other overlays, this is when `willPresent` is emitted.
|
||||
*/
|
||||
this.ref.current?.addEventListener('ionMount', () => {
|
||||
this.setState({ isOpen: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Mount the inner component
|
||||
* when overlay is about to open.
|
||||
* Also manually call the onWillPresent
|
||||
* handler if present as setState will
|
||||
* cause the event handlers to be
|
||||
* destroyed and re-created.
|
||||
*/
|
||||
this.ref.current?.addEventListener('willPresent', (evt: any) => {
|
||||
this.setState({ isOpen: true });
|
||||
|
||||
this.props.onWillPresent && this.props.onWillPresent(evt);
|
||||
});
|
||||
|
||||
/**
|
||||
* Unmount the inner component.
|
||||
* React will call Node.removeChild
|
||||
* which expects the child to be
|
||||
* a direct descendent of the parent
|
||||
* but due to the presence of
|
||||
* Web Component slots, this is not
|
||||
* always the case. To work around this
|
||||
* we move the inner component to the root
|
||||
* of the Web Component so React can
|
||||
* cleanup properly.
|
||||
*/
|
||||
this.ref.current?.addEventListener('didDismiss', (evt: any) => {
|
||||
const wrapper = this.wrapperRef.current;
|
||||
const el = this.ref.current;
|
||||
|
||||
/**
|
||||
* This component might be unmounted already, if the containing
|
||||
* element was removed while the popover was still open. (For
|
||||
* example, if an item contains an inline popover with a button
|
||||
* that removes the item.)
|
||||
*/
|
||||
if (wrapper && el) {
|
||||
el.append(wrapper);
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
|
||||
this.props.onDidDismiss && this.props.onDidDismiss(evt);
|
||||
});
|
||||
this.ref.current?.addEventListener('ionMount', this.handleIonMount);
|
||||
this.ref.current?.addEventListener('willPresent', this.handleWillPresent);
|
||||
this.ref.current?.addEventListener('didDismiss', this.handleDidDismiss);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IonicReactInternalProps<PropType>) {
|
||||
@ -115,6 +65,35 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
||||
attachProps(node, this.props, prevProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const node = this.ref.current;
|
||||
/**
|
||||
* If the overlay is being unmounted, but is still
|
||||
* open, this means the unmount was triggered outside
|
||||
* of the overlay being dismissed.
|
||||
*
|
||||
* This can happen with:
|
||||
* - The parent component being unmounted
|
||||
* - The overlay being conditionally rendered
|
||||
* - A route change (push/pop/replace)
|
||||
*
|
||||
* Unmounting the overlay at this stage should skip
|
||||
* the dismiss lifecycle, including skipping the transition.
|
||||
*
|
||||
*/
|
||||
if (node && this.state.isOpen) {
|
||||
/**
|
||||
* Detach the local event listener that performs the state updates,
|
||||
* before dismissing the overlay, to prevent the callback handlers
|
||||
* executing after the component has been unmounted. This is to
|
||||
* avoid memory leaks.
|
||||
*/
|
||||
node.removeEventListener('didDismiss', this.handleDidDismiss);
|
||||
node.remove();
|
||||
detachProps(node, this.props);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { children, forwardedRef, style, className, ref, ...cProps } = this.props;
|
||||
@ -172,6 +151,46 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
||||
static get displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
private handleIonMount = () => {
|
||||
/**
|
||||
* Mount the inner component when the
|
||||
* overlay is about to open.
|
||||
*
|
||||
* For ion-popover, this is when `ionMount` is emitted.
|
||||
* For other overlays, this is when `willPresent` is emitted.
|
||||
*/
|
||||
this.setState({ isOpen: true });
|
||||
};
|
||||
|
||||
private handleWillPresent = (evt: any) => {
|
||||
this.setState({ isOpen: true });
|
||||
/**
|
||||
* Manually call the onWillPresent
|
||||
* handler if present as setState will
|
||||
* cause the event handlers to be
|
||||
* destroyed and re-created.
|
||||
*/
|
||||
this.props.onWillPresent && this.props.onWillPresent(evt);
|
||||
};
|
||||
|
||||
private handleDidDismiss = (evt: any) => {
|
||||
const wrapper = this.wrapperRef.current;
|
||||
const el = this.ref.current;
|
||||
|
||||
/**
|
||||
* This component might be unmounted already, if the containing
|
||||
* element was removed while the overlay was still open. (For
|
||||
* example, if an item contains an inline overlay with a button
|
||||
* that removes the item.)
|
||||
*/
|
||||
if (wrapper && el) {
|
||||
el.append(wrapper);
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
|
||||
this.props.onDidDismiss && this.props.onDidDismiss(evt);
|
||||
};
|
||||
};
|
||||
return createForwardRef<PropType, ElementType>(ReactComponent, displayName);
|
||||
};
|
||||
|
42
packages/react/src/components/utils/detachProps.ts
Normal file
42
packages/react/src/components/utils/detachProps.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { isCoveredByReact } from '../react-component-lib/utils';
|
||||
|
||||
/**
|
||||
* The @stencil/react-output-target will bind event listeners for any
|
||||
* attached props that use the `on` prefix. This function will remove
|
||||
* those event listeners when the component is unmounted.
|
||||
*
|
||||
* This prevents memory leaks and React state updates on unmounted components.
|
||||
*/
|
||||
export const detachProps = (node: HTMLElement, props: any) => {
|
||||
if (node instanceof Element) {
|
||||
Object.keys(props).forEach((name) => {
|
||||
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
|
||||
const eventName = name.substring(2);
|
||||
const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1);
|
||||
if (!isCoveredByReact(eventNameLc)) {
|
||||
/**
|
||||
* Detach custom event bindings (not built-in React events)
|
||||
* that were added by the @stencil/react-output-target attachProps function.
|
||||
*/
|
||||
detachEvent(node, eventNameLc);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const detachEvent = (
|
||||
node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } },
|
||||
eventName: string
|
||||
) => {
|
||||
const eventStore = node.__events || (node.__events = {});
|
||||
/**
|
||||
* If the event listener was added by attachProps, it will
|
||||
* be stored in the __events object.
|
||||
*/
|
||||
const eventHandler = eventStore[eventName];
|
||||
if (eventHandler) {
|
||||
node.removeEventListener(eventName, eventHandler);
|
||||
eventStore[eventName] = undefined;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user