mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
feat(react): React Router Enhancements (#21693)
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import PageManager from '../routing/PageManager';
|
||||
|
||||
import { IonicReactProps } from './IonicReactProps';
|
||||
import { createForwardRef } from './utils';
|
||||
@ -14,28 +15,29 @@ interface IonPageInternalProps extends IonPageProps {
|
||||
|
||||
class IonPageInternal extends React.Component<IonPageInternalProps> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: IonPageInternalProps) {
|
||||
super(props);
|
||||
this.ref = this.props.forwardedRef || React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.context && this.ref && this.ref.current) {
|
||||
if (this.context.hasIonicRouter()) {
|
||||
this.context.registerIonPage(this.ref.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, forwardedRef, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
this.context.hasIonicRouter() ? (
|
||||
<PageManager
|
||||
className={className ? `${className}` : ''}
|
||||
routeInfo={this.context.routeInfo}
|
||||
forwardedRef={forwardedRef}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</PageManager>
|
||||
) : (
|
||||
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={forwardedRef} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
38
packages/react/src/components/IonRedirect.tsx
Normal file
38
packages/react/src/components/IonRedirect.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
|
||||
export interface IonRedirectProps {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
to: string;
|
||||
routerOptions?: unknown;
|
||||
}
|
||||
|
||||
interface IonRedirectState {
|
||||
|
||||
}
|
||||
|
||||
export class IonRedirect extends React.PureComponent<IonRedirectProps, IonRedirectState> {
|
||||
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
render() {
|
||||
|
||||
const IonRedirectInner = this.context.getIonRedirect();
|
||||
|
||||
if (!this.context.hasIonicRouter() || !IonRedirect) {
|
||||
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRedirect>');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonRedirectInner {...this.props} />
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
|
||||
}
|
39
packages/react/src/components/IonRoute.tsx
Normal file
39
packages/react/src/components/IonRoute.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
|
||||
export interface IonRouteProps {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
show?: boolean;
|
||||
render: (props?: any) => JSX.Element;
|
||||
disableIonPageManagement?: boolean;
|
||||
}
|
||||
|
||||
interface IonRouteState {
|
||||
|
||||
}
|
||||
|
||||
export class IonRoute extends React.PureComponent<IonRouteProps, IonRouteState> {
|
||||
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
render() {
|
||||
|
||||
const IonRouteInner = this.context.getIonRoute();
|
||||
|
||||
if (!this.context.hasIonicRouter() || !IonRoute) {
|
||||
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRoute>');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonRouteInner {...this.props} />
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
|
||||
}
|
24
packages/react/src/components/IonRouterContext.tsx
Normal file
24
packages/react/src/components/IonRouterContext.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { RouteAction, RouterDirection, RouterOptions } from '../models';
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
export interface IonRouterContextState {
|
||||
routeInfo: RouteInfo;
|
||||
push: (pathname: string, routerDirection?: RouterDirection, routeAction?: RouteAction, routerOptions?: RouterOptions, animationBuilder?: AnimationBuilder) => void;
|
||||
back: (animationBuilder?: AnimationBuilder) => void;
|
||||
canGoBack: () => boolean;
|
||||
}
|
||||
|
||||
export const IonRouterContext = React.createContext<IonRouterContextState>({
|
||||
routeInfo: undefined as any,
|
||||
push: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
|
||||
back: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
|
||||
canGoBack: () => { throw new Error('An Ionic Router is required for IonRouterContext'); }
|
||||
});
|
||||
|
||||
export function useIonRouter() {
|
||||
const context = useContext(IonRouterContext);
|
||||
return context;
|
||||
}
|
@ -3,35 +3,56 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import OutletPageManager from '../routing/OutletPageManager';
|
||||
|
||||
import { IonicReactProps } from './IonicReactProps';
|
||||
import { IonRouterOutletInner } from './inner-proxies';
|
||||
import { createForwardRef } from './utils';
|
||||
|
||||
type Props = LocalJSX.IonRouterOutlet & {
|
||||
basePath?: string;
|
||||
ref?: React.RefObject<any>;
|
||||
ionPage?: boolean;
|
||||
};
|
||||
|
||||
type InternalProps = Props & {
|
||||
interface InternalProps extends Props {
|
||||
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
||||
};
|
||||
}
|
||||
|
||||
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {
|
||||
interface InternalState {
|
||||
}
|
||||
|
||||
class IonRouterOutletContainer extends React.Component<InternalProps, InternalState> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
constructor(props: InternalProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const StackManager = this.context.getStackManager();
|
||||
const { children, forwardedRef, ...props } = this.props;
|
||||
|
||||
return (
|
||||
this.context.hasIonicRouter() ? (
|
||||
<StackManager>
|
||||
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
|
||||
{this.props.children}
|
||||
</IonRouterOutletInner>
|
||||
</StackManager>
|
||||
props.ionPage ? (
|
||||
<OutletPageManager
|
||||
StackManager={StackManager}
|
||||
routeInfo={this.context.routeInfo}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</OutletPageManager>
|
||||
) : (
|
||||
<StackManager routeInfo={this.context.routeInfo}>
|
||||
<IonRouterOutletInner {...props}>
|
||||
{children}
|
||||
</IonRouterOutletInner>
|
||||
</StackManager>
|
||||
)
|
||||
) : (
|
||||
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
|
||||
<IonRouterOutletInner ref={forwardedRef} {...this.props}>
|
||||
{this.props.children}
|
||||
</IonRouterOutletInner>
|
||||
)
|
||||
@ -41,6 +62,6 @@ const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Compone
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export const IonRouterOutlet = createForwardRef<Props & IonicReactProps, HTMLIonRouterOutletElement>(IonRouterOutletContainer, 'IonRouterOutlet');
|
||||
|
@ -10,7 +10,7 @@ describe('IonTabs', () => {
|
||||
const { container } = render(
|
||||
<IonTabs>
|
||||
<IonRouterOutlet></IonRouterOutlet>
|
||||
<IonTabBar slot="bottom" currentPath={'/'}>
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="schedule">
|
||||
<IonLabel>Schedule</IonLabel>
|
||||
<IonIcon name="schedule"></IonIcon>
|
||||
@ -44,7 +44,7 @@ describe('IonTabs', () => {
|
||||
const { container } = render(
|
||||
<IonTabs>
|
||||
<IonRouterOutlet></IonRouterOutlet>
|
||||
<IonTabBar slot="bottom" currentPath={'/'}>
|
||||
<IonTabBar slot="bottom">
|
||||
{false &&
|
||||
<IonTabButton tab="schedule">
|
||||
<IonLabel>Schedule</IonLabel>
|
||||
|
@ -3,11 +3,11 @@ import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
describe('isCoveredByReact', () => {
|
||||
it('should identify standard events as covered by React', () => {
|
||||
expect(utils.isCoveredByReact('click', document)).toEqual(true);
|
||||
expect(utils.isCoveredByReact('click')).toEqual(true);
|
||||
});
|
||||
it('should identify custom events as not covered by React', () => {
|
||||
expect(utils.isCoveredByReact('change', document)).toEqual(true);
|
||||
expect(utils.isCoveredByReact('ionchange', document)).toEqual(false);
|
||||
expect(utils.isCoveredByReact('change')).toEqual(true);
|
||||
expect(utils.isCoveredByReact('ionchange')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import { RouterOptions } from '../models';
|
||||
import { RouterDirection } from '../models/RouterDirection';
|
||||
|
||||
import { RouterDirection } from './hrefprops';
|
||||
import { attachProps, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
|
||||
import { attachProps, camelToDashCase, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
|
||||
|
||||
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
|
||||
forwardedRef?: React.Ref<ElementType>;
|
||||
@ -12,6 +14,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
|
||||
routerLink?: string;
|
||||
ref?: React.Ref<any>;
|
||||
routerDirection?: RouterDirection;
|
||||
routerOptions?: RouterOptions;
|
||||
routerAnimation?: AnimationBuilder;
|
||||
}
|
||||
|
||||
export const createReactComponent = <PropType, ElementType>(
|
||||
@ -36,10 +40,10 @@ export const createReactComponent = <PropType, ElementType>(
|
||||
}
|
||||
|
||||
private handleClick = (e: React.MouseEvent<PropType>) => {
|
||||
const { routerLink, routerDirection } = this.props;
|
||||
const { routerLink, routerDirection, routerOptions, routerAnimation } = this.props;
|
||||
if (routerLink !== undefined) {
|
||||
e.preventDefault();
|
||||
this.context.navigate(routerLink, routerDirection);
|
||||
this.context.navigate(routerLink, routerDirection, undefined, routerAnimation, routerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +56,8 @@ export const createReactComponent = <PropType, ElementType>(
|
||||
if (isCoveredByReact(eventName)) {
|
||||
(acc as any)[name] = (cProps as any)[name];
|
||||
}
|
||||
} else if (typeof (cProps as any)[name] === 'string') {
|
||||
(acc as any)[camelToDashCase(name)] = (cProps as any)[name];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -33,11 +33,13 @@ export const createOverlayComponent = <OverlayComponent extends object, OverlayT
|
||||
|
||||
class Overlay extends React.Component<Props> {
|
||||
overlay?: OverlayType;
|
||||
el: HTMLDivElement;
|
||||
el!: HTMLDivElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.el = document.createElement('div');
|
||||
if (typeof document !== 'undefined') {
|
||||
this.el = document.createElement('div');
|
||||
}
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none';
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
|
||||
import { RouterOptions } from '../models';
|
||||
import { RouterDirection } from '../models/RouterDirection';
|
||||
|
||||
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
|
||||
routerLink?: string;
|
||||
routerDirection?: RouterDirection;
|
||||
routerOptions?: RouterOptions;
|
||||
routerAnimation?: AnimationBuilder;
|
||||
};
|
||||
|
@ -21,13 +21,17 @@ export { IonPage } from './IonPage';
|
||||
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
|
||||
export { IonTabs } from './navigation/IonTabs';
|
||||
export { IonTabBar } from './navigation/IonTabBar';
|
||||
export { IonTabButton } from './navigation/IonTabButton';
|
||||
export { IonBackButton } from './navigation/IonBackButton';
|
||||
export { IonRouterOutlet } from './IonRouterOutlet';
|
||||
export { IonIcon } from './IonIcon';
|
||||
export * from './IonRoute';
|
||||
export * from './IonRedirect';
|
||||
export * from './IonRouterContext';
|
||||
|
||||
// Utils
|
||||
export { isPlatform, getPlatforms, getConfig } from './utils';
|
||||
export { RouterDirection } from './hrefprops';
|
||||
export { isPlatform, getPlatforms, getConfig, ionRenderToString } from './utils';
|
||||
export * from './hrefprops';
|
||||
|
||||
// Ionic Animations
|
||||
export { CreateAnimation } from './CreateAnimation';
|
||||
@ -51,4 +55,6 @@ addIcons({
|
||||
|
||||
// TODO: defineCustomElements() is asyncronous
|
||||
// We need to use the promise
|
||||
defineCustomElements(window);
|
||||
if (typeof window !== 'undefined') {
|
||||
defineCustomElements(window);
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import { JSX as IoniconsJSX } from 'ionicons';
|
||||
|
||||
import { /*@__PURE__*/ createReactComponent } from './createComponent';
|
||||
|
||||
export const IonTabButtonInner = /*@__PURE__*/createReactComponent<JSX.IonTabButton & { onIonTabButtonClick?: (e: CustomEvent) => void; }, HTMLIonTabButtonElement>('ion-tab-button');
|
||||
export const IonTabBarInner = /*@__PURE__*/createReactComponent<JSX.IonTabBar, HTMLIonTabBarElement>('ion-tab-bar');
|
||||
export const IonBackButtonInner = /*@__PURE__*/createReactComponent<Omit<JSX.IonBackButton, 'icon'>, HTMLIonBackButtonElement>('ion-back-button');
|
||||
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet, HTMLIonRouterOutletElement>('ion-router-outlet');
|
||||
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet & { setRef?: (val: HTMLIonRouterOutletElement) => void; }, HTMLIonRouterOutletElement>('ion-router-outlet');
|
||||
|
||||
// ionicons
|
||||
export const IonIconInner = /*@__PURE__*/createReactComponent<IoniconsJSX.IonIcon, HTMLIonIconElement>('ion-icon');
|
||||
|
@ -17,10 +17,10 @@ export const IonBackButton = /*@__PURE__*/(() => class extends React.Component<P
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
clickButton = (e: React.MouseEvent) => {
|
||||
const defaultHref = this.props.defaultHref;
|
||||
const { defaultHref, routerAnimation } = this.props;
|
||||
if (this.context.hasIonicRouter()) {
|
||||
e.stopPropagation();
|
||||
this.context.goBack(defaultHref);
|
||||
this.context.goBack(defaultHref, routerAnimation);
|
||||
} else if (defaultHref !== undefined) {
|
||||
window.location.href = defaultHref;
|
||||
}
|
||||
|
@ -2,25 +2,31 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
import { RouteInfo } from '../../models';
|
||||
import { IonicReactProps } from '../IonicReactProps';
|
||||
import { IonTabBarInner } from '../inner-proxies';
|
||||
import { IonTabButton } from '../proxies';
|
||||
import { createForwardRef } from '../utils';
|
||||
|
||||
import { IonTabButton } from './IonTabButton';
|
||||
|
||||
type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & {
|
||||
onIonTabsDidChange?: (event: CustomEvent<{ tab: string; }>) => void;
|
||||
onIonTabsWillChange?: (event: CustomEvent<{ tab: string; }>) => void;
|
||||
currentPath?: string;
|
||||
slot?: 'bottom' | 'top';
|
||||
style?: { [key: string]: string; };
|
||||
};
|
||||
|
||||
interface InternalProps extends IonTabBarProps {
|
||||
forwardedRef?: React.RefObject<HTMLIonIconElement>;
|
||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||
routeInfo: RouteInfo;
|
||||
}
|
||||
|
||||
interface TabUrls {
|
||||
originalHref: string;
|
||||
currentHref: string;
|
||||
originalRouteOptions?: unknown;
|
||||
currentRouteOptions?: unknown;
|
||||
}
|
||||
|
||||
interface IonTabBarState {
|
||||
@ -34,12 +40,13 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
constructor(props: InternalProps) {
|
||||
super(props);
|
||||
const tabs: { [key: string]: TabUrls; } = {};
|
||||
|
||||
React.Children.forEach((props as any).children, (child: any) => {
|
||||
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
|
||||
tabs[child.props.tab] = {
|
||||
originalHref: child.props.href,
|
||||
currentHref: child.props.href
|
||||
currentHref: child.props.href,
|
||||
originalRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
|
||||
currentRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -48,7 +55,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
const activeTab = tabKeys
|
||||
.find(key => {
|
||||
const href = tabs[key].originalHref;
|
||||
return props.currentPath!.startsWith(href);
|
||||
return props.routeInfo!.pathname.startsWith(href);
|
||||
}) || tabKeys[0];
|
||||
|
||||
this.state = {
|
||||
@ -59,71 +66,74 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
this.onTabButtonClick = this.onTabButtonClick.bind(this);
|
||||
this.renderTabButton = this.renderTabButton.bind(this);
|
||||
this.setActiveTabOnContext = this.setActiveTabOnContext.bind(this);
|
||||
this.selectTab = this.selectTab.bind(this);
|
||||
}
|
||||
|
||||
setActiveTabOnContext = (_tab: string) => { };
|
||||
|
||||
selectTab(tab: string) {
|
||||
const tabUrl = this.state.tabs[tab];
|
||||
if (tabUrl) {
|
||||
this.onTabButtonClick(new CustomEvent('ionTabButtonClick', {
|
||||
detail: {
|
||||
href: tabUrl.currentHref,
|
||||
tab,
|
||||
selected: tab === this.state.activeTab
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: IonTabBarProps, state: IonTabBarState) {
|
||||
|
||||
static getDerivedStateFromProps(props: InternalProps, state: IonTabBarState) {
|
||||
const tabs = { ...state.tabs };
|
||||
const activeTab = Object.keys(state.tabs)
|
||||
const tabKeys = Object.keys(state.tabs);
|
||||
const activeTab = tabKeys
|
||||
.find(key => {
|
||||
const href = state.tabs[key].originalHref;
|
||||
return props.currentPath!.startsWith(href);
|
||||
return props.routeInfo!.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
// Check to see if the tab button href has changed, and if so, update it in the tabs state
|
||||
React.Children.forEach((props as any).children, (child: any) => {
|
||||
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
|
||||
const tab = tabs[child.props.tab];
|
||||
if (tab.originalHref !== child.props.href) {
|
||||
if (!tab || (tab.originalHref !== child.props.href)) {
|
||||
tabs[child.props.tab] = {
|
||||
originalHref: child.props.href,
|
||||
currentHref: child.props.href
|
||||
currentHref: child.props.href,
|
||||
originalRouteOptions: child.props.routeOptions,
|
||||
currentRouteOptions: child.props.routeOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(activeTab === undefined || (activeTab === state.activeTab && state.tabs[activeTab].currentHref === props.currentPath))) {
|
||||
tabs[activeTab] = {
|
||||
originalHref: tabs[activeTab].originalHref,
|
||||
currentHref: props.currentPath!
|
||||
};
|
||||
const { activeTab: prevActiveTab } = state;
|
||||
if (activeTab && prevActiveTab) {
|
||||
const prevHref = state.tabs[prevActiveTab].currentHref;
|
||||
const prevRouteOptions = state.tabs[prevActiveTab].currentRouteOptions;
|
||||
if (activeTab !== prevActiveTab || (prevHref !== props.routeInfo?.pathname || prevRouteOptions !== props.routeInfo?.routeOptions)) {
|
||||
tabs[activeTab] = {
|
||||
originalHref: tabs[activeTab].originalHref,
|
||||
currentHref: props.routeInfo!.pathname + (props.routeInfo!.search || ''),
|
||||
originalRouteOptions: tabs[activeTab].originalRouteOptions,
|
||||
currentRouteOptions: props.routeInfo?.routeOptions
|
||||
};
|
||||
if (props.routeInfo.routeAction === 'pop') {
|
||||
// If navigating back and the tabs change, set the prev tab back to its original href
|
||||
tabs[prevActiveTab] = {
|
||||
originalHref: tabs[prevActiveTab].originalHref,
|
||||
currentHref: tabs[prevActiveTab].originalHref,
|
||||
originalRouteOptions: tabs[prevActiveTab].originalRouteOptions,
|
||||
currentRouteOptions: tabs[prevActiveTab].currentRouteOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeTab && props.onSetCurrentTab(activeTab, props.routeInfo);
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
tabs
|
||||
};
|
||||
}
|
||||
|
||||
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; }>) {
|
||||
const originalHref = this.state.tabs[e.detail.tab].originalHref;
|
||||
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; routeOptions: unknown; }>) {
|
||||
const tappedTab = this.state.tabs[e.detail.tab];
|
||||
const originalHref = tappedTab.originalHref;
|
||||
const currentHref = e.detail.href;
|
||||
const { activeTab: prevActiveTab } = this.state;
|
||||
// this.props.onSetCurrentTab(e.detail.tab, this.props.routeInfo);
|
||||
// Clicking the current tab will bring you back to the original href
|
||||
if (prevActiveTab === e.detail.tab) {
|
||||
if (originalHref === currentHref) {
|
||||
this.context.navigate(originalHref, 'none');
|
||||
} else {
|
||||
this.context.navigate(originalHref, 'back', 'pop');
|
||||
if (originalHref !== currentHref) {
|
||||
this.context.resetTab(e.detail.tab, originalHref, tappedTab.originalRouteOptions);
|
||||
}
|
||||
} else {
|
||||
if (this.props.onIonTabsWillChange) {
|
||||
@ -132,19 +142,22 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
if (this.props.onIonTabsDidChange) {
|
||||
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
||||
}
|
||||
this.setActiveTabOnContext(e.detail.tab);
|
||||
this.context.navigate(currentHref, 'none');
|
||||
|
||||
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private renderTabButton(activeTab: string | null | undefined) {
|
||||
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onIonTabButtonClick: (e: CustomEvent) => void; }>) | null | undefined) => {
|
||||
|
||||
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onClick: (e: any) => void; routeOptions?: unknown; }>) | null | undefined) => {
|
||||
if (child != null && child.props && child.type === IonTabButton) {
|
||||
const href = (child.props.tab === activeTab) ? this.props.currentPath : (this.state.tabs[child.props.tab!].currentHref);
|
||||
const href = (child.props.tab === activeTab) ? this.props.routeInfo?.pathname : (this.state.tabs[child.props.tab!].currentHref);
|
||||
const routeOptions = (child.props.tab === activeTab) ? this.props.routeInfo?.routeOptions : (this.state.tabs[child.props.tab!].currentRouteOptions);
|
||||
|
||||
return React.cloneElement(child, {
|
||||
href,
|
||||
onIonTabButtonClick: this.onTabButtonClick
|
||||
routeOptions,
|
||||
onClick: this.onTabButtonClick
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@ -171,7 +184,8 @@ const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({
|
||||
<IonTabBarUnwrapped
|
||||
ref={forwardedRef}
|
||||
{...props as any}
|
||||
currentPath={props.currentPath || context.currentPath}
|
||||
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
|
||||
onSetCurrentTab={context.setCurrentTab}
|
||||
>
|
||||
{props.children}
|
||||
</IonTabBarUnwrapped>
|
||||
|
39
packages/react/src/components/navigation/IonTabButton.tsx
Normal file
39
packages/react/src/components/navigation/IonTabButton.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { RouterOptions } from '../../models';
|
||||
import { IonicReactProps } from '../IonicReactProps';
|
||||
import { IonTabButtonInner } from '../inner-proxies';
|
||||
|
||||
type Props = LocalJSX.IonTabButton & IonicReactProps & {
|
||||
routerOptions?: RouterOptions;
|
||||
ref?: React.RefObject<HTMLIonTabButtonElement>;
|
||||
onClick?: (e: any) => void;
|
||||
};
|
||||
|
||||
export class IonTabButton extends React.Component<Props> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
|
||||
}
|
||||
|
||||
handleIonTabButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(new CustomEvent('ionTabButtonClick', {
|
||||
detail: { tab: this.props.tab, href: this.props.href, routeOptions: this.props.routerOptions }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClick, ...rest } = this.props;
|
||||
return (
|
||||
<IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>
|
||||
);
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonTabButton';
|
||||
}
|
||||
}
|
@ -2,14 +2,34 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
import PageManager from '../../routing/PageManager';
|
||||
import { IonRouterOutlet } from '../IonRouterOutlet';
|
||||
|
||||
import { IonTabBar } from './IonTabBar';
|
||||
import { IonTabsContext, IonTabsContextState } from './IonTabsContext';
|
||||
|
||||
class IonTabsElement extends HTMLDivElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
if (window && window.customElements) {
|
||||
customElements.define('ion-tabs', IonTabsElement, { extends: 'div' });
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'ion-tabs': any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
|
||||
|
||||
interface Props extends LocalJSX.IonTabs {
|
||||
className?: string;
|
||||
children: ChildFunction | React.ReactNode;
|
||||
}
|
||||
|
||||
@ -71,7 +91,7 @@ export class IonTabs extends React.Component<Props> {
|
||||
return;
|
||||
}
|
||||
if (child.type === IonRouterOutlet) {
|
||||
outlet = child;
|
||||
outlet = React.cloneElement(child, { tabs: true });
|
||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||
outlet = child.props.children[0];
|
||||
}
|
||||
@ -96,21 +116,34 @@ export class IonTabs extends React.Component<Props> {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet');
|
||||
}
|
||||
if (!tabBar) {
|
||||
// TODO, this is not required
|
||||
throw new Error('IonTabs needs a IonTabBar');
|
||||
}
|
||||
|
||||
const { className, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<IonTabsContext.Provider
|
||||
value={this.ionTabContextState}
|
||||
>
|
||||
<div style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</div>
|
||||
{this.context.hasIonicRouter() ? (
|
||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||
<ion-tabs className="ion-tabs" style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</ion-tabs>
|
||||
</PageManager>
|
||||
) : (
|
||||
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</div>
|
||||
)}
|
||||
</IonTabsContext.Provider >
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { HrefProps } from './hrefprops';
|
||||
// ionic/core
|
||||
export const IonApp = /*@__PURE__*/createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app');
|
||||
export const IonTab = /*@__PURE__*/createReactComponent<JSX.IonTab, HTMLIonTabElement>('ion-tab');
|
||||
export const IonTabButton = /*@__PURE__*/createReactComponent<JSX.IonTabButton, HTMLIonTabButtonElement>('ion-tab-button');
|
||||
export const IonRouterLink = /*@__PURE__*/createReactComponent<HrefProps<JSX.IonRouterLink>, HTMLIonRouterLinkElement>('ion-router-link', true);
|
||||
export const IonAvatar = /*@__PURE__*/createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>('ion-avatar');
|
||||
export const IonBackdrop = /*@__PURE__*/createReactComponent<JSX.IonBackdrop, HTMLIonBackdropElement>('ion-backdrop');
|
||||
|
@ -21,7 +21,6 @@ export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}
|
||||
syncEvent(node, eventNameLc, newProps[name]);
|
||||
}
|
||||
} else {
|
||||
(node as any)[name] = newProps[name];
|
||||
const propType = typeof newProps[name];
|
||||
if (propType === 'string') {
|
||||
node.setAttribute(camelToDashCase(name), newProps[name]);
|
||||
@ -61,20 +60,24 @@ export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: a
|
||||
* Checks if an event is supported in the current execution environment.
|
||||
* @license Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
export const isCoveredByReact = (eventNameSuffix: string, doc: Document = document) => {
|
||||
const eventName = 'on' + eventNameSuffix;
|
||||
let isSupported = eventName in doc;
|
||||
export const isCoveredByReact = (eventNameSuffix: string) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return true;
|
||||
} else {
|
||||
const eventName = 'on' + eventNameSuffix;
|
||||
let isSupported = eventName in document;
|
||||
|
||||
if (!isSupported) {
|
||||
const element = doc.createElement('div');
|
||||
element.setAttribute(eventName, 'return;');
|
||||
isSupported = typeof (element as any)[eventName] === 'function';
|
||||
if (!isSupported) {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute(eventName, 'return;');
|
||||
isSupported = typeof (element as any)[eventName] === 'function';
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
};
|
||||
|
||||
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, eventName: string, newEventHandler?: (e: Event) => any) => {
|
||||
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined; }; }, eventName: string, newEventHandler?: (e: Event) => any) => {
|
||||
const eventStore = node.__events || (node.__events = {});
|
||||
const oldEventHandler = eventStore[eventName];
|
||||
|
||||
|
@ -16,6 +16,7 @@ export const createForwardRef = <PropType, ElementType>(ReactComponent: any, dis
|
||||
|
||||
export * from './attachProps';
|
||||
export * from './case';
|
||||
export * from './ionRenderToString';
|
||||
|
||||
export const isPlatform = (platform: Platforms) => {
|
||||
return isPlatformCore(window, platform);
|
||||
|
37
packages/react/src/components/utils/ionRenderToString.ts
Normal file
37
packages/react/src/components/utils/ionRenderToString.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { SerializeDocumentOptions, renderToString } from '@ionic/core/hydrate';
|
||||
|
||||
export async function ionRenderToString(html: string, userAgent: string, options: SerializeDocumentOptions = {}) {
|
||||
|
||||
const renderToStringOptions = Object.assign({}, {
|
||||
clientHydrateAnnotations: false,
|
||||
excludeComponents: [
|
||||
// overlays
|
||||
'ion-action-sheet',
|
||||
'ion-alert',
|
||||
'ion-loading',
|
||||
'ion-modal',
|
||||
'ion-picker',
|
||||
'ion-popover',
|
||||
'ion-toast',
|
||||
|
||||
// navigation
|
||||
'ion-router',
|
||||
'ion-route',
|
||||
'ion-route-redirect',
|
||||
'ion-router-link',
|
||||
'ion-router-outlet',
|
||||
|
||||
// tabs
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
|
||||
// auxiliary
|
||||
'ion-picker-column',
|
||||
'ion-virtual-scroll'
|
||||
],
|
||||
userAgent
|
||||
}, options);
|
||||
|
||||
const ionHtml = await renderToString(html, renderToStringOptions);
|
||||
return ionHtml.html;
|
||||
}
|
Reference in New Issue
Block a user