fix(react): overlays shown with useIonModal and useIonPopover no longer render outside of main react tree

closes #23516 and #23516
This commit is contained in:
Ely Lucas
2021-10-15 15:29:25 -06:00
committed by GitHub
parent 3451a34ad0
commit f3e492c897
13 changed files with 197 additions and 46 deletions

View File

@ -0,0 +1,63 @@
import { JSX as LocalJSX } from '@ionic/core/components';
import React from 'react';
import { IonContext, IonContextInterface } from '../contexts/IonContext';
import { ReactComponentOrElement } from '../models';
import { IonOverlayManager } from './IonOverlayManager';
import { IonicReactProps } from './IonicReactProps';
import { IonAppInner } from './inner-proxies';
type Props = LocalJSX.IonApp &
IonicReactProps & {
ref?: React.Ref<HTMLIonAppElement>;
};
export class IonApp extends React.Component<Props> {
addOverlayCallback?: (id: string, overlay: any, containerElement: HTMLDivElement) => void;
removeOverlayCallback?: (id: string) => void;
constructor(props: Props) {
super(props);
}
/*
Wire up methods to call into IonOverlayManager
*/
ionContext: IonContextInterface = {
addOverlay: (
id: string,
overlay: ReactComponentOrElement,
containerElement: HTMLDivElement
) => {
if (this.addOverlayCallback) {
this.addOverlayCallback(id, overlay, containerElement);
}
},
removeOverlay: (id: string) => {
if (this.removeOverlayCallback) {
this.removeOverlayCallback(id);
}
},
};
render() {
return (
<IonContext.Provider value={this.ionContext}>
<IonAppInner {...this.props}>{this.props.children}</IonAppInner>
<IonOverlayManager
onAddOverlay={(callback) => {
this.addOverlayCallback = callback;
}}
onRemoveOverlay={(callback) => {
this.removeOverlayCallback = callback;
}}
/>
</IonContext.Provider>
);
}
static get displayName() {
return 'IonApp';
}
}

View File

@ -0,0 +1,67 @@
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { ReactComponentOrElement } from '../models';
interface IonOverlayManagerProps {
onAddOverlay: (
callback: (
id: string,
component: ReactComponentOrElement,
containerElement: HTMLDivElement
) => void
) => void;
onRemoveOverlay: (callback: (id: string) => void) => void;
}
/**
* Manages overlays that are added via the useOverlay hook.
* This is a standalone component so changes to its children don't cause other descendant
* components to re-render when overlays are added. However, we need to communicate with the IonContext
* that is set up in <IonApp />, so we register callbacks so when overlays are added to IonContext,
* they ultimately added here.
*/
export const IonOverlayManager: React.FC<IonOverlayManagerProps> = ({
onAddOverlay,
onRemoveOverlay,
}) => {
const [overlays, setOverlays] = useState<{
[key: string]: {
component: any;
containerElement: HTMLDivElement;
};
}>({});
useEffect(() => {
/* Setup the callbacks that get called from <IonApp /> */
onAddOverlay(addOverlay);
onRemoveOverlay(removeOverlay);
}, []);
const addOverlay = (
id: string,
component: ReactComponentOrElement,
containerElement: HTMLDivElement
) => {
const newOverlays = { ...overlays };
newOverlays[id] = { component, containerElement };
setOverlays(newOverlays);
};
const removeOverlay = (id: string) => {
const newOverlays = { ...overlays };
delete newOverlays[id];
setOverlays(newOverlays);
};
const overlayKeys = Object.keys(overlays);
return (
<>
{overlayKeys.map((key) => {
const overlay = overlays[key];
return ReactDOM.createPortal(overlay.component, overlay.containerElement, `overlay-${key}`);
})}
</>
);
};

View File

@ -142,6 +142,7 @@ export { IonModal } from './IonModal';
export { IonPopover } from './IonPopover';
// Custom Components
export { IonApp } from './IonApp';
export { IonPage } from './IonPage';
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
export { IonTabs } from './navigation/IonTabs';

View File

@ -1,4 +1,5 @@
import { JSX } from '@ionic/core/components';
import { IonApp as IonAppCmp } from '@ionic/core/components/ion-app.js';
import { IonBackButton as IonBackButtonCmp } from '@ionic/core/components/ion-back-button.js';
import { IonRouterOutlet as IonRouterOutletCmp } from '@ionic/core/components/ion-router-outlet.js';
import { IonTabBar as IonTabBarCmp } from '@ionic/core/components/ion-tab-bar.js';
@ -28,6 +29,13 @@ export const IonRouterOutletInner = /*@__PURE__*/ createReactComponent<
HTMLIonRouterOutletElement
>('ion-router-outlet', undefined, undefined, IonRouterOutletCmp);
export const IonAppInner = /*@__PURE__*/ createReactComponent<JSX.IonApp, HTMLIonAppElement>(
'ion-app',
undefined,
undefined,
IonAppCmp
);
// ionicons
export const IonIconInner = /*@__PURE__*/ createReactComponent<
IoniconsJSX.IonIcon,

View File

@ -7,7 +7,6 @@ import type { JSX } from '@ionic/core/components';
import { IonAccordion as IonAccordionCmp } from '@ionic/core/components/ion-accordion.js';
import { IonAccordionGroup as IonAccordionGroupCmp } from '@ionic/core/components/ion-accordion-group.js';
import { IonApp as IonAppCmp } from '@ionic/core/components/ion-app.js';
import { IonAvatar as IonAvatarCmp } from '@ionic/core/components/ion-avatar.js';
import { IonBackdrop as IonBackdropCmp } from '@ionic/core/components/ion-backdrop.js';
import { IonBadge as IonBadgeCmp } from '@ionic/core/components/ion-badge.js';
@ -76,7 +75,6 @@ import { IonVirtualScroll as IonVirtualScrollCmp } from '@ionic/core/components/
export const IonAccordion = /*@__PURE__*/createReactComponent<JSX.IonAccordion, HTMLIonAccordionElement>('ion-accordion', undefined, undefined, IonAccordionCmp);
export const IonAccordionGroup = /*@__PURE__*/createReactComponent<JSX.IonAccordionGroup, HTMLIonAccordionGroupElement>('ion-accordion-group', undefined, undefined, IonAccordionGroupCmp);
export const IonApp = /*@__PURE__*/createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app', undefined, undefined, IonAppCmp);
export const IonAvatar = /*@__PURE__*/createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>('ion-avatar', undefined, undefined, IonAvatarCmp);
export const IonBackdrop = /*@__PURE__*/createReactComponent<JSX.IonBackdrop, HTMLIonBackdropElement>('ion-backdrop', undefined, undefined, IonBackdropCmp);
export const IonBadge = /*@__PURE__*/createReactComponent<JSX.IonBadge, HTMLIonBadgeElement>('ion-badge', undefined, undefined, IonBadgeCmp);