diff --git a/packages/react/src/components/IonApp.tsx b/packages/react/src/components/IonApp.tsx index 2e91eac3b6..1882561eec 100644 --- a/packages/react/src/components/IonApp.tsx +++ b/packages/react/src/components/IonApp.tsx @@ -1,5 +1,5 @@ import type { JSX as LocalJSX } from '@ionic/core/components'; -import React from 'react'; +import React, { type PropsWithChildren } from 'react'; import type { IonContextInterface } from '../contexts/IonContext'; import { IonContext } from '../contexts/IonContext'; @@ -9,53 +9,46 @@ import { IonOverlayManager } from './IonOverlayManager'; import type { IonicReactProps } from './IonicReactProps'; import { IonAppInner } from './inner-proxies'; -type Props = LocalJSX.IonApp & - IonicReactProps & { - ref?: React.Ref; +type Props = PropsWithChildren; +}>; + +export class IonApp extends React.Component { + addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void; + removeOverlayCallback?: (id: string) => void; + + constructor(props: Props) { + super(props); + } + + 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); + } + }, }; -export const IonApp = /*@__PURE__*/ (() => - class extends React.Component { - addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void; - removeOverlayCallback?: (id: string) => void; + render() { + return ( + + {this.props.children} + { + this.addOverlayCallback = callback; + }} + onRemoveOverlay={(callback) => { + this.removeOverlayCallback = callback; + }} + /> + + ); + } - 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 ( - - {this.props.children} - { - this.addOverlayCallback = callback; - }} - onRemoveOverlay={(callback) => { - this.removeOverlayCallback = callback; - }} - /> - - ); - } - - static get displayName() { - return 'IonApp'; - } - })(); + static displayName = 'IonApp'; +} diff --git a/packages/react/src/components/navigation/IonBackButton.tsx b/packages/react/src/components/navigation/IonBackButton.tsx index d53df30128..ae512bec37 100644 --- a/packages/react/src/components/navigation/IonBackButton.tsx +++ b/packages/react/src/components/navigation/IonBackButton.tsx @@ -1,54 +1,50 @@ import type { JSX as LocalJSX } from '@ionic/core/components'; -import React from 'react'; +import React, { type PropsWithChildren } from 'react'; import { NavContext } from '../../contexts/NavContext'; import type { IonicReactProps } from '../IonicReactProps'; import { IonBackButtonInner } from '../inner-proxies'; -type Props = Omit & - IonicReactProps & { - icon?: - | { - ios: string; - md: string; - } - | string; - ref?: React.Ref; +type Props = PropsWithChildren; +}>; + +export class IonBackButton extends React.Component { + context!: React.ContextType; + + 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); + } else if (defaultHref !== undefined) { + window.location.href = defaultHref; + } }; -export const IonBackButton = /*@__PURE__*/ (() => - class extends React.Component { - context!: React.ContextType; + render() { + return ; + } - 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; - } + static get displayName() { + return 'IonBackButton'; + } - const { defaultHref, routerAnimation } = this.props; + static get contextType() { + return NavContext; + } - if (this.context.hasIonicRouter()) { - e.stopPropagation(); - this.context.goBack(defaultHref, routerAnimation); - } else if (defaultHref !== undefined) { - window.location.href = defaultHref; - } - }; - - render() { - return ; - } - - static get displayName() { - return 'IonBackButton'; - } - - static get contextType() { - return NavContext; - } - })(); + shouldComponentUpdate(_nextProps: Readonly): boolean { + return true; + } +} diff --git a/packages/react/src/components/navigation/IonTabButton.tsx b/packages/react/src/components/navigation/IonTabButton.tsx index c78af7ec92..4f0c011a90 100644 --- a/packages/react/src/components/navigation/IonTabButton.tsx +++ b/packages/react/src/components/navigation/IonTabButton.tsx @@ -13,41 +13,45 @@ type Props = LocalJSX.IonTabButton & onPointerDown?: React.PointerEventHandler; onTouchEnd?: React.TouchEventHandler; onTouchMove?: React.TouchEventHandler; + children?: React.ReactNode; }; -export const IonTabButton = /*@__PURE__*/ (() => - class extends React.Component { - constructor(props: Props) { - super(props); - this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this); - } +export class IonTabButton extends React.Component { + shouldComponentUpdate(_nextProps: Readonly, _nextState: Readonly<{}>): boolean { + return true; + } - handleIonTabButtonClick() { - if (this.props.onClick) { - this.props.onClick( - new CustomEvent('ionTabButtonClick', { - detail: { - tab: this.props.tab, - href: this.props.href, - routeOptions: this.props.routerOptions, - }, - }) - ); - } - } + constructor(props: Props) { + super(props); + this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this); + } - render() { - /** - * onClick is excluded from the props, since it has a custom - * implementation within IonTabBar.tsx. Calling onClick within this - * component would result in duplicate handler calls. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { onClick, ...rest } = this.props; - return ; + handleIonTabButtonClick() { + if (this.props.onClick) { + this.props.onClick( + new CustomEvent('ionTabButtonClick', { + detail: { + tab: this.props.tab, + href: this.props.href, + routeOptions: this.props.routerOptions, + }, + }) + ); } + } - static get displayName() { - return 'IonTabButton'; - } - })(); + render() { + /** + * onClick is excluded from the props, since it has a custom + * implementation within IonTabBar.tsx. Calling onClick within this + * component would result in duplicate handler calls. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onClick, ...rest } = this.props; + return ; + } + + static get displayName() { + return 'IonTabButton'; + } +} diff --git a/packages/react/src/components/navigation/IonTabs.tsx b/packages/react/src/components/navigation/IonTabs.tsx index a7a8a250bf..5095d00cc8 100644 --- a/packages/react/src/components/navigation/IonTabs.tsx +++ b/packages/react/src/components/navigation/IonTabs.tsx @@ -1,3 +1,4 @@ +import type { Components } from '@ionic/core'; import type { JSX as LocalJSX } from '@ionic/core/components'; import React, { Fragment } from 'react'; @@ -26,12 +27,14 @@ if (typeof (window as any) !== 'undefined' && window.customElements) { } } -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - 'ion-tabs': any; - } +export interface IonTabsProps extends React.HTMLAttributes { + onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void; + onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void; +} + +declare module 'react' { + interface HTMLElements { + 'ion-tabs': IonTabsProps; } } @@ -40,169 +43,174 @@ type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode; interface Props extends LocalJSX.IonTabs { className?: string; children: ChildFunction | React.ReactNode; + onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void; + onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void; } -export const IonTabs = /*@__PURE__*/ (() => - class extends React.Component { - context!: React.ContextType; +export class IonTabs extends React.Component { + shouldComponentUpdate(_nextProps: Readonly, _nextState: Readonly<{}>): boolean { + return true; + } + + context!: React.ContextType; + /** + * `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`. + * Without this, `ref.current` will be `undefined` in the user's app, + * breaking their ability to access the `IonRouterOutlet` instance. + * Do not remove this ref. + */ + routerOutletRef: React.Ref = React.createRef(); + selectTabHandler?: (tag: string) => boolean; + tabBarRef = React.createRef(); + + ionTabContextState: IonTabsContextState = { + activeTab: undefined, + selectTab: () => false, + hasRouterOutlet: false, /** - * `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`. - * Without this, `ref.current` will be `undefined` in the user's app, - * breaking their ability to access the `IonRouterOutlet` instance. - * Do not remove this ref. + * Tab bar can be used as a standalone component, + * so the props can not be passed directly to the + * tab bar component. Instead, props will be + * passed through the context. */ - routerOutletRef: React.Ref = React.createRef(); - selectTabHandler?: (tag: string) => boolean; - tabBarRef = React.createRef(); + tabBarProps: { ref: this.tabBarRef }, + }; - ionTabContextState: IonTabsContextState = { - activeTab: undefined, - selectTab: () => false, - hasRouterOutlet: false, - /** - * Tab bar can be used as a standalone component, - * so the props can not be passed directly to the - * tab bar component. Instead, props will be - * passed through the context. - */ - tabBarProps: { ref: this.tabBarRef }, - }; + constructor(props: Props) { + super(props); + } - constructor(props: Props) { - super(props); + componentDidMount() { + if (this.tabBarRef.current) { + // Grab initial value + this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab; + // Override method + this.tabBarRef.current.setActiveTabOnContext = (tab: string) => { + this.ionTabContextState.activeTab = tab; + }; + this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab; } + } - componentDidMount() { - if (this.tabBarRef.current) { - // Grab initial value - this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab; - // Override method - this.tabBarRef.current.setActiveTabOnContext = (tab: string) => { - this.ionTabContextState.activeTab = tab; - }; - this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab; - } - } + renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) { + return ( + + {React.Children.map(children, (child: React.ReactNode) => { + if (React.isValidElement(child)) { + const isRouterOutlet = + child.type === IonRouterOutlet || + (child.type as any).isRouterOutlet || + (child.type === Fragment && child.props.children[0].type === IonRouterOutlet); - renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) { - return ( - - {React.Children.map(children, (child: React.ReactNode) => { - if (React.isValidElement(child)) { - const isRouterOutlet = - child.type === IonRouterOutlet || - (child.type as any).isRouterOutlet || - (child.type === Fragment && child.props.children[0].type === IonRouterOutlet); - - if (isRouterOutlet) { - /** - * The modified outlet needs to be returned to include - * the ref. - */ - return outlet; - } + if (isRouterOutlet) { + /** + * The modified outlet needs to be returned to include + * the ref. + */ + return outlet; } - return child; - })} - - ); - } + } + return child; + })} + + ); + } - render() { - let outlet: React.ReactElement<{}> | undefined; - // Check if IonTabs has any IonTab children - let hasTab = false; - const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props; + render() { + let outlet: React.ReactElement<{}> | undefined; + // Check if IonTabs has any IonTab children + let hasTab = false; + const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props; - const children = - typeof this.props.children === 'function' - ? (this.props.children as ChildFunction)(this.ionTabContextState) - : this.props.children; - - React.Children.forEach(children, (child: any) => { - // eslint-disable-next-line no-prototype-builtins - if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) { - return; - } - if (child.type === IonRouterOutlet || child.type.isRouterOutlet) { - outlet = React.cloneElement(child); - } else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) { - outlet = React.cloneElement(child.props.children[0]); - } else if (child.type === IonTab) { - /** - * This indicates that IonTabs will be using a basic tab-based navigation - * without the history stack or URL updates associated with the router. - */ - hasTab = true; - } - - this.ionTabContextState.hasRouterOutlet = !!outlet; - - let childProps: any = { - ...this.ionTabContextState.tabBarProps, - }; + const children = + typeof this.props.children === 'function' + ? (this.props.children as ChildFunction)(this.ionTabContextState) + : this.props.children; + React.Children.forEach(children, (child: any) => { + // eslint-disable-next-line no-prototype-builtins + if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) { + return; + } + if (child.type === IonRouterOutlet || child.type.isRouterOutlet) { + outlet = React.cloneElement(child); + } else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) { + outlet = React.cloneElement(child.props.children[0]); + } else if (child.type === IonTab) { /** - * Only pass these props - * down from IonTabs to IonTabBar - * if they are defined, otherwise - * if you have a handler set on - * IonTabBar it will be overridden. + * This indicates that IonTabs will be using a basic tab-based navigation + * without the history stack or URL updates associated with the router. */ - if (onIonTabsDidChange !== undefined) { - childProps = { - ...childProps, - onIonTabsDidChange, - }; - } - - if (onIonTabsWillChange !== undefined) { - childProps = { - ...childProps, - onIonTabsWillChange, - }; - } - - this.ionTabContextState.tabBarProps = childProps; - }); - - if (!outlet && !hasTab) { - throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab'); - } - if (outlet && hasTab) { - throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time'); + hasTab = true; } - if (hasTab) { - return ; - } + this.ionTabContextState.hasRouterOutlet = !!outlet; + + let childProps: any = { + ...this.ionTabContextState.tabBarProps, + }; /** - * TODO(ROU-11051) - * - * There is no error handling for the case where there - * is no associated Route for the given IonTabButton. - * - * More investigation is needed to determine how to - * handle this to prevent any overwriting of the - * IonTabButton's onClick handler and how the routing - * is handled. + * Only pass these props + * down from IonTabs to IonTabBar + * if they are defined, otherwise + * if you have a handler set on + * IonTabBar it will be overridden. */ + if (onIonTabsDidChange !== undefined) { + childProps = { + ...childProps, + onIonTabsDidChange, + }; + } - return ( - - {this.context.hasIonicRouter() ? ( - - {this.renderTabsInner(children, outlet)} - - ) : ( - this.renderTabsInner(children, outlet) - )} - - ); + if (onIonTabsWillChange !== undefined) { + childProps = { + ...childProps, + onIonTabsWillChange, + }; + } + + this.ionTabContextState.tabBarProps = childProps; + }); + + if (!outlet && !hasTab) { + throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab'); + } + if (outlet && hasTab) { + throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time'); } - static get contextType() { - return NavContext; + if (hasTab) { + return ; } - })(); + + /** + * TODO(ROU-11051) + * + * There is no error handling for the case where there + * is no associated Route for the given IonTabButton. + * + * More investigation is needed to determine how to + * handle this to prevent any overwriting of the + * IonTabButton's onClick handler and how the routing + * is handled. + */ + + return ( + + {this.context.hasIonicRouter() ? ( + + {this.renderTabsInner(children, outlet)} + + ) : ( + this.renderTabsInner(children, outlet) + )} + + ); + } + + static get contextType() { + return NavContext; + } +} diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx index eaf99c129f..8ae9f65291 100644 --- a/packages/react/test/base/src/App.tsx +++ b/packages/react/test/base/src/App.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Route } from 'react-router-dom'; import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; +import React from 'react'; +import { Route } from 'react-router-dom'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -21,19 +21,19 @@ import '@ionic/react/css/display.css'; /* Theme variables */ import './theme/variables.css'; +import Icons from './pages/Icons'; import Main from './pages/Main'; -import OverlayHooks from './pages/overlay-hooks/OverlayHooks'; -import OverlayComponents from './pages/overlay-components/OverlayComponents'; -import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted'; import Tabs from './pages/Tabs'; import TabsBasic from './pages/TabsBasic'; -import Icons from './pages/Icons'; import NavComponent from './pages/navigation/NavComponent'; -import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling'; import IonModalConditional from './pages/overlay-components/IonModalConditional'; +import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling'; import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton'; -import IonPopoverNested from './pages/overlay-components/IonPopoverNested'; import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren'; +import IonPopoverNested from './pages/overlay-components/IonPopoverNested'; +import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted'; +import OverlayComponents from './pages/overlay-components/OverlayComponents'; +import OverlayHooks from './pages/overlay-hooks/OverlayHooks'; setupIonicReact(); diff --git a/tsconfig.json b/tsconfig.json index ae1ecab3c7..3fc50f7e20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,12 +7,14 @@ "declaration": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, + "importHelpers": true, "lib": [ "dom", - "es2017" + "es2020", + "dom.iterable" ], - "module": "es2015", - "moduleResolution": "node", + "module": "esnext", + "moduleResolution": "bundler", "noImplicitAny": true, "noImplicitReturns": true, "noUnusedLocals": true, @@ -20,7 +22,8 @@ "pretty": true, "removeComments": false, "strictPropertyInitialization": false, - "target": "es2017", + "target": "es2020", + "jsx": "react-jsx", "baseUrl": ".", "paths": { "@ionic/core/hydrate": [