feat(react): complete controller integrations and navigation (#16849)

* fix(react): correct controller types and reexport AlertOptions.

* feat(): add Ion Stack and Tabs navigation items based on React Router.

* rework tabs and add router outlet

* fixes to the outlet rendering.

* add direction as state

* fixed transitions

* Update to core rc2.
This commit is contained in:
Josh Thomas
2019-01-22 14:09:58 -06:00
committed by GitHub
parent 3612651334
commit f46cd507c2
16 changed files with 479 additions and 55 deletions

View File

@ -37,13 +37,22 @@
"@types/node": "10.12.9", "@types/node": "10.12.9",
"@types/react": "16.7.6", "@types/react": "16.7.6",
"@types/react-dom": "16.0.9", "@types/react-dom": "16.0.9",
"react": "latest", "@types/react-router": "^4.4.3",
"react-dom": "latest", "react": "^16.7.0",
"typescript": "3.1.1" "react-dom": "^16.7.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"typescript": "^3.2.2",
"np": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"@ionic/core": "4.0.0-rc.0", "@ionic/core": "4.0.0-rc.2",
"ionicons": "^4.5.0", "ionicons": "^4.5.0"
"np": "^3.1.0" },
"peerDependencies": {
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1"
} }
} }

View File

@ -1,8 +1,7 @@
import { Components } from '@ionic/core'; import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent'; import { createOverlayComponent } from './createOverlayComponent';
import { Omit } from './types';
export type ActionSheetOptions = Omit<Components.IonActionSheetAttributes, 'overlayIndex'>; export type ActionSheetOptions = Components.IonActionSheetAttributes;
const IonActionSheet = createOverlayComponent<ActionSheetOptions, HTMLIonActionSheetElement, HTMLIonActionSheetControllerElement>('ion-action-sheet', 'ion-action-sheet-controller') const IonActionSheet = createOverlayComponent<ActionSheetOptions, HTMLIonActionSheetElement, HTMLIonActionSheetControllerElement>('ion-action-sheet', 'ion-action-sheet-controller')
export default IonActionSheet; export default IonActionSheet;

View File

@ -1,8 +1,7 @@
import { Components } from '@ionic/core'; import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent'; import { createControllerComponent } from './createControllerComponent';
import { Omit } from './types';
export type AlertOptions = Omit<Components.IonAlertAttributes, 'overlayIndex'>; export type AlertOptions = Components.IonAlertAttributes;
const IonAlert = createControllerComponent<AlertOptions, HTMLIonAlertElement, HTMLIonAlertControllerElement>('ion-alert', 'ion-alert-controller') const IonAlert = createControllerComponent<AlertOptions, HTMLIonAlertElement, HTMLIonAlertControllerElement>('ion-alert', 'ion-alert-controller')
export default IonAlert; export default IonAlert;

View File

@ -1,8 +1,7 @@
import { Components } from '@ionic/core'; import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent'; import { createControllerComponent } from './createControllerComponent';
import { Omit } from './types';
export type LoadingOptions = Omit<Components.IonLoadingAttributes, 'overlayIndex'>; export type LoadingOptions = Components.IonLoadingAttributes;
const IonActionSheet = createControllerComponent<LoadingOptions, HTMLIonLoadingElement, HTMLIonLoadingControllerElement>('ion-loading', 'ion-loading-controller') const IonActionSheet = createControllerComponent<LoadingOptions, HTMLIonLoadingElement, HTMLIonLoadingControllerElement>('ion-loading', 'ion-loading-controller')
export default IonActionSheet; export default IonActionSheet;

View File

@ -2,7 +2,7 @@ import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent'; import { createOverlayComponent } from './createOverlayComponent';
import { Omit } from './types'; import { Omit } from './types';
export type ModalOptions = Omit<Components.IonModalAttributes, 'delegate' | 'overlayIndex' | 'component' | 'componentProps'> & { export type ModalOptions = Omit<Components.IonModalAttributes, 'component' | 'componentProps'> & {
children: React.ReactNode; children: React.ReactNode;
}; };

