fix(react): resolve errors with JSX and types

This commit is contained in:
Brandy Smith
2025-02-26 16:40:05 -05:00
parent 30b1e7f3a5
commit 22b852eba8
6 changed files with 287 additions and 283 deletions

View File

@ -1,5 +1,5 @@
import type { JSX as LocalJSX } from '@ionic/core/components'; 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 type { IonContextInterface } from '../contexts/IonContext';
import { IonContext } from '../contexts/IonContext'; import { IonContext } from '../contexts/IonContext';
@ -9,53 +9,46 @@ import { IonOverlayManager } from './IonOverlayManager';
import type { IonicReactProps } from './IonicReactProps'; import type { IonicReactProps } from './IonicReactProps';
import { IonAppInner } from './inner-proxies'; import { IonAppInner } from './inner-proxies';
type Props = LocalJSX.IonApp & type Props = PropsWithChildren<LocalJSX.IonApp & IonicReactProps & {
IonicReactProps & { ref?: React.Ref<HTMLIonAppElement>;
ref?: React.Ref<HTMLIonAppElement>; }>;
export class IonApp extends React.Component<Props> {
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__*/ (() => render() {
class extends React.Component<Props> { return (
addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void; <IonContext.Provider value={this.ionContext}>
removeOverlayCallback?: (id: string) => void; <IonAppInner {...this.props}>{this.props.children}</IonAppInner>
<IonOverlayManager
onAddOverlay={(callback) => {
this.addOverlayCallback = callback;
}}
onRemoveOverlay={(callback) => {
this.removeOverlayCallback = callback;
}}
/>
</IonContext.Provider>
);
}
constructor(props: Props) { static displayName = 'IonApp';
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 (
<IonContext.Provider value={this.ionContext}>
<IonAppInner {...this.props}>{this.props.children}</IonAppInner>
<IonOverlayManager
onAddOverlay={(callback) => {
this.addOverlayCallback = callback;
}}
onRemoveOverlay={(callback) => {
this.removeOverlayCallback = callback;
}}
/>
</IonContext.Provider>
);
}
static get displayName() {
return 'IonApp';
}
})();

View File

@ -1,54 +1,50 @@
import type { JSX as LocalJSX } from '@ionic/core/components'; 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 { NavContext } from '../../contexts/NavContext';
import type { IonicReactProps } from '../IonicReactProps'; import type { IonicReactProps } from '../IonicReactProps';
import { IonBackButtonInner } from '../inner-proxies'; import { IonBackButtonInner } from '../inner-proxies';
type Props = Omit<LocalJSX.IonBackButton, 'icon'> & type Props = PropsWithChildren<LocalJSX.IonBackButton & IonicReactProps & {
IonicReactProps & { ref?: React.Ref<HTMLIonBackButtonElement>;
icon?: }>;
| {
ios: string; export class IonBackButton extends React.Component<Props> {
md: string; context!: React.ContextType<typeof NavContext>;
}
| string; clickButton = (e: React.MouseEvent) => {
ref?: React.Ref<HTMLIonBackButtonElement>; /**
* 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__*/ (() => render() {
class extends React.Component<Props> { return <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
context!: React.ContextType<typeof NavContext>; }
clickButton = (e: React.MouseEvent) => { static get displayName() {
/** return 'IonBackButton';
* 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; static get contextType() {
return NavContext;
}
if (this.context.hasIonicRouter()) { shouldComponentUpdate(_nextProps: Readonly<Props>): boolean {
e.stopPropagation(); return true;
this.context.goBack(defaultHref, routerAnimation); }
} else if (defaultHref !== undefined) { }
window.location.href = defaultHref;
}
};
render() {
return <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
}
static get displayName() {
return 'IonBackButton';
}
static get contextType() {
return NavContext;
}
})();

View File

@ -13,41 +13,45 @@ type Props = LocalJSX.IonTabButton &
onPointerDown?: React.PointerEventHandler<HTMLIonTabButtonElement>; onPointerDown?: React.PointerEventHandler<HTMLIonTabButtonElement>;
onTouchEnd?: React.TouchEventHandler<HTMLIonTabButtonElement>; onTouchEnd?: React.TouchEventHandler<HTMLIonTabButtonElement>;
onTouchMove?: React.TouchEventHandler<HTMLIonTabButtonElement>; onTouchMove?: React.TouchEventHandler<HTMLIonTabButtonElement>;
children?: React.ReactNode;
}; };
export const IonTabButton = /*@__PURE__*/ (() => export class IonTabButton extends React.Component<Props> {
class extends React.Component<Props> { shouldComponentUpdate(_nextProps: Readonly<Props>, _nextState: Readonly<{}>): boolean {
constructor(props: Props) { return true;
super(props); }
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
}
handleIonTabButtonClick() { constructor(props: Props) {
if (this.props.onClick) { super(props);
this.props.onClick( this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
new CustomEvent('ionTabButtonClick', { }
detail: {
tab: this.props.tab,
href: this.props.href,
routeOptions: this.props.routerOptions,
},
})
);
}
}
render() { handleIonTabButtonClick() {
/** if (this.props.onClick) {
* onClick is excluded from the props, since it has a custom this.props.onClick(
* implementation within IonTabBar.tsx. Calling onClick within this new CustomEvent('ionTabButtonClick', {
* component would result in duplicate handler calls. detail: {
*/ tab: this.props.tab,
// eslint-disable-next-line @typescript-eslint/no-unused-vars href: this.props.href,
const { onClick, ...rest } = this.props; routeOptions: this.props.routerOptions,
return <IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>; },
})
);
} }
}
static get displayName() { render() {
return 'IonTabButton'; /**
} * 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 <IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>;
}
static get displayName() {
return 'IonTabButton';
}
}

View File

@ -1,3 +1,4 @@
import type { Components } from '@ionic/core';
import type { JSX as LocalJSX } from '@ionic/core/components'; import type { JSX as LocalJSX } from '@ionic/core/components';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
@ -26,12 +27,14 @@ if (typeof (window as any) !== 'undefined' && window.customElements) {
} }
} }
declare global { export interface IonTabsProps extends React.HTMLAttributes<Components.IonTabs> {
// eslint-disable-next-line @typescript-eslint/no-namespace onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
namespace JSX { onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
interface IntrinsicElements { }
'ion-tabs': any;
} declare module 'react' {
interface HTMLElements {
'ion-tabs': IonTabsProps;
} }
} }
@ -40,169 +43,174 @@ type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
interface Props extends LocalJSX.IonTabs { interface Props extends LocalJSX.IonTabs {
className?: string; className?: string;
children: ChildFunction | React.ReactNode; children: ChildFunction | React.ReactNode;
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
} }
export const IonTabs = /*@__PURE__*/ (() => export class IonTabs extends React.Component<Props> {
class extends React.Component<Props> { shouldComponentUpdate(_nextProps: Readonly<Props>, _nextState: Readonly<{}>): boolean {
context!: React.ContextType<typeof NavContext>; return true;
}
context!: React.ContextType<typeof NavContext>;
/**
* `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<Components.IonRouterOutlet> = React.createRef();
selectTabHandler?: (tag: string) => boolean;
tabBarRef = React.createRef<any>();
ionTabContextState: IonTabsContextState = {
activeTab: undefined,
selectTab: () => false,
hasRouterOutlet: false,
/** /**
* `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`. * Tab bar can be used as a standalone component,
* Without this, `ref.current` will be `undefined` in the user's app, * so the props can not be passed directly to the
* breaking their ability to access the `IonRouterOutlet` instance. * tab bar component. Instead, props will be
* Do not remove this ref. * passed through the context.
*/ */
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef(); tabBarProps: { ref: this.tabBarRef },
selectTabHandler?: (tag: string) => boolean; };
tabBarRef = React.createRef<any>();
ionTabContextState: IonTabsContextState = { constructor(props: Props) {
activeTab: undefined, super(props);
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) { componentDidMount() {
super(props); 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() { renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
if (this.tabBarRef.current) { return (
// Grab initial value <IonTabsInner {...this.props}>
this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab; {React.Children.map(children, (child: React.ReactNode) => {
// Override method if (React.isValidElement(child)) {
this.tabBarRef.current.setActiveTabOnContext = (tab: string) => { const isRouterOutlet =
this.ionTabContextState.activeTab = tab; child.type === IonRouterOutlet ||
}; (child.type as any).isRouterOutlet ||
this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab; (child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
}
}
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) { if (isRouterOutlet) {
return ( /**
<IonTabsInner {...this.props}> * The modified outlet needs to be returned to include
{React.Children.map(children, (child: React.ReactNode) => { * the ref.
if (React.isValidElement(child)) { */
const isRouterOutlet = return outlet;
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;
}
} }
return child; }
})} return child;
</IonTabsInner> })}
); </IonTabsInner>
} );
}
render() { render() {
let outlet: React.ReactElement<{}> | undefined; let outlet: React.ReactElement<{}> | undefined;
// Check if IonTabs has any IonTab children // Check if IonTabs has any IonTab children
let hasTab = false; let hasTab = false;
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props; const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
const children = const children =
typeof this.props.children === 'function' typeof this.props.children === 'function'
? (this.props.children as ChildFunction)(this.ionTabContextState) ? (this.props.children as ChildFunction)(this.ionTabContextState)
: this.props.children; : 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,
};
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 * This indicates that IonTabs will be using a basic tab-based navigation
* down from IonTabs to IonTabBar * without the history stack or URL updates associated with the router.
* if they are defined, otherwise
* if you have a handler set on
* IonTabBar it will be overridden.
*/ */
if (onIonTabsDidChange !== undefined) { hasTab = true;
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');
} }
if (hasTab) { this.ionTabContextState.hasRouterOutlet = !!outlet;
return <IonTabsInner {...this.props}></IonTabsInner>;
} let childProps: any = {
...this.ionTabContextState.tabBarProps,
};
/** /**
* TODO(ROU-11051) * Only pass these props
* * down from IonTabs to IonTabBar
* There is no error handling for the case where there * if they are defined, otherwise
* is no associated Route for the given IonTabButton. * if you have a handler set on
* * IonTabBar it will be overridden.
* 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.
*/ */
if (onIonTabsDidChange !== undefined) {
childProps = {
...childProps,
onIonTabsDidChange,
};
}
return ( if (onIonTabsWillChange !== undefined) {
<IonTabsContext.Provider value={this.ionTabContextState}> childProps = {
{this.context.hasIonicRouter() ? ( ...childProps,
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}> onIonTabsWillChange,
{this.renderTabsInner(children, outlet)} };
</PageManager> }
) : (
this.renderTabsInner(children, outlet) this.ionTabContextState.tabBarProps = childProps;
)} });
</IonTabsContext.Provider>
); 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() { if (hasTab) {
return NavContext; return <IonTabsInner {...this.props}></IonTabsInner>;
} }
})();
/**
* 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 (
<IonTabsContext.Provider value={this.ionTabContextState}>
{this.context.hasIonicRouter() ? (
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
{this.renderTabsInner(children, outlet)}
</PageManager>
) : (
this.renderTabsInner(children, outlet)
)}
</IonTabsContext.Provider>
);
}
static get contextType() {
return NavContext;
}
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router'; 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 */ /* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css'; import '@ionic/react/css/core.css';
@ -21,19 +21,19 @@ import '@ionic/react/css/display.css';
/* Theme variables */ /* Theme variables */
import './theme/variables.css'; import './theme/variables.css';
import Icons from './pages/Icons';
import Main from './pages/Main'; 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 Tabs from './pages/Tabs';
import TabsBasic from './pages/TabsBasic'; import TabsBasic from './pages/TabsBasic';
import Icons from './pages/Icons';
import NavComponent from './pages/navigation/NavComponent'; import NavComponent from './pages/navigation/NavComponent';
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
import IonModalConditional from './pages/overlay-components/IonModalConditional'; import IonModalConditional from './pages/overlay-components/IonModalConditional';
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton'; import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren'; 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(); setupIonicReact();

View File

@ -7,12 +7,14 @@
"declaration": true, "declaration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"importHelpers": true,
"lib": [ "lib": [
"dom", "dom",
"es2017" "es2020",
"dom.iterable"
], ],
"module": "es2015", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "bundler",
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@ -20,7 +22,8 @@
"pretty": true, "pretty": true,
"removeComments": false, "removeComments": false,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"target": "es2017", "target": "es2020",
"jsx": "react-jsx",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@ionic/core/hydrate": [ "@ionic/core/hydrate": [