diff --git a/packages/react/src/components/navigation/IonNav.tsx b/packages/react/src/components/navigation/IonNav.tsx index a59b4a57e3c..c928cd24484 100644 --- a/packages/react/src/components/navigation/IonNav.tsx +++ b/packages/react/src/components/navigation/IonNav.tsx @@ -4,27 +4,34 @@ import React, { useState } from 'react'; import { ReactDelegate } from '../../framework-delegate'; import { createReactComponent } from '../react-component-lib'; +import { createForwardRef } from '../utils'; const IonNavInner = createReactComponent< JSX.IonNav & { delegate: FrameworkDelegate }, HTMLIonNavElement >('ion-nav', undefined, undefined, defineCustomElement); -export const IonNav: React.FC = ({ children, ...restOfProps }) => { - const [views, setViews] = useState([]); +type IonNavProps = JSX.IonNav & { + forwardedRef?: React.ForwardedRef; +}; + +const IonNavInternal: React.FC = ({ children, forwardedRef, ...restOfProps }) => { + const [views, setViews] = useState([]); /** * 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 addView = (view: React.ReactElement) => setViews([...views, view]); + const removeView = (view: React.ReactElement) => setViews(views.filter((v) => v !== view)); const delegate = ReactDelegate(addView, removeView); return ( - + {views} ); }; + +export const IonNav = createForwardRef(IonNavInternal, 'IonNav'); diff --git a/packages/react/src/framework-delegate.tsx b/packages/react/src/framework-delegate.tsx index e2370fee184..4143fa2abb1 100644 --- a/packages/react/src/framework-delegate.tsx +++ b/packages/react/src/framework-delegate.tsx @@ -1,15 +1,17 @@ import { FrameworkDelegate } from '@ionic/core/components'; import { createPortal } from 'react-dom'; +type ReactComponent = (props?: any) => JSX.Element; + export const ReactDelegate = ( - addView: (view: React.ReactPortal) => void, - removeView: (view: React.ReactPortal) => void + addView: (view: React.ReactElement) => void, + removeView: (view: React.ReactElement) => void ): FrameworkDelegate => { - let Component: React.ReactPortal; + const refMap = new WeakMap(); const attachViewToDom = async ( parentElement: HTMLElement, - component: () => JSX.Element, + component: ReactComponent, propsOrDataObj?: any, cssClasses?: string[] ): Promise => { @@ -17,17 +19,20 @@ export const ReactDelegate = ( cssClasses && div.classList.add(...cssClasses); parentElement.appendChild(div); - Component = createPortal(component(), div); + const componentWithProps = component(propsOrDataObj); + const hostComponent = createPortal(componentWithProps, div); - Component.props = propsOrDataObj; + refMap.set(component, hostComponent); - addView(Component); + addView(hostComponent); return Promise.resolve(div); }; - const removeViewFromDom = (): Promise => { - Component && removeView(Component); + const removeViewFromDom = (_container: any, component: ReactComponent): Promise => { + const hostComponent = refMap.get(component); + hostComponent && removeView(hostComponent); + return Promise.resolve(); }; diff --git a/packages/react/test-app/cypress/integration/navigation/IonNav.spec.ts b/packages/react/test-app/cypress/integration/navigation/IonNav.spec.ts index c9d466bcad6..2df509984fa 100644 --- a/packages/react/test-app/cypress/integration/navigation/IonNav.spec.ts +++ b/packages/react/test-app/cypress/integration/navigation/IonNav.spec.ts @@ -7,6 +7,10 @@ describe('IonNav', () => { cy.get('ion-nav').contains('Page one content'); }); + it('should have a ref defined', () => { + cy.get('#navRef').should('have.text', 'Nav ref is defined: true'); + }); + it('should push a page', () => { cy.get('ion-button').contains('Go to Page Two').click(); cy.get('#pageTwoContent').should('be.visible'); @@ -25,4 +29,16 @@ describe('IonNav', () => { cy.get('ion-nav').contains('Page one content'); }); + it('should pass params to the page', () => { + cy.get('#pageOneProps').should('have.text', '{"someString":"Hello","someNumber":3,"someBoolean":true}'); + }); + + it('should pass componentProps to sub pages', () => { + cy.get('ion-button').contains('Go to Page Two').click(); + + cy.get('#pageTwoContent').should('be.visible'); + + cy.get('#pageTwoProps').should('have.text', '{"someValue":"Hello"}'); + }); + }); diff --git a/packages/react/test-app/src/pages/navigation/NavComponent.tsx b/packages/react/test-app/src/pages/navigation/NavComponent.tsx index a40bb7c3ed4..96e4b14af0a 100644 --- a/packages/react/test-app/src/pages/navigation/NavComponent.tsx +++ b/packages/react/test-app/src/pages/navigation/NavComponent.tsx @@ -11,72 +11,97 @@ import { IonBackButton, IonPage, } from '@ionic/react'; -import React from 'react'; +import React, { useRef } from 'react'; + +const PageOne = ({ + nav, + ...restOfProps +}: { + someString: string; + someNumber: number; + someBoolean: boolean; + nav: React.MutableRefObject; +}) => { + return ( + <> + + + Page One + + + + + + + Page one content +
{JSON.stringify(restOfProps)}
+ + + Go to Page Two + +
+ + ); +}; + +const PageTwo = (props?: { someValue: string }) => { + return ( + <> + + + Page Two + + + + + + + Page two content +
{JSON.stringify(props)}
+ + Go to Page Three + +
+ + ); +}; + +const PageThree = () => { + return ( + <> + + + Page Three + + + + + + + Page three content + + + ); +}; const NavComponent: React.FC = () => { + const ref = useRef(); return ( { - return ( - <> - - - Page One - - - - - - - Page one content - { - return ( - <> - - - Page Two - - - - - - - Page two content - ( - <> - - - Page Three - - - - - - - Page three content - - - )} - > - Go to Page Three - - - - ); - }} - > - Go to Page Two - - - - ); + ref={ref} + root={PageOne} + rootParams={{ + someString: 'Hello', + someNumber: 3, + someBoolean: true, + nav: ref, }} - > + /> ); };