Skip to content

Commit 420f9bb

Browse files
fix(react): IonNav works with react (#25565)
Resolves #24002 Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
1 parent ffb0311 commit 420f9bb

File tree

9 files changed

+197
-1
lines changed

9 files changed

+197
-1
lines changed

packages/react/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export { IonPopover } from './IonPopover';
132132
// Custom Components
133133
export { IonApp } from './IonApp';
134134
export { IonPage } from './IonPage';
135+
export { IonNav } from './navigation/IonNav';
135136
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
136137
export { IonTabs } from './navigation/IonTabs';
137138
export { IonTabBar } from './navigation/IonTabBar';

packages/react/src/components/navigation/IonBackButton.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ export const IonBackButton = /*@__PURE__*/ (() =>
2121
context!: React.ContextType<typeof NavContext>;
2222

2323
clickButton = (e: React.MouseEvent) => {
24+
/**
25+
* If ion-back-button is being used inside
26+
* of ion-nav then we should not interact with
27+
* the router.
28+
*/
29+
if (e.target && (e.target as HTMLElement).closest('ion-nav') !== null) { return; }
30+
2431
const { defaultHref, routerAnimation } = this.props;
32+
2533
if (this.context.hasIonicRouter()) {
2634
e.stopPropagation();
2735
this.context.goBack(defaultHref, routerAnimation);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { FrameworkDelegate, JSX } from '@ionic/core/components';
2+
import { defineCustomElement } from '@ionic/core/components/ion-nav.js';
3+
import React, { useState } from 'react';
4+
5+
import { ReactDelegate } from '../../framework-delegate';
6+
import { createReactComponent } from '../react-component-lib';
7+
8+
const IonNavInner = createReactComponent<
9+
JSX.IonNav & { delegate: FrameworkDelegate },
10+
HTMLIonNavElement
11+
>('ion-nav', undefined, undefined, defineCustomElement);
12+
13+
export const IonNav: React.FC<JSX.IonNav> = ({ children, ...restOfProps }) => {
14+
const [views, setViews] = useState<React.ReactPortal[]>([]);
15+
16+
/**
17+
* Allows us to create React components that are rendered within
18+
* the context of the IonNav component.
19+
*/
20+
const addView = (view: React.ReactPortal) => setViews([...views, view]);
21+
const removeView = (view: React.ReactPortal) => setViews(views.filter((v) => v !== view));
22+
23+
const delegate = ReactDelegate(addView, removeView);
24+
25+
return (
26+
<IonNavInner delegate={delegate} {...restOfProps}>
27+
{views}
28+
</IonNavInner>
29+
);
30+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { FrameworkDelegate } from '@ionic/core/components';
2+
import { createPortal } from 'react-dom';
3+
4+
export const ReactDelegate = (
5+
addView: (view: React.ReactPortal) => void,
6+
removeView: (view: React.ReactPortal) => void
7+
): FrameworkDelegate => {
8+
let Component: React.ReactPortal;
9+
10+
const attachViewToDom = async (
11+
parentElement: HTMLElement,
12+
component: () => JSX.Element,
13+
propsOrDataObj?: any,
14+
cssClasses?: string[]
15+
): Promise<any> => {
16+
const div = document.createElement('div');
17+
cssClasses && div.classList.add(...cssClasses);
18+
parentElement.appendChild(div);
19+
20+
Component = createPortal(component(), div);
21+
22+
Component.props = propsOrDataObj;
23+
24+
addView(Component);
25+
26+
return Promise.resolve(div);
27+
};
28+
29+
const removeViewFromDom = (): Promise<void> => {
30+
Component && removeView(Component);
31+
return Promise.resolve();
32+
};
33+
34+
return {
35+
attachViewToDom,
36+
removeViewFromDom,
37+
};
38+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
describe('IonNav', () => {
2+
beforeEach(() => {
3+
cy.visit('/navigation');
4+
});
5+
6+
it('should render the root page', () => {
7+
cy.get('ion-nav').contains('Page one content');
8+
});
9+
10+
it('should push a page', () => {
11+
cy.get('ion-button').contains('Go to Page Two').click();
12+
cy.get('#pageTwoContent').should('be.visible');
13+
cy.get('ion-nav').contains('Page two content');
14+
});
15+
16+
it('should pop a page', () => {
17+
cy.get('ion-button').contains('Go to Page Two').click();
18+
19+
cy.get('#pageTwoContent').should('be.visible');
20+
cy.get('ion-nav').contains('Page two content');
21+
22+
cy.get('.ion-page.can-go-back ion-back-button').click();
23+
24+
cy.get('#pageOneContent').should('be.visible');
25+
cy.get('ion-nav').contains('Page one content');
26+
});
27+
28+
});

packages/react/test-app/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Main from './pages/Main';
2525
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
2626
import OverlayComponents from './pages/overlay-components/OverlayComponents';
2727
import Tabs from './pages/Tabs';
28+
import NavComponent from './pages/navigation/NavComponent';
2829

2930
setupIonicReact();
3031

@@ -35,6 +36,7 @@ const App: React.FC = () => (
3536
<Route path="/" component={Main} />
3637
<Route path="/overlay-hooks" component={OverlayHooks} />
3738
<Route path="/overlay-components" component={OverlayComponents} />
39+
<Route path="/navigation" component={NavComponent} />
3840
<Route path="/tabs" component={Tabs} />
3941
</IonRouterOutlet>
4042
</IonReactRouter>

packages/react/test-app/src/pages/Main.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const Main: React.FC<MainProps> = () => {
3131
<IonLabel>Overlay Components</IonLabel>
3232
</IonItem>
3333
</IonList>
34+
<IonList>
35+
<IonItem routerLink="/navigation">
36+
<IonLabel>Navigation</IonLabel>
37+
</IonItem>
38+
</IonList>
3439
<IonList>
3540
<IonItem routerLink="/tabs">
3641
<IonLabel>Tabs</IonLabel>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
IonButton,
3+
IonContent,
4+
IonHeader,
5+
IonLabel,
6+
IonNav,
7+
IonNavLink,
8+
IonTitle,
9+
IonToolbar,
10+
IonButtons,
11+
IonBackButton,
12+
IonPage,
13+
} from '@ionic/react';
14+
import React from 'react';
15+
16+
const NavComponent: React.FC = () => {
17+
return (
18+
<IonPage>
19+
<IonNav
20+
root={() => {
21+
return (
22+
<>
23+
<IonHeader>
24+
<IonToolbar>
25+
<IonTitle>Page One</IonTitle>
26+
<IonButtons>
27+
<IonBackButton />
28+
</IonButtons>
29+
</IonToolbar>
30+
</IonHeader>
31+
<IonContent id="pageOneContent">
32+
<IonLabel>Page one content</IonLabel>
33+
<IonNavLink
34+
routerDirection="forward"
35+
component={() => {
36+
return (
37+
<>
38+
<IonHeader>
39+
<IonToolbar>
40+
<IonTitle>Page Two</IonTitle>
41+
<IonButtons>
42+
<IonBackButton />
43+
</IonButtons>
44+
</IonToolbar>
45+
</IonHeader>
46+
<IonContent id="pageTwoContent">
47+
<IonLabel>Page two content</IonLabel>
48+
<IonNavLink
49+
routerDirection="forward"
50+
component={() => (
51+
<>
52+
<IonHeader>
53+
<IonToolbar>
54+
<IonTitle>Page Three</IonTitle>
55+
<IonButtons>
56+
<IonBackButton />
57+
</IonButtons>
58+
</IonToolbar>
59+
</IonHeader>
60+
<IonContent>
61+
<IonLabel>Page three content</IonLabel>
62+
</IonContent>
63+
</>
64+
)}
65+
>
66+
<IonButton>Go to Page Three</IonButton>
67+
</IonNavLink>
68+
</IonContent>
69+
</>
70+
);
71+
}}
72+
>
73+
<IonButton>Go to Page Two</IonButton>
74+
</IonNavLink>
75+
</IonContent>
76+
</>
77+
);
78+
}}
79+
></IonNav>
80+
</IonPage>
81+
);
82+
};
83+
84+
export default NavComponent;

packages/react/test-app/src/pages/overlay-hooks/ModalHook.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const ModalHook: React.FC = () => {
4949
setCount(count + 1);
5050
}, [count, setCount]);
5151

52-
const handleDismissWithComponent = useCallback((data, role) => {
52+
const handleDismissWithComponent = useCallback((data: any, role: string) => {
5353
dismissWithComponent(data, role);
5454
// eslint-disable-next-line react-hooks/exhaustive-deps
5555
}, []);

0 commit comments

Comments
 (0)