mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 04:14:21 +08:00
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:
@ -37,13 +37,22 @@
|
||||
"@types/node": "10.12.9",
|
||||
"@types/react": "16.7.6",
|
||||
"@types/react-dom": "16.0.9",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"typescript": "3.1.1"
|
||||
"@types/react-router": "^4.4.3",
|
||||
"react": "^16.7.0",
|
||||
"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": {
|
||||
"@ionic/core": "4.0.0-rc.0",
|
||||
"ionicons": "^4.5.0",
|
||||
"np": "^3.1.0"
|
||||
"@ionic/core": "4.0.0-rc.2",
|
||||
"ionicons": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Components } from '@ionic/core';
|
||||
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')
|
||||
export default IonActionSheet;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Components } from '@ionic/core';
|
||||
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')
|
||||
export default IonAlert;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Components } from '@ionic/core';
|
||||
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')
|
||||
export default IonActionSheet;
|
||||
|
||||
@ -2,7 +2,7 @@ import { Components } from '@ionic/core';
|
||||
import { createOverlayComponent } from './createOverlayComponent';
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { Components } from '@ionic/core';
|
||||
import { createOverlayComponent } from './createOverlayComponent';
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Components } from '@ionic/core';
|
||||
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')
|
||||
export default IonToast;
|
||||
|
||||
@ -2,20 +2,21 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
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);
|
||||
|
||||
type IonicReactInternalProps = {
|
||||
forwardedRef?: React.RefObject<E>;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
type InternalProps = T & IonicReactInternalProps;
|
||||
|
||||
type IonicReactExternalProps = {
|
||||
ref?: React.RefObject<E>;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
class ReactComponent extends React.Component<T & IonicReactInternalProps> {
|
||||
class ReactComponent extends React.Component<InternalProps> {
|
||||
componentRef: React.RefObject<E>;
|
||||
|
||||
constructor(props: T & IonicReactInternalProps) {
|
||||
@ -31,7 +32,7 @@ export function createReactComponent<T, E>(tagName: string) {
|
||||
this.componentWillReceiveProps(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
componentWillReceiveProps(props: InternalProps) {
|
||||
const node = ReactDOM.findDOMNode(this);
|
||||
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
@ -42,14 +43,20 @@ export function createReactComponent<T, E>(tagName: string) {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, forwardedRef, ...cProps } = this.props as any;
|
||||
cProps.ref = forwardedRef;
|
||||
const { children, forwardedRef, ...cProps } = this.props;
|
||||
|
||||
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} />;
|
||||
}
|
||||
forwardRef.displayName = displayName;
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import React from 'react';
|
||||
import { attachEventProps } 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);
|
||||
|
||||
type IonicReactInternalProps = {
|
||||
forwardedRef?: React.RefObject<E>;
|
||||
children?: React.ReactNode;
|
||||
show: boolean
|
||||
type ReactProps = {
|
||||
show: boolean;
|
||||
}
|
||||
type Props = T & ReactProps;
|
||||
|
||||
return class ReactControllerComponent extends React.Component<T & IonicReactInternalProps> {
|
||||
return class ReactControllerComponent extends React.Component<Props> {
|
||||
element: E;
|
||||
controllerElement: C;
|
||||
|
||||
constructor(props: T & IonicReactInternalProps) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@ -25,20 +25,20 @@ export function createControllerComponent<T, E extends HTMLElement, C extends HT
|
||||
|
||||
async componentDidMount() {
|
||||
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) {
|
||||
const { children, show, ...cProps} = this.props as any;
|
||||
const { show, ...cProps} = this.props;
|
||||
|
||||
this.element = await (this.controllerElement as any).create(cProps);
|
||||
await (this.element as any).present();
|
||||
this.element = await this.controllerElement.create(cProps);
|
||||
await this.element.present();
|
||||
|
||||
attachEventProps(this.element, cProps);
|
||||
}
|
||||
if (prevProps.show !== this.props.show && this.props.show === false) {
|
||||
return await (this.element as any).dismiss();
|
||||
await this.element.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,22 +2,23 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { attachEventProps } 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);
|
||||
|
||||
type IonicReactInternalProps = {
|
||||
forwardedRef?: React.RefObject<E>;
|
||||
type ReactProps = {
|
||||
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;
|
||||
controllerElement: C;
|
||||
el: HTMLDivElement;
|
||||
|
||||
constructor(props: T & IonicReactInternalProps) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.el = document.createElement('div');
|
||||
@ -29,22 +30,24 @@ export function createOverlayComponent<T, E extends HTMLElement, C extends HTMLE
|
||||
|
||||
async componentDidMount() {
|
||||
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) {
|
||||
const { children, show, ...cProps} = this.props as any;
|
||||
cProps.component = this.el;
|
||||
cProps.componentProps = {};
|
||||
const { children, show, ...cProps} = this.props;
|
||||
|
||||
this.element = await (this.controllerElement as any).create(cProps);
|
||||
await (this.element as any).present();
|
||||
this.element = await this.controllerElement.create({
|
||||
...cProps,
|
||||
component: this.el,
|
||||
componentProps: {}
|
||||
});
|
||||
await this.element.present();
|
||||
|
||||
attachEventProps(this.element, cProps);
|
||||
}
|
||||
if (prevProps.show !== this.props.show && this.props.show === false) {
|
||||
return await (this.element as any).dismiss();
|
||||
await this.element.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,23 @@ import { Components as IoniconsComponents } from 'ionicons';
|
||||
import { Components } from '@ionic/core';
|
||||
import { createReactComponent } from './createComponent';
|
||||
|
||||
export { AlertButton, AlertInput } from '@ionic/core';
|
||||
|
||||
export { default as IonActionSheet } from './IonActionSheet';
|
||||
export { default as IonAlert } from './IonAlert';
|
||||
export { default as IonLoading } from './IonLoading';
|
||||
export { default as IonModal } from './IonModal';
|
||||
export { default as IonPopover } from './IonPopover';
|
||||
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 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 IonSpinner = createReactComponent<Components.IonSpinnerAttributes, HTMLIonSpinnerElement>('ion-spinner');
|
||||
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 IonTextarea = createReactComponent<Components.IonTextareaAttributes, HTMLIonTextareaElement>('ion-textarea');
|
||||
export const IonThumbnail = createReactComponent<Components.IonThumbnailAttributes, HTMLIonThumbnailElement>('ion-thumbnail');
|
||||
|
||||
245
react/src/components/navigation/IonRouterOutlet.tsx
Normal file
245
react/src/components/navigation/IonRouterOutlet.tsx
Normal 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;
|
||||
88
react/src/components/navigation/IonTabBar.tsx
Normal file
88
react/src/components/navigation/IonTabBar.tsx
Normal 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);
|
||||
53
react/src/components/navigation/IonTabs.tsx
Normal file
53
react/src/components/navigation/IonTabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,10 @@
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user