mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +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/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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
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 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