View File

@ -2,7 +2,7 @@ import { Components } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent'; import { createOverlayComponent } from './createOverlayComponent';
import { Omit } from './types'; import { Omit } from './types';
export type PopoverOptions = Omit<Components.IonPopoverAttributes, 'delegate' | 'overlayIndex' | 'component' | 'componentProps'> & { export type PopoverOptions = Omit<Components.IonPopoverAttributes, 'component' | 'componentProps'> & {
children: React.ReactNode; children: React.ReactNode;
}; };

View File

@ -1,8 +1,7 @@
import { Components } from '@ionic/core'; import { Components } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent'; import { createControllerComponent } from './createControllerComponent';
import { Omit } from './types';
export type ToastOptions = Omit<Components.IonToastAttributes, 'overlayIndex'>; export type ToastOptions = Components.IonToastAttributes;
const IonToast = createControllerComponent<ToastOptions, HTMLIonToastElement, HTMLIonToastControllerElement>('ion-toast', 'ion-toast-controller') const IonToast = createControllerComponent<ToastOptions, HTMLIonToastElement, HTMLIonToastControllerElement>('ion-toast', 'ion-toast-controller')
export default IonToast; export default IonToast;

View File

@ -2,20 +2,21 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { dashToPascalCase, attachEventProps } from './utils'; import { dashToPascalCase, attachEventProps } from './utils';
export function createReactComponent<T, E>(tagName: string) { export function createReactComponent<T extends object, E>(tagName: string) {
const displayName = dashToPascalCase(tagName); const displayName = dashToPascalCase(tagName);
type IonicReactInternalProps = { type IonicReactInternalProps = {
forwardedRef?: React.RefObject<E>; forwardedRef?: React.RefObject<E>;
children?: React.ReactNode; children?: React.ReactNode;
} }
type InternalProps = T & IonicReactInternalProps;
type IonicReactExternalProps = { type IonicReactExternalProps = {
ref?: React.RefObject<E>; ref?: React.RefObject<E>;
children?: React.ReactNode; children?: React.ReactNode;
} }
class ReactComponent extends React.Component<T & IonicReactInternalProps> { class ReactComponent extends React.Component<InternalProps> {
componentRef: React.RefObject<E>; componentRef: React.RefObject<E>;
constructor(props: T & IonicReactInternalProps) { constructor(props: T & IonicReactInternalProps) {
@ -31,7 +32,7 @@ export function createReactComponent<T, E>(tagName: string) {
this.componentWillReceiveProps(this.props); this.componentWillReceiveProps(this.props);
} }
componentWillReceiveProps(props: any) { componentWillReceiveProps(props: InternalProps) {
const node = ReactDOM.findDOMNode(this); const node = ReactDOM.findDOMNode(this);
if (!(node instanceof HTMLElement)) { if (!(node instanceof HTMLElement)) {
@ -42,14 +43,20 @@ export function createReactComponent<T, E>(tagName: string) {
} }
render() { render() {
const { children, forwardedRef, ...cProps } = this.props as any; const { children, forwardedRef, ...cProps } = this.props;
cProps.ref = forwardedRef;
return React.createElement(tagName, cProps, children); return React.createElement(
tagName,
{
...cProps,
ref: forwardedRef
},
children
);
} }
} }
function forwardRef(props: T & IonicReactInternalProps, ref: React.RefObject<E>) { function forwardRef(props: InternalProps, ref: React.RefObject<E>) {
return <ReactComponent {...props} forwardedRef={ref} />; return <ReactComponent {...props} forwardedRef={ref} />;
} }
forwardRef.displayName = displayName; forwardRef.displayName = displayName;

View File

@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
import { attachEventProps } from './utils' import { attachEventProps } from './utils'
import { ensureElementInBody, dashToPascalCase } from './utils'; import { ensureElementInBody, dashToPascalCase } from './utils';
import { OverlayComponentElement, OverlayControllerComponentElement } from './types';
export function createControllerComponent<T, E extends HTMLElement, C extends HTMLElement>(tagName: string, controllerTagName: string) { export function createControllerComponent<T extends object, E extends OverlayComponentElement, C extends OverlayControllerComponentElement<E>>(tagName: string, controllerTagName: string) {
const displayName = dashToPascalCase(tagName); const displayName = dashToPascalCase(tagName);
type IonicReactInternalProps = { type ReactProps = {
forwardedRef?: React.RefObject<E>; show: boolean;
children?: React.ReactNode;
show: boolean
} }
type Props = T & ReactProps;
return class ReactControllerComponent extends React.Component<T & IonicReactInternalProps> { return class ReactControllerComponent extends React.Component<Props> {
element: E; element: E;
controllerElement: C; controllerElement: C;
constructor(props: T & IonicReactInternalProps) { constructor(props: Props) {
super(props); super(props);
} }
@ -25,20 +25,20 @@ export function createControllerComponent<T, E extends HTMLElement, C extends HT
async componentDidMount() { async componentDidMount() {
this.controllerElement = ensureElementInBody<C>(controllerTagName); this.controllerElement = ensureElementInBody<C>(controllerTagName);
await (this.controllerElement as any).componentOnReady(); await this.controllerElement.componentOnReady();
} }
async componentDidUpdate(prevProps: T & IonicReactInternalProps) { async componentDidUpdate(prevProps: Props) {
if (prevProps.show !== this.props.show && this.props.show === true) { if (prevProps.show !== this.props.show && this.props.show === true) {
const { children, show, ...cProps} = this.props as any; const { show, ...cProps} = this.props;
this.element = await (this.controllerElement as any).create(cProps); this.element = await this.controllerElement.create(cProps);
await (this.element as any).present(); await this.element.present();
attachEventProps(this.element, cProps); attachEventProps(this.element, cProps);
} }
if (prevProps.show !== this.props.show && this.props.show === false) { if (prevProps.show !== this.props.show && this.props.show === false) {
return await (this.element as any).dismiss(); await this.element.dismiss();
} }
} }

View File

@ -2,22 +2,23 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { attachEventProps } from './utils' import { attachEventProps } from './utils'
import { ensureElementInBody, dashToPascalCase } from './utils'; import { ensureElementInBody, dashToPascalCase } from './utils';
import { OverlayComponentElement, OverlayControllerComponentElement } from './types';
export function createOverlayComponent<T, E extends HTMLElement, C extends HTMLElement>(tagName: string, controllerTagName: string) { export function createOverlayComponent<T extends object, E extends OverlayComponentElement, C extends OverlayControllerComponentElement<E>>(tagName: string, controllerTagName: string) {
const displayName = dashToPascalCase(tagName); const displayName = dashToPascalCase(tagName);
type IonicReactInternalProps = { type ReactProps = {
forwardedRef?: React.RefObject<E>;
children: React.ReactNode; children: React.ReactNode;
show: boolean; show: boolean;
} }
type Props = T & ReactProps;
return class ReactControllerComponent extends React.Component<T & IonicReactInternalProps> { return class ReactControllerComponent extends React.Component<Props> {
element: E; element: E;
controllerElement: C; controllerElement: C;
el: HTMLDivElement; el: HTMLDivElement;
constructor(props: T & IonicReactInternalProps) { constructor(props: Props) {
super(props); super(props);
this.el = document.createElement('div'); this.el = document.createElement('div');
@ -29,22 +30,24 @@ export function createOverlayComponent<T, E extends HTMLElement, C extends HTMLE
async componentDidMount() { async componentDidMount() {
this.controllerElement = ensureElementInBody<C>(controllerTagName); this.controllerElement = ensureElementInBody<C>(controllerTagName);
await (this.controllerElement as any).componentOnReady(); await this.controllerElement.componentOnReady();
} }
async componentDidUpdate(prevProps: T & IonicReactInternalProps) { async componentDidUpdate(prevProps: Props) {
if (prevProps.show !== this.props.show && this.props.show === true) { if (prevProps.show !== this.props.show && this.props.show === true) {
const { children, show, ...cProps} = this.props as any; const { children, show, ...cProps} = this.props;
cProps.component = this.el;
cProps.componentProps = {};
this.element = await (this.controllerElement as any).create(cProps); this.element = await this.controllerElement.create({
await (this.element as any).present(); ...cProps,
component: this.el,
componentProps: {}
});
await this.element.present();
attachEventProps(this.element, cProps); attachEventProps(this.element, cProps);
} }
if (prevProps.show !== this.props.show && this.props.show === false) { if (prevProps.show !== this.props.show && this.props.show === false) {
return await (this.element as any).dismiss(); await this.element.dismiss();
} }
} }

View File

@ -2,12 +2,23 @@ import { Components as IoniconsComponents } from 'ionicons';
import { Components } from '@ionic/core'; import { Components } from '@ionic/core';
import { createReactComponent } from './createComponent'; import { createReactComponent } from './createComponent';
export { AlertButton, AlertInput } from '@ionic/core';
export { default as IonActionSheet } from './IonActionSheet'; export { default as IonActionSheet } from './IonActionSheet';
export { default as IonAlert } from './IonAlert'; export { default as IonAlert } from './IonAlert';
export { default as IonLoading } from './IonLoading'; export { default as IonLoading } from './IonLoading';
export { default as IonModal } from './IonModal'; export { default as IonModal } from './IonModal';
export { default as IonPopover } from './IonPopover'; export { default as IonPopover } from './IonPopover';
export { default as IonToast } from './IonToast'; export { default as IonToast } from './IonToast';
export { default as IonTabs } from './navigation/IonTabs';
export { default as IonTabBar } from './navigation/IonTabBar';
export { IonRouterOutlet, IonBackButton } from './navigation/IonRouterOutlet';
export const IonTabBarInner = createReactComponent<Components.IonTabBarAttributes, HTMLIonTabBarElement>('ion-tab-bar');
export const IonRouterOutletInner = createReactComponent<Components.IonRouterOutletAttributes, HTMLIonRouterOutletElement>('ion-router-outlet');
export const IonBackButtonInner = createReactComponent<Components.IonBackButtonAttributes, HTMLIonBackButtonElement>('ion-back-button');
export const IonTab = createReactComponent<Components.IonTabAttributes, HTMLIonTabElement>('ion-tab');
export const IonTabButton = createReactComponent<Components.IonTabButtonAttributes, HTMLIonTabButtonElement>('ion-tab-button');
export const IonAnchor = createReactComponent<Components.IonAnchorAttributes, HTMLIonAnchorElement>('ion-anchor'); export const IonAnchor = createReactComponent<Components.IonAnchorAttributes, HTMLIonAnchorElement>('ion-anchor');
export const IonApp = createReactComponent<Components.IonAppAttributes, HTMLIonAppElement>('ion-app'); export const IonApp = createReactComponent<Components.IonAppAttributes, HTMLIonAppElement>('ion-app');
@ -73,10 +84,6 @@ export const IonSlide = createReactComponent<Components.IonSlideAttributes, HTML
export const IonSlides = createReactComponent<Components.IonSlidesAttributes, HTMLIonSlidesElement>('ion-slides'); export const IonSlides = createReactComponent<Components.IonSlidesAttributes, HTMLIonSlidesElement>('ion-slides');
export const IonSpinner = createReactComponent<Components.IonSpinnerAttributes, HTMLIonSpinnerElement>('ion-spinner'); export const IonSpinner = createReactComponent<Components.IonSpinnerAttributes, HTMLIonSpinnerElement>('ion-spinner');
export const IonSplitPane = createReactComponent<Components.IonSplitPaneAttributes, HTMLIonSplitPaneElement>('ion-split-pane'); export const IonSplitPane = createReactComponent<Components.IonSplitPaneAttributes, HTMLIonSplitPaneElement>('ion-split-pane');
export const IonTab = createReactComponent<Components.IonTabAttributes, HTMLIonTabElement>('ion-tab');
export const IonTabBar = createReactComponent<Components.IonTabBarAttributes, HTMLIonTabBarElement>('ion-tab-bar');
export const IonTabButton = createReactComponent<Components.IonTabButtonAttributes, HTMLIonTabButtonElement>('ion-tab-button');
export const IonTabs = createReactComponent<Components.IonTabsAttributes, HTMLIonTabsElement>('ion-tabs');
export const IonText = createReactComponent<Components.IonTextAttributes, HTMLIonTextElement>('ion-text'); export const IonText = createReactComponent<Components.IonTextAttributes, HTMLIonTextElement>('ion-text');
export const IonTextarea = createReactComponent<Components.IonTextareaAttributes, HTMLIonTextareaElement>('ion-textarea'); export const IonTextarea = createReactComponent<Components.IonTextareaAttributes, HTMLIonTextareaElement>('ion-textarea');
export const IonThumbnail = createReactComponent<Components.IonThumbnailAttributes, HTMLIonThumbnailElement>('ion-thumbnail'); export const IonThumbnail = createReactComponent<Components.IonThumbnailAttributes, HTMLIonThumbnailElement>('ion-thumbnail');

View File

@ -0,0 +1,245 @@
import React, { Component } from 'react';
import { withRouter, RouteComponentProps, matchPath, match, RouteProps } from 'react-router';
import { Components } from '@ionic/core';
import { generateUniqueId } from '../utils';
import { Location } from 'history';
import { IonBackButtonInner, IonRouterOutletInner } from '../index';
type ChildProps = RouteProps & {
computedMatch: match<any>
}
type Props = RouteComponentProps & {
children?: React.ReactElement<ChildProps>[] | React.ReactElement<ChildProps>;
};
interface StackItem {
id: string;
location: Location;
match: match<{ tab: string }>;
element: React.ReactElement<any>;
prevId: string;
}
interface State {
direction: 'forward' | 'back' | undefined,
inTransition: boolean;
activeId: string | undefined;
prevActiveId: string | undefined;
tabActiveIds: { [tab: string]: string };
views: StackItem[];
}
interface ContextInterface {
goBack: () => void
}
const Context = React.createContext<ContextInterface>({
goBack: () => {}
});
class RouterOutlet extends Component<Props, State> {
enteringEl: React.RefObject<HTMLDivElement> = React.createRef();
leavingEl: React.RefObject<HTMLDivElement> = React.createRef();
containerEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
constructor(props: Props) {
super(props);
this.state = {
direction: undefined,
inTransition: false,
activeId: undefined,
prevActiveId: undefined,
tabActiveIds: {},
views: []
};
}
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
const location = props.location;
let match: StackItem['match'] = null;
let element: StackItem['element'];
/**
* Get the current active view and if the path is the same then do nothing
*/
const activeView = state.views.find(v => v.id === state.activeId);
/**
* Look at all available paths and find the one that matches
*/
React.Children.forEach(props.children, (child: React.ReactElement<ChildProps>) => {
if (match == null) {
element = child;
match = matchPath(location.pathname, child.props);
}
});
/**
* If there are no matches then set the active view to null and exit
*/
if (!match) {
return {
direction: undefined,
activeId: undefined,
prevActiveId: undefined
};
}
/**
* Get the active view for the tab that matches.
* If the location matches the existing tab path then set that view as active
*/
const id = state.tabActiveIds[match.params.tab];
const currentActiveTabView = state.views.find(v => v.id === id);
if (currentActiveTabView && currentActiveTabView.location.pathname === props.location.pathname) {
if (currentActiveTabView.id === state.activeId) {
return null;
}
return {
direction: undefined,
activeId: currentActiveTabView.id,
prevActiveId: undefined
};
}
/**
* If the new active view is a previous view
*/
if (activeView) {
const prevActiveView = state.views.find(v => v.id === activeView.prevId);
if (prevActiveView && activeView.match.params.tab === match.params.tab && prevActiveView.match.url === match.url) {
return {
direction: 'back',
activeId: prevActiveView.id,
prevActiveId: activeView.id,
tabActiveIds: {
...state.tabActiveIds,
[match.params.tab]: prevActiveView.id,
},
}
}
}
const viewId = generateUniqueId();
return {
direction: (state.tabActiveIds[match.params.tab]) ? 'forward' : undefined,
activeId: viewId,
prevActiveId: state.tabActiveIds[match.params.tab],
tabActiveIds: {
...state.tabActiveIds,
[match.params.tab]: viewId
},
views: state.views.concat({
id: viewId,
location,
match,
element,
prevId: state.tabActiveIds[match.params.tab]
})
};
}
renderChild(item: StackItem) {
return React.cloneElement(item.element, {
location: item.location,
computedMatch: item.match
});
}
goBack = () => {
const prevView = this.state.views.find(v => v.id === this.state.activeId);
const newView = this.state.views.find(v => v.id === prevView.prevId);
this.props.history.replace(newView.location.pathname);
}
componentDidUpdate() {
const enteringEl = (this.enteringEl.current != null) ? this.enteringEl.current : undefined;
const leavingEl = (this.leavingEl.current != null) ? this.leavingEl.current : undefined;
if (this.state.direction && !this.state.inTransition) {
this.setState({ inTransition: true });
this.containerEl.current.commit(enteringEl, leavingEl, {
deepWait: true,
duration: this.state.direction === undefined ? 0: undefined,
direction: this.state.direction,
showGoBack: true,
progressAnimation: false
}).then(() => {
this.setState(() => ({
inTransition: false,
direction: undefined
}));
});
}
}
render() {
return (
<IonRouterOutletInner ref={this.containerEl}>
<Context.Provider value={{ goBack: this.goBack }}>
{this.state.views.map((item) => {
let props: any = {};
if (item.id === this.state.prevActiveId) {
props = {
'ref': this.leavingEl,
'hidden': this.state.direction == null,
'className': 'ion-page' + (this.state.direction == null ? ' ion-page-hidden' : '')
};
} else if (item.id === this.state.activeId) {
props = {
'ref': this.enteringEl,
'className': 'ion-page' + (this.state.direction != null ? ' ion-page-invisible' : '')
};
} else {
props = {
'aria-hidden': true,
'className': 'ion-page ion-page-hidden'
};
}
return (
<div
{...props}
key={item.id}
>
{ this.renderChild(item) }
</div>
);
})}
</Context.Provider>
</IonRouterOutletInner>
);
}
}
export const IonRouterOutlet = withRouter(RouterOutlet);
type ButtonProps = Components.IonBackButtonAttributes & {
goBack: () => void;
};
export class IonBackButton extends Component<ButtonProps> {
context!: React.ContextType<typeof Context>;
clickButton = (e: MouseEvent) => {
e.stopPropagation();
this.context.goBack();
}
render() {
return (
<IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>
);
}
}
IonBackButton.contextType = Context;

View File

@ -0,0 +1,88 @@
import React, { Component } from 'react';
import { IonTabBarInner, IonTabButton } from '../index';
import { withRouter, RouteComponentProps } from 'react-router';
import { Components } from '@ionic/core';
type Props = RouteComponentProps & Components.IonTabBarAttributes & {
children: React.ReactNode;
}
type Tab = {
originalHref: string,
currentHref: string
}
type State = {
activeTab: string | null,
tabs: { [key: string]: Tab }
}
class IonTabBar extends Component<Props, State> {
constructor(props: Props) {
super(props);
const tabActiveUrls: { [key: string]: Tab } = {};
React.Children.forEach(this.props.children, (child) => {
if (typeof child === 'object' && child.type === IonTabButton) {
tabActiveUrls[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href
};
}
});
this.state = {
activeTab: null,
tabs: tabActiveUrls
}
}
static getDerivedStateFromProps(props: Props, state: State) {
const activeTab = Object.keys(state.tabs)
.find(key => {
const href = state.tabs[key].originalHref;
return props.location.pathname.startsWith(href)
});
if (!activeTab || (activeTab === state.activeTab && state.tabs[activeTab].currentHref === props.location.pathname)) {
return null;
}
return {
activeTab,
tabs: {
...state.tabs,
[activeTab]: {
originalHref: state.tabs[activeTab].originalHref,
currentHref: props.location.pathname
}
}
}
}
onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => {
this.props.history.push(e.detail.href);
}
renderChild = (activeTab: string) => (child: React.ReactElement<Components.IonTabButtonAttributes & { onIonTabButtonClick: (e: CustomEvent) => void }>) => {
const href = (child.props.tab === activeTab) ? this.props.location.pathname : (this.state.tabs[child.props.tab].currentHref);
return React.cloneElement(child, {
href,
onIonTabButtonClick: this.onTabButtonClick
})
}
render() {
return (
<IonTabBarInner {...this.props} selectedTab={this.state.activeTab}>
{ React.Children.map(this.props.children, this.renderChild(this.state.activeTab)) }
</IonTabBarInner>
);
}
}
export default withRouter(IonTabBar);

View File

@ -0,0 +1,53 @@
import React, { Component } from 'react';
import { IonTabBar, IonRouterOutlet } from '../index';
type Props = {
children: React.ReactNode;
}
const hostStyles: React.CSSProperties = {
display: 'flex',
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
flexDirection: 'column',
width: '100%',
height: '100%',
contain: 'layout size style'
};
const tabsInner: React.CSSProperties = {
position: 'relative',
flex: 1,
contain: 'layout size style'
};
export default class IonTabs extends Component<Props> {
render() {
let outlet: React.ReactElement<{}>;
let tabBar: React.ReactElement<{ slot: 'bottom' | 'top' }>;
React.Children.forEach(this.props.children, child => {
if (typeof child === 'object' && child.type === IonRouterOutlet) {
outlet = child;
}
if (typeof child === 'object' && child.type === IonTabBar) {
tabBar = child;
}
});
return (
<div style={hostStyles}>
{ tabBar.props.slot === 'top' ? tabBar : null }
<div style={tabsInner} className="tabs-inner">
{ outlet }
</div>
{ tabBar.props.slot === 'bottom' ? tabBar : null }
</div>
);
}
}

View File

@ -1,2 +1,10 @@
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface OverlayComponentElement extends HTMLStencilElement {
'present': () => Promise<void>;
'dismiss': (data?: any, role?: string | undefined) => Promise<boolean>;
}
export interface OverlayControllerComponentElement<E extends OverlayComponentElement> extends HTMLStencilElement {
'create': (opts: any) => Promise<E>;
}

View File

@ -41,3 +41,11 @@ export function attachEventProps<E extends HTMLElement>(node: E, props: any) {
} }
}); });
} }
export function generateUniqueId() {
return ([1e7].toString() + -1e3.toString() + -4e3.toString() + -8e3.toString() + -1e11.toString()).replace(/[018]/g, function(c: any) {
const random = window.crypto.getRandomValues(new Uint8Array(1)) as Uint8Array;
return (c ^ random[0] & 15 >> c / 4).toString(16);
});
}