mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
fix(react): IonNav works with react (#25565)
Resolves #24002 Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
This commit is contained in:
@ -132,6 +132,7 @@ export { IonPopover } from './IonPopover';
|
|||||||
// Custom Components
|
// Custom Components
|
||||||
export { IonApp } from './IonApp';
|
export { IonApp } from './IonApp';
|
||||||
export { IonPage } from './IonPage';
|
export { IonPage } from './IonPage';
|
||||||
|
export { IonNav } from './navigation/IonNav';
|
||||||
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
|
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
|
||||||
export { IonTabs } from './navigation/IonTabs';
|
export { IonTabs } from './navigation/IonTabs';
|
||||||
export { IonTabBar } from './navigation/IonTabBar';
|
export { IonTabBar } from './navigation/IonTabBar';
|
||||||
|
@ -21,7 +21,15 @@ export const IonBackButton = /*@__PURE__*/ (() =>
|
|||||||
context!: React.ContextType<typeof NavContext>;
|
context!: React.ContextType<typeof NavContext>;
|
||||||
|
|
||||||
clickButton = (e: React.MouseEvent) => {
|
clickButton = (e: React.MouseEvent) => {
|
||||||
|
/**
|
||||||
|
* If ion-back-button is being used inside
|
||||||
|
* of ion-nav then we should not interact with
|
||||||
|
* the router.
|
||||||
|
*/
|
||||||
|
if (e.target && (e.target as HTMLElement).closest('ion-nav') !== null) { return; }
|
||||||
|
|
||||||
const { defaultHref, routerAnimation } = this.props;
|
const { defaultHref, routerAnimation } = this.props;
|
||||||
|
|
||||||
if (this.context.hasIonicRouter()) {
|
if (this.context.hasIonicRouter()) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.context.goBack(defaultHref, routerAnimation);
|
this.context.goBack(defaultHref, routerAnimation);
|
||||||
|
30
packages/react/src/components/navigation/IonNav.tsx
Normal file
30
packages/react/src/components/navigation/IonNav.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { FrameworkDelegate, JSX } from '@ionic/core/components';
|
||||||
|
import { defineCustomElement } from '@ionic/core/components/ion-nav.js';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { ReactDelegate } from '../../framework-delegate';
|
||||||
|
import { createReactComponent } from '../react-component-lib';
|
||||||
|
|
||||||
|
const IonNavInner = createReactComponent<
|
||||||
|
JSX.IonNav & { delegate: FrameworkDelegate },
|
||||||
|
HTMLIonNavElement
|
||||||
|
>('ion-nav', undefined, undefined, defineCustomElement);
|
||||||
|
|
||||||
|
export const IonNav: React.FC<JSX.IonNav> = ({ children, ...restOfProps }) => {
|
||||||
|
const [views, setViews] = useState<React.ReactPortal[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows us to create React components that are rendered within
|
||||||
|
* the context of the IonNav component.
|
||||||
|
*/
|
||||||
|
const addView = (view: React.ReactPortal) => setViews([...views, view]);
|
||||||
|
const removeView = (view: React.ReactPortal) => setViews(views.filter((v) => v !== view));
|
||||||
|
|
||||||
|
const delegate = ReactDelegate(addView, removeView);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonNavInner delegate={delegate} {...restOfProps}>
|
||||||
|
{views}
|
||||||
|
</IonNavInner>
|
||||||
|
);
|
||||||
|
};
|
38
packages/react/src/framework-delegate.tsx
Normal file
38
packages/react/src/framework-delegate.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { FrameworkDelegate } from '@ionic/core/components';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
export const ReactDelegate = (
|
||||||
|
addView: (view: React.ReactPortal) => void,
|
||||||
|
removeView: (view: React.ReactPortal) => void
|
||||||
|
): FrameworkDelegate => {
|
||||||
|
let Component: React.ReactPortal;
|
||||||
|
|
||||||
|
const attachViewToDom = async (
|
||||||
|
parentElement: HTMLElement,
|
||||||
|
component: () => JSX.Element,
|
||||||
|
propsOrDataObj?: any,
|
||||||
|
cssClasses?: string[]
|
||||||
|
): Promise<any> => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
cssClasses && div.classList.add(...cssClasses);
|
||||||
|
parentElement.appendChild(div);
|
||||||
|
|
||||||
|
Component = createPortal(component(), div);
|
||||||
|
|
||||||
|
Component.props = propsOrDataObj;
|
||||||
|
|
||||||
|
addView(Component);
|
||||||
|
|
||||||
|
return Promise.resolve(div);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeViewFromDom = (): Promise<void> => {
|
||||||
|
Component && removeView(Component);
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
attachViewToDom,
|
||||||
|
removeViewFromDom,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
describe('IonNav', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/navigation');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the root page', () => {
|
||||||
|
cy.get('ion-nav').contains('Page one content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should push a page', () => {
|
||||||
|
cy.get('ion-button').contains('Go to Page Two').click();
|
||||||
|
cy.get('#pageTwoContent').should('be.visible');
|
||||||
|
cy.get('ion-nav').contains('Page two content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pop a page', () => {
|
||||||
|
cy.get('ion-button').contains('Go to Page Two').click();
|
||||||
|
|
||||||
|
cy.get('#pageTwoContent').should('be.visible');
|
||||||
|
cy.get('ion-nav').contains('Page two content');
|
||||||
|
|
||||||
|
cy.get('.ion-page.can-go-back ion-back-button').click();
|
||||||
|
|
||||||
|
cy.get('#pageOneContent').should('be.visible');
|
||||||
|
cy.get('ion-nav').contains('Page one content');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -25,6 +25,7 @@ import Main from './pages/Main';
|
|||||||
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
|
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
|
||||||
import OverlayComponents from './pages/overlay-components/OverlayComponents';
|
import OverlayComponents from './pages/overlay-components/OverlayComponents';
|
||||||
import Tabs from './pages/Tabs';
|
import Tabs from './pages/Tabs';
|
||||||
|
import NavComponent from './pages/navigation/NavComponent';
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ const App: React.FC = () => (
|
|||||||
<Route path="/" component={Main} />
|
<Route path="/" component={Main} />
|
||||||
<Route path="/overlay-hooks" component={OverlayHooks} />
|
<Route path="/overlay-hooks" component={OverlayHooks} />
|
||||||
<Route path="/overlay-components" component={OverlayComponents} />
|
<Route path="/overlay-components" component={OverlayComponents} />
|
||||||
|
<Route path="/navigation" component={NavComponent} />
|
||||||
<Route path="/tabs" component={Tabs} />
|
<Route path="/tabs" component={Tabs} />
|
||||||
</IonRouterOutlet>
|
</IonRouterOutlet>
|
||||||
</IonReactRouter>
|
</IonReactRouter>
|
||||||
|
@ -31,6 +31,11 @@ const Main: React.FC<MainProps> = () => {
|
|||||||
<IonLabel>Overlay Components</IonLabel>
|
<IonLabel>Overlay Components</IonLabel>
|
||||||
</IonItem>
|
</IonItem>
|
||||||
</IonList>
|
</IonList>
|
||||||
|
<IonList>
|
||||||
|
<IonItem routerLink="/navigation">
|
||||||
|
<IonLabel>Navigation</IonLabel>
|
||||||
|
</IonItem>
|
||||||
|
</IonList>
|
||||||
<IonList>
|
<IonList>
|
||||||
<IonItem routerLink="/tabs">
|
<IonItem routerLink="/tabs">
|
||||||
<IonLabel>Tabs</IonLabel>
|
<IonLabel>Tabs</IonLabel>
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonContent,
|
||||||
|
IonHeader,
|
||||||
|
IonLabel,
|
||||||
|
IonNav,
|
||||||
|
IonNavLink,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
IonButtons,
|
||||||
|
IonBackButton,
|
||||||
|
IonPage,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const NavComponent: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonNav
|
||||||
|
root={() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Page One</IonTitle>
|
||||||
|
<IonButtons>
|
||||||
|
<IonBackButton />
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent id="pageOneContent">
|
||||||
|
<IonLabel>Page one content</IonLabel>
|
||||||
|
<IonNavLink
|
||||||
|
routerDirection="forward"
|
||||||
|
component={() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Page Two</IonTitle>
|
||||||
|
<IonButtons>
|
||||||
|
<IonBackButton />
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent id="pageTwoContent">
|
||||||
|
<IonLabel>Page two content</IonLabel>
|
||||||
|
<IonNavLink
|
||||||
|
routerDirection="forward"
|
||||||
|
component={() => (
|
||||||
|
<>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Page Three</IonTitle>
|
||||||
|
<IonButtons>
|
||||||
|
<IonBackButton />
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent>
|
||||||
|
<IonLabel>Page three content</IonLabel>
|
||||||
|
</IonContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IonButton>Go to Page Three</IonButton>
|
||||||
|
</IonNavLink>
|
||||||
|
</IonContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonButton>Go to Page Two</IonButton>
|
||||||
|
</IonNavLink>
|
||||||
|
</IonContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></IonNav>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavComponent;
|
@ -49,7 +49,7 @@ const ModalHook: React.FC = () => {
|
|||||||
setCount(count + 1);
|
setCount(count + 1);
|
||||||
}, [count, setCount]);
|
}, [count, setCount]);
|
||||||
|
|
||||||
const handleDismissWithComponent = useCallback((data, role) => {
|
const handleDismissWithComponent = useCallback((data: any, role: string) => {
|
||||||
dismissWithComponent(data, role);
|
dismissWithComponent(data, role);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
Reference in New Issue
Block a user