mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 10:41:13 +08:00
feat(react): fixing support for react 19, adding test app for react 19 (#30217)
Issue number: resolves #29991 Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -198,7 +198,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
apps: [react17, react18]
|
apps: [react17, react18, react19]
|
||||||
needs: [build-react, build-react-router]
|
needs: [build-react, build-react-router]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
2
.github/workflows/stencil-nightly.yml
vendored
2
.github/workflows/stencil-nightly.yml
vendored
@ -208,7 +208,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
apps: [react17, react18]
|
apps: [react17, react18, react19]
|
||||||
needs: [build-react, build-react-router]
|
needs: [build-react, build-react-router]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -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,49 @@ 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<
|
||||||
IonicReactProps & {
|
LocalJSX.IonApp &
|
||||||
ref?: React.Ref<HTMLIonAppElement>;
|
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__*/ (() =>
|
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';
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
@ -1,54 +1,53 @@
|
|||||||
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<
|
||||||
IonicReactProps & {
|
LocalJSX.IonBackButton &
|
||||||
icon?:
|
IonicReactProps & {
|
||||||
| {
|
ref?: React.Ref<HTMLIonBackButtonElement>;
|
||||||
ios: string;
|
}
|
||||||
md: string;
|
>;
|
||||||
}
|
|
||||||
| string;
|
export class IonBackButton extends React.Component<Props> {
|
||||||
ref?: React.Ref<HTMLIonBackButtonElement>;
|
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__*/ (() =>
|
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(): 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;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
@ -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(): 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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(): 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
14
packages/react/test/apps/react17/src/OutputTarget.test.tsx
Normal file
14
packages/react/test/apps/react17/src/OutputTarget.test.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { IonButton } from '@ionic/react';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
test('should support onDoubleClick bindings', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
|
||||||
|
render(<IonButton onDoubleClick={mockFn}>Click me</IonButton>);
|
||||||
|
|
||||||
|
// Simulate a double click on the button
|
||||||
|
fireEvent.dblClick(screen.getByText('Click me'));
|
||||||
|
|
||||||
|
expect(mockFn).toBeCalled();
|
||||||
|
});
|
30
packages/react/test/apps/react19/index.html
Normal file
30
packages/react/test/apps/react19/index.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Ionic App</title>
|
||||||
|
|
||||||
|
<base href="/" />
|
||||||
|
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
|
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||||
|
|
||||||
|
<!-- add to homescreen for ios -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Ionic App" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
10603
packages/react/test/apps/react19/package-lock.json
generated
Normal file
10603
packages/react/test/apps/react19/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
packages/react/test/apps/react19/package.json
Normal file
49
packages/react/test/apps/react19/package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "test-app",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@ionic/react": "^8.4.0",
|
||||||
|
"@ionic/react-router": "^8.4.0",
|
||||||
|
"ionicons": "^7.4.0",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0",
|
||||||
|
"react-router": "^5.3.4",
|
||||||
|
"react-router-dom": "^5.3.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"start": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"test": "vitest",
|
||||||
|
"sync": "sh ./scripts/sync.sh",
|
||||||
|
"cypress": "cypress run --headless --browser chrome",
|
||||||
|
"cypress.open": "cypress open",
|
||||||
|
"e2e": "concurrently \"serve -s dist -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
"@types/react": "19.0.10",
|
||||||
|
"@types/react-dom": "19.0.4",
|
||||||
|
"@types/react-router": "^5.1.20",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@vitejs/plugin-legacy": "^4.0.2",
|
||||||
|
"@vitejs/plugin-react": "^4.0.1",
|
||||||
|
"concurrently": "^6.3.0",
|
||||||
|
"cypress": "^13.2.0",
|
||||||
|
"eslint": "^8.35.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"serve": "^14.0.1",
|
||||||
|
"typescript": "^5.1.6",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vitest": "^0.32.2",
|
||||||
|
"wait-on": "^6.0.0"
|
||||||
|
},
|
||||||
|
"description": "An Ionic project",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
}
|
||||||
|
}
|
11
packages/react/test/apps/react19/src/index.tsx
Normal file
11
packages/react/test/apps/react19/src/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
const root = createRoot(container!);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
1
packages/react/test/apps/react19/src/vite-env.d.ts
vendored
Normal file
1
packages/react/test/apps/react19/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
21
packages/react/test/apps/react19/tsconfig.json
Normal file
21
packages/react/test/apps/react19/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
9
packages/react/test/apps/react19/tsconfig.node.json
Normal file
9
packages/react/test/apps/react19/tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
19
packages/react/test/apps/react19/vite.config.ts
Normal file
19
packages/react/test/apps/react19/vite.config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import legacy from '@vitejs/plugin-legacy'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
legacy()
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/setupTests.ts',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
})
|
@ -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();
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IonButton } from '@ionic/react';
|
import { IonButton } from '@ionic/react';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import { vi, test, expect } from 'vitest';
|
||||||
|
|
||||||
test('should support onDoubleClick bindings', () => {
|
test('should support onDoubleClick bindings', () => {
|
||||||
const mockFn = jest.fn();
|
const mockFn = vi.fn();
|
||||||
|
|
||||||
render(<IonButton onDoubleClick={mockFn}>Click me</IonButton>);
|
render(<IonButton onDoubleClick={mockFn}>Click me</IonButton>);
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ const PageThree = ({ nav }: { nav: React.MutableRefObject<HTMLIonNavElement> })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NavComponent: React.FC = () => {
|
const NavComponent: React.FC = () => {
|
||||||
const ref = useRef<any>();
|
const ref = useRef<any>(null);
|
||||||
return (
|
return (
|
||||||
<IonPage>
|
<IonPage>
|
||||||
<IonNav
|
<IonNav
|
||||||
|
@ -7,7 +7,11 @@
|
|||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["dom", "es2015"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2020",
|
||||||
|
"dom.iterable"
|
||||||
|
],
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
@ -20,8 +24,8 @@
|
|||||||
"removeComments": false,
|
"removeComments": false,
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"jsx": "react",
|
"jsx": "react-jsx",
|
||||||
"target": "es2017"
|
"target": "es2020",
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"importHelpers": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"es2017"
|
"es2017",
|
||||||
],
|
],
|
||||||
"module": "es2015",
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
Reference in New Issue
Block a user