feat(react): Add IonTabsContext to add some missing element methods, closes #19935 (#21171)

This commit is contained in:
Ely Lucas
2020-04-30 15:08:23 -06:00
committed by GitHub
parent ae5f1ddff0
commit 43f9d24824
5 changed files with 111 additions and 22 deletions

View File

@ -18,6 +18,7 @@ export { IonPopover } from './IonPopover';
// Custom Components // Custom Components
export { IonPage } from './IonPage'; export { IonPage } from './IonPage';
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
export { IonTabs } from './navigation/IonTabs'; export { IonTabs } from './navigation/IonTabs';
export { IonTabBar } from './navigation/IonTabBar'; export { IonTabBar } from './navigation/IonTabBar';
export { IonBackButton } from './navigation/IonBackButton'; export { IonBackButton } from './navigation/IonBackButton';

View File

@ -5,28 +5,33 @@ import { NavContext } from '../../contexts/NavContext';
import { IonicReactProps } from '../IonicReactProps'; import { IonicReactProps } from '../IonicReactProps';
import { IonTabBarInner } from '../inner-proxies'; import { IonTabBarInner } from '../inner-proxies';
import { IonTabButton } from '../proxies'; import { IonTabButton } from '../proxies';
import { createForwardRef } from '../utils';
type Props = LocalJSX.IonTabBar & IonicReactProps & { type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & {
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void; onIonTabsDidChange?: (event: CustomEvent<{ tab: string; }>) => void;
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void; onIonTabsWillChange?: (event: CustomEvent<{ tab: string; }>) => void;
currentPath?: string; currentPath?: string;
slot?: 'bottom' | 'top'; slot?: 'bottom' | 'top';
}; };
interface InternalProps extends IonTabBarProps {
forwardedRef?: React.RefObject<HTMLIonIconElement>;
}
interface TabUrls { interface TabUrls {
originalHref: string; originalHref: string;
currentHref: string; currentHref: string;
} }
interface State { interface IonTabBarState {
activeTab: string | undefined; activeTab: string | undefined;
tabs: { [key: string]: TabUrls }; tabs: { [key: string]: TabUrls; };
} }
class IonTabBarUnwrapped extends React.PureComponent<Props, State> { class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarState> {
context!: React.ContextType<typeof NavContext>; context!: React.ContextType<typeof NavContext>;
constructor(props: Props) { constructor(props: InternalProps) {
super(props); super(props);
const tabs: { [key: string]: TabUrls; } = {}; const tabs: { [key: string]: TabUrls; } = {};
@ -39,16 +44,42 @@ class IonTabBarUnwrapped extends React.PureComponent<Props, State> {
} }
}); });
const tabKeys = Object.keys(tabs);
const activeTab = tabKeys
.find(key => {
const href = tabs[key].originalHref;
return props.currentPath!.startsWith(href);
}) || tabKeys[0];
this.state = { this.state = {
activeTab: undefined, activeTab,
tabs tabs
}; };
this.onTabButtonClick = this.onTabButtonClick.bind(this); this.onTabButtonClick = this.onTabButtonClick.bind(this);
this.renderTabButton = this.renderTabButton.bind(this); this.renderTabButton = this.renderTabButton.bind(this);
this.setActiveTabOnContext = this.setActiveTabOnContext.bind(this);
this.selectTab = this.selectTab.bind(this);
} }
static getDerivedStateFromProps(props: Props, state: State) { 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) {
const tabs = { ...state.tabs }; const tabs = { ...state.tabs };
const activeTab = Object.keys(state.tabs) const activeTab = Object.keys(state.tabs)
@ -101,6 +132,7 @@ class IonTabBarUnwrapped extends React.PureComponent<Props, State> {
if (this.props.onIonTabsDidChange) { if (this.props.onIonTabsDidChange) {
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } })); this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
} }
this.setActiveTabOnContext(e.detail.tab);
this.context.navigate(currentHref, 'none'); this.context.navigate(currentHref, 'none');
} }
} }
@ -120,9 +152,7 @@ class IonTabBarUnwrapped extends React.PureComponent<Props, State> {
} }
render() { render() {
const { activeTab } = this.state; const { activeTab } = this.state;
return ( return (
<IonTabBarInner {...this.props} selectedTab={activeTab}> <IonTabBarInner {...this.props} selectedTab={activeTab}>
{React.Children.map(this.props.children as any, this.renderTabButton(activeTab))} {React.Children.map(this.props.children as any, this.renderTabButton(activeTab))}
@ -135,10 +165,11 @@ class IonTabBarUnwrapped extends React.PureComponent<Props, State> {
} }
} }
export const IonTabBar: React.FC<Props> = React.memo<Props>(props => { const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
const context = useContext(NavContext); const context = useContext(NavContext);
return ( return (
<IonTabBarUnwrapped <IonTabBarUnwrapped
ref={forwardedRef}
{...props as any} {...props as any}
currentPath={props.currentPath || context.currentPath} currentPath={props.currentPath || context.currentPath}
> >
@ -146,3 +177,5 @@ export const IonTabBar: React.FC<Props> = React.memo<Props>(props => {
</IonTabBarUnwrapped> </IonTabBarUnwrapped>
); );
}); });
export const IonTabBar = createForwardRef<IonTabBarProps, HTMLIonTabBarElement>(IonTabBarContainer, 'IonTabBar');

View File

@ -1,13 +1,16 @@
import { JSX as LocalJSX } from '@ionic/core'; import { JSX as LocalJSX } from '@ionic/core';
import React from 'react'; import React, { Fragment } from 'react';
import { NavContext } from '../../contexts/NavContext'; import { NavContext } from '../../contexts/NavContext';
import { IonRouterOutlet } from '../IonRouterOutlet'; import { IonRouterOutlet } from '../IonRouterOutlet';
import { IonTabBar } from './IonTabBar'; import { IonTabBar } from './IonTabBar';
import { IonTabsContext, IonTabsContextState } from './IonTabsContext';
type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
interface Props extends LocalJSX.IonTabs { interface Props extends LocalJSX.IonTabs {
children: React.ReactNode; children: ChildFunction | React.ReactNode;
} }
const hostStyles: React.CSSProperties = { const hostStyles: React.CSSProperties = {
@ -32,25 +35,60 @@ const tabsInner: React.CSSProperties = {
export class IonTabs extends React.Component<Props> { export class IonTabs extends React.Component<Props> {
context!: React.ContextType<typeof NavContext>; context!: React.ContextType<typeof NavContext>;
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef(); routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
selectTabHandler?: (tag: string) => boolean;
tabBarRef = React.createRef<any>();
ionTabContextState: IonTabsContextState = {
activeTab: undefined,
selectTab: () => false
};
constructor(props: Props) { constructor(props: Props) {
super(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;
}
}
render() { render() {
let outlet: React.ReactElement<{}> | undefined; let outlet: React.ReactElement<{}> | undefined;
let tabBar: React.ReactElement | undefined; let tabBar: React.ReactElement | undefined;
React.Children.forEach(this.props.children, (child: any) => { const children = typeof this.props.children === 'function' ?
(this.props.children as ChildFunction)(this.ionTabContextState) : this.props.children;
React.Children.forEach(children, (child: any) => {
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) { if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
return; return;
} }
if (child.type === IonRouterOutlet) { if (child.type === IonRouterOutlet) {
outlet = child; outlet = child;
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
outlet = child.props.children[0];
} }
if (child.type === IonTabBar) { if (child.type === IonTabBar) {
const { onIonTabsDidChange, onIonTabsWillChange } = this.props; const { onIonTabsDidChange, onIonTabsWillChange } = this.props;
tabBar = React.cloneElement(child, { onIonTabsDidChange, onIonTabsWillChange }); tabBar = React.cloneElement(child, {
onIonTabsDidChange,
onIonTabsWillChange,
ref: this.tabBarRef
});
} else if (child.type === Fragment && child.props.children[1].type === IonTabBar) {
const { onIonTabsDidChange, onIonTabsWillChange } = this.props;
tabBar = React.cloneElement(child.props.children[1], {
onIonTabsDidChange,
onIonTabsWillChange,
ref: this.tabBarRef
});
} }
}); });
@ -63,13 +101,17 @@ export class IonTabs extends React.Component<Props> {
} }
return ( return (
<div style={hostStyles}> <IonTabsContext.Provider
{tabBar.props.slot === 'top' ? tabBar : null} value={this.ionTabContextState}
<div style={tabsInner} className="tabs-inner"> >
{outlet} <div style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{outlet}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</div> </div>
{tabBar.props.slot === 'bottom' ? tabBar : null} </IonTabsContext.Provider >
</div>
); );
} }

View File

@ -0,0 +1,11 @@
import React from 'react';
export interface IonTabsContextState {
activeTab: string | undefined;
selectTab: (tab: string) => boolean;
}
export const IonTabsContext = React.createContext<IonTabsContextState>({
activeTab: undefined,
selectTab: () => false
});

View File

@ -27,8 +27,10 @@
"jsx-no-lambda": false, "jsx-no-lambda": false,
"jsx-no-multiline-js": false, "jsx-no-multiline-js": false,
"jsx-wrap-multiline": false, "jsx-wrap-multiline": false,
"no-empty": false,
"no-empty-interface": false, "no-empty-interface": false,
"no-unbound-method": false, "no-unbound-method": false,
"no-unused-expression": false,
"prefer-conditional-expression": false "prefer-conditional-expression": false
} }
} }