feat(react): React Router Enhancements (#21693)

This commit is contained in:
Ely Lucas
2020-07-07 11:02:05 -06:00
committed by GitHub
parent a0735b97bf
commit c171ccbd37
245 changed files with 26872 additions and 1126 deletions

View File

@ -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;
}

View File

@ -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>

View 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';
}
}

View File

@ -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 >
);
}