Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react): IonNav works with react #25565

Merged
merged 19 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export { IonPopover } from './IonPopover';
// Custom Components
export { IonApp } from './IonApp';
export { IonPage } from './IonPage';
export { IonNav } from './navigation/IonNav';
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
export { IonTabs } from './navigation/IonTabs';
export { IonTabBar } from './navigation/IonTabBar';
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/components/navigation/IonBackButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ export const IonBackButton = /*@__PURE__*/ (() =>
context!: React.ContextType<typeof NavContext>;

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;

if (this.context.hasIonicRouter()) {
e.stopPropagation();
this.context.goBack(defaultHref, routerAnimation);
Expand Down
30 changes: 30 additions & 0 deletions packages/react/src/components/navigation/IonNav.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions packages/react/src/framework-delegate.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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');
});

});
2 changes: 2 additions & 0 deletions packages/react/test-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Main from './pages/Main';
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
import OverlayComponents from './pages/overlay-components/OverlayComponents';
import Tabs from './pages/Tabs';
import NavComponent from './pages/navigation/NavComponent';

setupIonicReact();

Expand All @@ -35,6 +36,7 @@ const App: React.FC = () => (
<Route path="/" component={Main} />
<Route path="/overlay-hooks" component={OverlayHooks} />
<Route path="/overlay-components" component={OverlayComponents} />
<Route path="/navigation" component={NavComponent} />
<Route path="/tabs" component={Tabs} />
</IonRouterOutlet>
</IonReactRouter>
Expand Down
5 changes: 5 additions & 0 deletions packages/react/test-app/src/pages/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const Main: React.FC<MainProps> = () => {
<IonLabel>Overlay Components</IonLabel>
</IonItem>
</IonList>
<IonList>
<IonItem routerLink="/navigation">
<IonLabel>Navigation</IonLabel>
</IonItem>
</IonList>
<IonList>
<IonItem routerLink="/tabs">
<IonLabel>Tabs</IonLabel>
Expand Down
84 changes: 84 additions & 0 deletions packages/react/test-app/src/pages/navigation/NavComponent.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const ModalHook: React.FC = () => {
setCount(count + 1);
}, [count, setCount]);

const handleDismissWithComponent = useCallback((data, role) => {
const handleDismissWithComponent = useCallback((data: any, role: any) => {
sean-perkins marked this conversation as resolved.
Show resolved Hide resolved
dismissWithComponent(data, role);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down