chore(): sync with main

This commit is contained in:
Liam DeBeasi
2023-03-03 12:00:21 -05:00
196 changed files with 293 additions and 326 deletions

View File

@ -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);
};

View 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;
}
};