mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
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:
63
packages/react/src/components/IonApp.tsx
Normal file
63
packages/react/src/components/IonApp.tsx
Normal 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';
|
||||
}
|
||||
}
|
67
packages/react/src/components/IonOverlayManager.tsx
Normal file
67
packages/react/src/components/IonOverlayManager.tsx
Normal 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}`);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user