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 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<HTMLIonAppElement>;
type Props = PropsWithChildren<LocalJSX.IonApp & IonicReactProps & {
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__*/ (() =>
class extends React.Component<Props> {
addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void;
removeOverlayCallback?: (id: string) => void;
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>
);
}
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 (
<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';
}
})();
static displayName = 'IonApp';
}

View File

@ -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<LocalJSX.IonBackButton, 'icon'> &
IonicReactProps & {
icon?:
| {
ios: string;
md: string;
}
| string;
ref?: React.Ref<HTMLIonBackButtonElement>;
type Props = PropsWithChildren<LocalJSX.IonBackButton & IonicReactProps & {
ref?: React.Ref<HTMLIonBackButtonElement>;
}>;
export class IonBackButton extends React.Component<Props> {
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);
} else if (defaultHref !== undefined) {
window.location.href = defaultHref;
}
};
export const IonBackButton = /*@__PURE__*/ (() =>
class extends React.Component<Props> {
context!: React.ContextType<typeof NavContext>;
render() {
return <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
}
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 <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
}
static get displayName() {
return 'IonBackButton';
}
static get contextType() {
return NavContext;
}
})();
shouldComponentUpdate(_nextProps: Readonly<Props>): boolean {
return true;
}
}

View File

@ -13,41 +13,45 @@ type Props = LocalJSX.IonTabButton &
onPointerDown?: React.PointerEventHandler<HTMLIonTabButtonElement>;
onTouchEnd?: React.TouchEventHandler<HTMLIonTabButtonElement>;
onTouchMove?: React.TouchEventHandler<HTMLIonTabButtonElement>;
children?: React.ReactNode;
};
export const IonTabButton = /*@__PURE__*/ (() =>
class extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
}
export class IonTabButton extends React.Component<Props> {
shouldComponentUpdate(_nextProps: Readonly<Props>, _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 <IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>;
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 <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 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<Components.IonTabs> {
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<Props> {
context!: React.ContextType<typeof NavContext>;
export class IonTabs extends React.Component<Props> {
shouldComponentUpdate(_nextProps: Readonly<Props>, _nextState: Readonly<{}>): boolean {
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`.
* 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<HTMLIonRouterOutletElement> = React.createRef();
selectTabHandler?: (tag: string) => boolean;
tabBarRef = React.createRef<any>();
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 (
<IonTabsInner {...this.props}>
{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 (
<IonTabsInner {...this.props}>
{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;
})}
</IonTabsInner>
);
}
}
return child;
})}
</IonTabsInner>
);
}
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 <IonTabsInner {...this.props}></IonTabsInner>;
}
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 (
<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>
);
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 <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 { 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();

View File

@ -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": [