Master react (#18998)

* chore(): bump to beta 8

* fix(): IonFabButton href fix

* fix(react): support components with href attributes

* fix(): Prep work to break router out

* fix(): breaking react-router and react-core into own packages

* chore(): moving view stuff out of react-core

* chore(): dev build 8-1

* chore(): update to react beta 8

* chore(): fixes to deps

* fix(): removing IonAnchor in favor of IonRouterLink

* chore(): beta 9 release

* refactor(react): treeshake, minify, api

* wip

* fix(): react dev builds

* fix(): fixes to get app builds working again

* fix(): removing tgz file

* feat(): adding platform helper methods

* fix(): don't map attributes to props

* chore(): add test app

* feat(): copy css folder from core

* chore(): move rollup node resolve to devDependencies

* fix(): expose setupConfig()

* perf(): improve treeshaking

* fix(): removing crypto from generateUniqueId

* fix(): adding missing rollup dp

* fix(): test cleanup and fixes to make tests pass

* chore(): moving react to packages folder

* fix(): fixing react build due to move to packages

* feat(): adding missing IonInfiniteScrollContent component

* chore(): add automated testing using cypress

* fix(): adding option onDidDismiss to controller components

* 0.0.10 react

* wip

* fix(): removing deprecated React calls

* fix(): exporting setupConfig from core

* chore(): bump to 4.8.0-rc.0

* chore(): updating test-app deps and fixing test

* chore(): updates to react readme
This commit is contained in:
Manu MA
2019-08-13 22:24:44 +02:00
committed by Ely Lucas
parent 0b1e23f754
commit 930b271a4a
224 changed files with 16337 additions and 1734 deletions

View File

@ -0,0 +1,7 @@
import { JSX, actionSheetController } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
export type ActionSheetOptions = JSX.IonActionSheet;
export const IonActionSheet = /*@__PURE__*/createOverlayComponent<ActionSheetOptions, HTMLIonActionSheetElement>('IonActionSheet', actionSheetController);

View File

@ -0,0 +1,5 @@
import { AlertOptions, alertController } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
export const IonAlert = /*@__PURE__*/createControllerComponent<AlertOptions, HTMLIonAlertElement>('IonAlert', alertController);

View File

@ -0,0 +1,5 @@
import { LoadingOptions, loadingController } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
export const IonLoading = /*@__PURE__*/createControllerComponent<LoadingOptions, HTMLIonLoadingElement>('IonLoading', loadingController);

View File

@ -0,0 +1,9 @@
import { ModalOptions, modalController } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
export type ReactModalOptions = Omit<ModalOptions, 'component' | 'componentProps'> & {
children: React.ReactNode;
};
export const IonModal = /*@__PURE__*/createOverlayComponent<ReactModalOptions, HTMLIonModalElement>('IonModal', modalController);

View File

@ -0,0 +1,25 @@
import React from 'react';
import { createForwardRef } from './utils';
type Props = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
type InternalProps = Props & {
forwardedRef?: React.Ref<HTMLDivElement>
};
type ExternalProps = Props & {
ref?: React.Ref<HTMLDivElement>
};
const IonPageInternal: React.FC<InternalProps> = ({ children, forwardedRef, className, ...props }) => (
<div
className={className !== undefined ? `ion-page ${className}` : 'ion-page'}
ref={forwardedRef}
{...props}
>
{children}
</div>
);
export const IonPage = /*@__PURE__*/createForwardRef<ExternalProps, HTMLDivElement>(IonPageInternal, 'IonPage');

View File

@ -0,0 +1,9 @@
import { PopoverOptions, popoverController } from '@ionic/core';
import { createOverlayComponent } from './createOverlayComponent';
export type ReactPopoverOptions = Omit<PopoverOptions, 'component' | 'componentProps'> & {
children: React.ReactNode;
};
export const IonPopover = /*@__PURE__*/createOverlayComponent<ReactPopoverOptions, HTMLIonPopoverElement>('IonPopover', popoverController);

View File

@ -0,0 +1,5 @@
import { ToastOptions, toastController } from '@ionic/core';
import { createControllerComponent } from './createControllerComponent';
export const IonToast = /*@__PURE__*/createControllerComponent<ToastOptions, HTMLIonToastElement>('IonToast', toastController);

View File

@ -0,0 +1,4 @@
export interface ReactProps {
className?: string;
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { render, fireEvent, cleanup } from 'react-testing-library';
import { IonButton } from '../index';
import { defineCustomElements } from '@ionic/core/loader';
describe('IonButton', () => {
beforeAll(async (done) => {
await defineCustomElements(window);
done();
})
afterEach(cleanup);
it('should render a button', () => {
const { getByText, } = render(<IonButton>my button</IonButton>);
const button = getByText('my button');
expect(button).toBeDefined();
});
it('when the button is clicked, it should call the click handler', () => {
const clickSpy = jest.fn();
const { getByText, } = render(<IonButton onClick={clickSpy}>my button</IonButton>);
const button = getByText('my button');
fireEvent.click(button);
expect(clickSpy).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,78 @@
import React from 'react';
import { IonTabs, IonTabButton, IonLabel, IonIcon, IonTabBar} from '../index';
import { render, cleanup } from 'react-testing-library';
import { IonRouterOutlet } from '../proxies';
afterEach(cleanup)
describe('IonTabs', () => {
test('should render happy path', () => {
const { container } = render(
<IonTabs>
<IonRouterOutlet></IonRouterOutlet>
<IonTabBar slot="bottom" currentPath={'/'} navigate={() => {}}>
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>
<IonIcon name="schedule"></IonIcon>
</IonTabButton>
<IonTabButton tab="speakers">
<IonLabel>Speakers</IonLabel>
<IonIcon name="speakers"></IonIcon>
</IonTabButton>
<IonTabButton tab="map">
<IonLabel>Map</IonLabel>
<IonIcon name="map"></IonIcon>
</IonTabButton>
<IonTabButton tab="about">
<IonLabel>About</IonLabel>
<IonIcon name="about"></IonIcon>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
expect(container.children[0].children.length).toEqual(2);
expect(container.children[0].children[0].tagName).toEqual('DIV');
expect(container.children[0].children[0].className).toEqual('tabs-inner');
expect(container.children[0].children[1].tagName).toEqual('ION-TAB-BAR');
expect(container.children[0].children[1].children.length).toEqual(4);
expect(Array.from(container.children[0].children[1].children).map(c => c.tagName)).toEqual(['ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON']);
});
test('should allow for conditional children', () => {
const { container } = render(
<IonTabs>
<IonRouterOutlet></IonRouterOutlet>
<IonTabBar slot="bottom" currentPath={'/'} navigate={() => {}}>
{false &&
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>
<IonIcon name="schedule"></IonIcon>
</IonTabButton>
}
<IonTabButton tab="speakers">
<IonLabel>Speakers</IonLabel>
<IonIcon name="speakers"></IonIcon>
</IonTabButton>
<IonTabButton tab="map">
<IonLabel>Map</IonLabel>
<IonIcon name="map"></IonIcon>
</IonTabButton>
<IonTabButton tab="about">
<IonLabel>About</IonLabel>
<IonIcon name="about"></IonIcon>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
expect(container.children[0].children.length).toEqual(2);
expect(container.children[0].children[0].tagName).toEqual('DIV');
expect(container.children[0].children[0].className).toEqual('tabs-inner');
expect(container.children[0].children[1].tagName).toEqual('ION-TAB-BAR');
expect(container.children[0].children[1].children.length).toEqual(3);
expect(Array.from(container.children[0].children[1].children).map(c => c.tagName)).toEqual(['ION-TAB-BUTTON', 'ION-TAB-BUTTON', 'ION-TAB-BUTTON']);
});
});

View File

@ -0,0 +1,135 @@
import React from 'react';
import { JSX } from '@ionic/core';
import { createReactComponent } from '../createComponent';
import { render, fireEvent, cleanup, RenderResult } from 'react-testing-library';
import { IonButton } from '../index';
afterEach(cleanup);
describe('createComponent - events', () => {
test('should set events on handler', () => {
const FakeOnClick = jest.fn((e) => e);
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
const { getByText } = render(
<IonButton onClick={FakeOnClick}>
ButtonNameA
</IonButton>
);
fireEvent.click(getByText('ButtonNameA'));
expect(FakeOnClick).toBeCalledTimes(1);
});
test('should add custom events', () => {
const FakeIonFocus = jest.fn((e) => e);
const IonInput = createReactComponent<JSX.IonInput, HTMLIonInputElement>('ion-input');
const { getByText } = render(
<IonInput onIonFocus={FakeIonFocus}>
ButtonNameA
</IonInput>
);
const ionInputItem = getByText('ButtonNameA');
expect(Object.keys((ionInputItem as any).__events)).toEqual(['ionFocus']);
});
});
describe('createComponent - ref', () => {
test('should pass ref on to web component instance', () => {
const ionButtonRef: React.RefObject<any> = React.createRef();
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
const { getByText } = render(
<IonButton ref={ionButtonRef}>
ButtonNameA
</IonButton>
)
const ionButtonItem = getByText('ButtonNameA');
expect(ionButtonRef.current).toEqual(ionButtonItem);
});
});
describe('when working with css classes', () => {
const myClass = 'my-class'
const myClass2 = 'my-class2'
const customClass = 'custom-class';
describe('when a class is added to className', () => {
let renderResult: RenderResult;
let button: HTMLElement;
beforeEach(() => {
renderResult = render(
<IonButton className={myClass}>
Hello!
</IonButton>
);
button = renderResult.getByText(/Hello/);
});
it('then it should be in the class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
});
it('when a class is added to class list outside of React, then that class should still be in class list when rendered again', () => {
button.classList.add(customClass);
expect(button.classList.contains(customClass)).toBeTruthy();
renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);
expect(button.classList.contains(customClass)).toBeTruthy();
});
});
describe('when multiple classes are added to className', () => {
let renderResult: RenderResult;
let button: HTMLElement;
beforeEach(() => {
renderResult = render(
<IonButton className={myClass + ' ' + myClass2}>
Hello!
</IonButton>
);
button = renderResult.getByText(/Hello/);
});
it('then both classes should be in class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();
});
it('when one of the classes is removed, then only the remaining class should be in class list', () => {
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();
renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeFalsy();
});
it('when a custom class is added outside of React and one of the classes is removed, then only the remaining class and the custom class should be in class list', () => {
button.classList.add(customClass);
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeTruthy();
expect(button.classList.contains(customClass)).toBeTruthy();
renderResult.rerender(
<IonButton className={myClass}>
Hello!
</IonButton>
);
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass)).toBeTruthy();
expect(button.classList.contains(myClass2)).toBeFalsy();
});
})
});

View File

@ -0,0 +1,54 @@
import * as utils from '../utils';
import 'jest-dom/extend-expect'
describe('isCoveredByReact', () => {
it('should identify standard events as covered by React', () => {
expect(utils.isCoveredByReact('click', document)).toEqual(true);
});
it('should identify custom events as not covered by React', () => {
expect(utils.isCoveredByReact('change', document)).toEqual(true);
expect(utils.isCoveredByReact('ionchange', document)).toEqual(false);
});
});
describe('syncEvent', () => {
it('should add event on sync and readd on additional syncs', () => {
var div = document.createElement("div");
const addEventListener = jest.spyOn(div, "addEventListener");
const removeEventListener = jest.spyOn(div, "removeEventListener");
const ionClickCallback = jest.fn();
utils.syncEvent(div, 'ionClick', ionClickCallback);
expect(removeEventListener).not.toHaveBeenCalled();
expect(addEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function));
utils.syncEvent(div, 'ionClick', ionClickCallback);
expect(removeEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function));
expect(addEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function));
const event = new CustomEvent('ionClick', { detail: 'test'});
div.dispatchEvent(event);
expect(ionClickCallback).toHaveBeenCalled();
})
});
describe('attachEventProps', () => {
it('should pass props to a dom node', () => {
const onIonClickCallback = () => {};
var div = document.createElement("div");
utils.attachEventProps(div, {
'children': [],
'style': 'color: red',
'ref': () => {},
'onClick': () => {},
'onIonClick': onIonClickCallback,
'testprop': ['red']
});
expect((div as any).testprop).toEqual(['red']);
expect(div).toHaveStyle('');
expect(Object.keys((div as any).__events)).toEqual(['ionClick']);
});
});

View File

@ -0,0 +1,101 @@
import { RouterDirection } from '@ionic/core';
import React from 'react';
import ReactDom from 'react-dom';
import { NavContext } from '../contexts/NavContext';
import { ReactProps } from './ReactProps';
import { attachEventProps, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
interface IonicReactInternalProps<ElementType> {
forwardedRef?: React.Ref<ElementType>;
children?: React.ReactNode;
href?: string;
target?: string;
style?: string;
ref?: React.Ref<any>;
routerDirection?: RouterDirection;
className?: string;
}
export const createReactComponent = <PropType, ElementType>(
tagName: string,
hrefComponent = false
) => {
const displayName = dashToPascalCase(tagName);
const ReactComponent = class extends React.Component<IonicReactInternalProps<ElementType>> {
context!: React.ContextType<typeof NavContext>;
constructor(props: IonicReactInternalProps<ElementType>) {
super(props);
}
componentDidMount() {
this.componentDidUpdate(this.props);
}
componentDidUpdate(prevProps: IonicReactInternalProps<ElementType>) {
const node = ReactDom.findDOMNode(this) as HTMLElement;
attachEventProps(node, this.props, prevProps);
}
private handleClick = (e: MouseEvent) => {
// TODO: review target usage
const { href, routerDirection } = this.props;
if (href !== undefined && this.context.hasIonicRouter()) {
e.preventDefault();
this.context.navigate(href, routerDirection);
}
}
render() {
const { children, forwardedRef, style, className, ref, ...cProps } = this.props;
const propsToPass = Object.keys(cProps).reduce((acc, name) => {
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
const eventName = name.substring(2).toLowerCase();
if (isCoveredByReact(eventName)) {
(acc as any)[name] = (cProps as any)[name];
}
}
return acc;
}, {});
const newProps: any = {
...propsToPass,
ref: forwardedRef,
style,
className
};
if (hrefComponent) {
if (newProps.onClick) {
const oldClick = newProps.onClick;
newProps.onClick = (e: MouseEvent) => {
oldClick(e);
if (!e.defaultPrevented) {
this.handleClick(e);
}
};
} else {
newProps.onClick = this.handleClick;
}
}
return React.createElement(
tagName,
newProps,
children
);
}
static get displayName() {
return displayName;
}
static get contextType() {
return NavContext;
}
};
return createForwardRef<PropType & ReactProps, ElementType>(ReactComponent, displayName);
};

View File

@ -0,0 +1,67 @@
import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import { attachEventProps } from './utils';
interface OverlayBase extends HTMLElement {
present: () => Promise<void>;
dismiss: (data?: any, role?: string | undefined) => Promise<boolean>;
}
export interface ReactControllerProps {
isOpen: boolean;
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
}
export const createControllerComponent = <OptionsType extends object, OverlayType extends OverlayBase>(
displayName: string,
controller: { create: (options: OptionsType) => Promise<OverlayType> }
) => {
const dismissEventName = `on${displayName}DidDismiss`;
type Props = OptionsType & ReactControllerProps;
return class extends React.Component<Props> {
overlay?: OverlayType;
constructor(props: Props) {
super(props);
}
static get displayName() {
return displayName;
}
async componentDidMount() {
const { isOpen } = this.props;
// TODO
if (isOpen as boolean) {
this.present();
}
}
async componentDidUpdate(prevProps: Props) {
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
this.present(prevProps);
}
if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) {
await this.overlay.dismiss();
}
}
async present(prevProps?: Props) {
const { isOpen, onDidDismiss, ...cProps } = this.props;
const overlay = this.overlay = await controller.create({
...cProps as any
});
attachEventProps(overlay, {
[dismissEventName]: onDidDismiss
}, prevProps);
await overlay.present();
}
render(): null {
return null;
}
};
};

View File

@ -0,0 +1,81 @@
import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import ReactDOM from 'react-dom';
import { attachEventProps } from './utils';
interface OverlayElement extends HTMLElement {
present: () => Promise<void>;
dismiss: (data?: any, role?: string | undefined) => Promise<boolean>;
}
export interface ReactOverlayProps {
children?: React.ReactNode;
isOpen: boolean;
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
}
export const createOverlayComponent = <T extends object, OverlayType extends OverlayElement>(
displayName: string,
controller: { create: (options: any) => Promise<OverlayType> }
) => {
const dismissEventName = `on${displayName}DidDismiss`;
type Props = T & ReactOverlayProps;
return class extends React.Component<Props> {
overlay?: OverlayType;
el: HTMLDivElement;
constructor(props: Props) {
super(props);
this.el = document.createElement('div');
}
static get displayName() {
return displayName;
}
componentDidMount() {
// TODO
if (this.props.isOpen as boolean) {
this.present();
}
}
async componentDidUpdate(prevProps: Props) {
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
this.present(prevProps);
}
if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) {
await this.overlay.dismiss();
}
}
async present(prevProps?: Props) {
const { children, isOpen, onDidDismiss = () => { return; }, ...cProps } = this.props;
const elementProps = {
...cProps,
[dismissEventName]: onDidDismiss
};
const overlay = this.overlay = await controller.create({
...elementProps,
component: this.el,
componentProps: {}
});
attachEventProps(overlay, elementProps, prevProps);
await overlay.present();
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
};
};

View File

@ -0,0 +1,46 @@
import { defineCustomElements } from '@ionic/core/loader';
import { addIcons } from 'ionicons';
import { arrowBack, arrowDown, arrowForward, close, closeCircle, menu, reorder, search } from 'ionicons/icons';
export { AlertButton, AlertInput, setupConfig } from '@ionic/core';
export * from './proxies';
// createControllerComponent
export { IonAlert } from './IonAlert';
export { IonLoading } from './IonLoading';
export { IonToast } from './IonToast';
// createOverlayComponent
export { IonActionSheet } from './IonActionSheet';
export { IonModal } from './IonModal';
export { IonPopover } from './IonPopover';
// Custom Components
export { IonPage } from './IonPage';
export { IonTabs } from './navigation/IonTabs';
export { IonTabBar } from './navigation/IonTabBar';
export { IonBackButton } from './navigation/IonBackButton';
// Icons that are used by internal components
addIcons({
'ios-close': close.ios,
'md-close': close.md,
'ios-reorder': reorder.ios,
'md-reorder': reorder.md,
'ios-menu': menu.ios,
'md-menu': menu.md,
'ios-arrow-forward': arrowForward.ios,
'md-arrow-forward': arrowForward.md,
'ios-arrow-back': arrowBack.ios,
'md-arrow-back': arrowBack.md,
'ios-arrow-down': arrowDown.ios,
'md-arrow-down': arrowDown.md,
'ios-search': search.ios,
'md-search': search.md,
'ios-close-circle': closeCircle.ios,
'md-close-circle': closeCircle.md,
});
// TODO: defineCustomElements() is asyncronous
// We need to use the promise
defineCustomElements(window);

View File

@ -0,0 +1,6 @@
import { JSX } from '@ionic/core';
import { /*@__PURE__*/ createReactComponent } from './createComponent';
export const IonTabBarInner = /*@__PURE__*/createReactComponent<JSX.IonTabBar, HTMLIonTabBarElement>('ion-tab-bar');
export const IonBackButtonInner = /*@__PURE__*/createReactComponent<JSX.IonBackButton, HTMLIonBackButtonElement>('ion-back-button');

View File

@ -0,0 +1,39 @@
import { JSX as LocalJSX } from '@ionic/core';
import React from 'react';
import { NavContext } from '../../contexts/NavContext';
import { IonBackButtonInner } from '../inner-proxies';
type Props = LocalJSX.IonBackButton & {
ref?: React.RefObject<HTMLIonBackButtonElement>;
};
export const IonBackButton = /*@__PURE__*/(() => class extends React.Component<Props> {
context!: React.ContextType<typeof NavContext>;
clickButton = (e: MouseEvent) => {
const defaultHref = this.props.defaultHref;
if (defaultHref !== undefined) {
if (this.context.hasIonicRouter()) {
e.stopPropagation();
this.context.goBack(defaultHref);
} else {
window.location.href = defaultHref;
}
}
}
render() {
return (
<IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>
);
}
static get displayName() {
return 'IonBackButton';
}
static get contextType() {
return NavContext;
}
})();

View File

@ -0,0 +1,111 @@
import { JSX as LocalJSX } from '@ionic/core';
import React, { useContext } from 'react';
import { NavContext } from '../../contexts/NavContext';
import { IonTabBarInner } from '../inner-proxies';
import { IonTabButton } from '../proxies';
type Props = LocalJSX.IonTabBar & {
ref?: React.RefObject<HTMLIonTabBarElement>;
navigate: (path: string) => void;
currentPath: string;
children?: React.ReactNode;
};
interface Tab {
originalHref: string;
currentHref: string;
}
interface State {
activeTab: string | undefined;
tabs: { [key: string]: Tab };
}
const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const tabActiveUrls: { [key: string]: Tab } = {};
React.Children.forEach(this.props.children, (child: any) => {
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
tabActiveUrls[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href
};
}
});
this.state = {
activeTab: undefined,
tabs: tabActiveUrls
};
}
static getDerivedStateFromProps(props: Props, state: State) {
const activeTab = Object.keys(state.tabs)
.find(key => {
const href = state.tabs[key].originalHref;
return props.currentPath.startsWith(href);
});
if (activeTab === undefined || (activeTab === state.activeTab && state.tabs[activeTab].currentHref === props.currentPath)) {
return null;
}
return {
activeTab,
tabs: {
...state.tabs,
[activeTab]: {
originalHref: state.tabs[activeTab].originalHref,
currentHref: props.currentPath
}
}
};
}
private onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => {
const targetUrl = (this.state.activeTab === e.detail.tab) ?
this.state.tabs[e.detail.tab].originalHref :
this.state.tabs[e.detail.tab].currentHref;
this.props.navigate(targetUrl);
}
private renderChild = (activeTab: string | null | undefined) => (child: (React.ReactElement<LocalJSX.IonTabButton & { onIonTabButtonClick: (e: CustomEvent) => void }>) | null | undefined) => {
if (child != null && child.props && child.type === IonTabButton) {
const href = (child.props.tab === activeTab) ? this.props.currentPath : (this.state.tabs[child.props.tab!].currentHref);
return React.cloneElement(child, {
href,
onIonTabButtonClick: this.onTabButtonClick
});
}
return null;
}
render() {
return (
<IonTabBarInner {...this.props} selectedTab={this.state.activeTab}>
{React.Children.map(this.props.children as any, this.renderChild(this.state.activeTab))}
</IonTabBarInner>
);
}
})();
export const IonTabBar: React.FC<LocalJSX.IonTabBar & { currentPath?: string, navigate?: (path: string) => void; }> = props => {
const context = useContext(NavContext);
return (
<IonTabBarUnwrapped
{...props as any}
navigate={props.navigate || ((path: string) => {
context.navigate(path);
})}
currentPath={props.currentPath || context.currentPath}
>
{props.children}
</IonTabBarUnwrapped>
);
};

View File

@ -0,0 +1,89 @@
import React from 'react';
import { NavContext } from '../../contexts/NavContext';
import { IonRouterOutlet } from '../proxies';
import { IonTabBar } from './IonTabBar';
interface 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 const IonTabs = /*@__PURE__*/(() => class extends React.Component<Props> {
context!: React.ContextType<typeof NavContext>;
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
constructor(props: Props) {
super(props);
}
render() {
let outlet: React.ReactElement<{}> | undefined;
let tabBar: React.ReactElement<{ slot: 'bottom' | 'top' }> | undefined;
React.Children.forEach(this.props.children, (child: any) => {
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
return;
}
if (child.type === IonRouterOutlet) {
outlet = child;
}
if (child.type === IonTabBar) {
tabBar = child;
}
});
if (!outlet) {
throw new Error('IonTabs must contain an IonRouterOutlet');
}
if (!tabBar) {
// TODO, this is not required
throw new Error('IonTabs needs a IonTabBar');
}
const NavManager = this.context.getViewManager();
return (
<div style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{this.context.hasIonicRouter() ? (
<NavManager>
{outlet}
</NavManager>
) : (
<>{outlet}</>
)}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</div>
);
}
static get displayName() {
return 'IonTabs';
}
static get contextType() {
return NavContext;
}
})();

View File

@ -0,0 +1,83 @@
import { JSX } from '@ionic/core';
import { JSX as IoniconsJSX } from 'ionicons';
import { createReactComponent } from './createComponent';
// ionicons
export const IonIcon = /*@__PURE__*/createReactComponent<IoniconsJSX.IonIcon, HTMLIonIconElement>('ion-icon');
// ionic/core
export const IonApp = /*@__PURE__*/createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app');
export const IonTab = /*@__PURE__*/createReactComponent<JSX.IonTab, HTMLIonTabElement>('ion-tab');
export const IonTabButton = /*@__PURE__*/createReactComponent<JSX.IonTabButton, HTMLIonTabButtonElement>('ion-tab-button');
export const IonRouterLink = /*@__PURE__*/createReactComponent<JSX.IonRouterLink, HTMLIonRouterLinkElement>('ion-router-link', true);
export const IonAvatar = /*@__PURE__*/createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>('ion-avatar');
export const IonBackdrop = /*@__PURE__*/createReactComponent<JSX.IonBackdrop, HTMLIonBackdropElement>('ion-backdrop');
export const IonBadge = /*@__PURE__*/createReactComponent<JSX.IonBadge, HTMLIonBadgeElement>('ion-badge');
export const IonButton = /*@__PURE__*/createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button', true);
export const IonButtons = /*@__PURE__*/createReactComponent<JSX.IonButtons, HTMLIonButtonsElement>('ion-buttons');
export const IonCard = /*@__PURE__*/createReactComponent<JSX.IonCard, HTMLIonCardElement>('ion-card', true);
export const IonCardContent = /*@__PURE__*/createReactComponent<JSX.IonCardContent, HTMLIonCardContentElement>('ion-card-content');
export const IonCardHeader = /*@__PURE__*/createReactComponent<JSX.IonCardHeader, HTMLIonCardHeaderElement>('ion-card-header');
export const IonCardSubtitle = /*@__PURE__*/createReactComponent<JSX.IonCardSubtitle, HTMLIonCardSubtitleElement>('ion-card-subtitle');
export const IonCardTitle = /*@__PURE__*/createReactComponent<JSX.IonCardTitle, HTMLIonCardTitleElement>('ion-card-title');
export const IonCheckbox = /*@__PURE__*/createReactComponent<JSX.IonCheckbox, HTMLIonCheckboxElement>('ion-checkbox');
export const IonCol = /*@__PURE__*/createReactComponent<JSX.IonCol, HTMLIonColElement>('ion-col');
export const IonContent = /*@__PURE__*/createReactComponent<JSX.IonContent, HTMLIonContentElement>('ion-content');
export const IonChip = /*@__PURE__*/createReactComponent<JSX.IonChip, HTMLIonChipElement>('ion-chip');
export const IonDatetime = /*@__PURE__*/createReactComponent<JSX.IonDatetime, HTMLIonDatetimeElement>('ion-datetime');
export const IonFab = /*@__PURE__*/createReactComponent<JSX.IonFab, HTMLIonFabElement>('ion-fab');
export const IonFabButton = /*@__PURE__*/createReactComponent<JSX.IonFabButton, HTMLIonFabButtonElement>('ion-fab-button', true);
export const IonFabList = /*@__PURE__*/createReactComponent<JSX.IonFabList, HTMLIonFabListElement>('ion-fab-list');
export const IonFooter = /*@__PURE__*/createReactComponent<JSX.IonFooter, HTMLIonFooterElement>('ion-footer');
export const IonGrid = /*@__PURE__*/createReactComponent<JSX.IonGrid, HTMLIonGridElement>('ion-grid');
export const IonHeader = /*@__PURE__*/createReactComponent<JSX.IonHeader, HTMLIonHeaderElement>('ion-header');
export const IonImg = /*@__PURE__*/createReactComponent<JSX.IonImg, HTMLIonImgElement>('ion-img');
export const IonInfiniteScroll = /*@__PURE__*/createReactComponent<JSX.IonInfiniteScroll, HTMLIonInfiniteScrollElement>('ion-infinite-scroll');
export const IonInfiniteScrollContent = /*@__PURE__*/createReactComponent<JSX.IonInfiniteScrollContent, HTMLIonInfiniteScrollContentElement>('ion-infinite-scroll-content');
export const IonInput = /*@__PURE__*/createReactComponent<JSX.IonInput, HTMLIonInputElement>('ion-input');
export const IonItem = /*@__PURE__*/createReactComponent<JSX.IonItem, HTMLIonItemElement>('ion-item', true);
export const IonItemDivider = /*@__PURE__*/createReactComponent<JSX.IonItemDivider, HTMLIonItemDividerElement>('ion-item-divider');
export const IonItemGroup = /*@__PURE__*/createReactComponent<JSX.IonItemGroup, HTMLIonItemGroupElement>('ion-item-group');
export const IonItemOption = /*@__PURE__*/createReactComponent<JSX.IonItemOption, HTMLIonItemOptionElement>('ion-item-option', true);
export const IonItemOptions = /*@__PURE__*/createReactComponent<JSX.IonItemOptions, HTMLIonItemOptionsElement>('ion-item-options');
export const IonItemSliding = /*@__PURE__*/createReactComponent<JSX.IonItemSliding, HTMLIonItemSlidingElement>('ion-item-sliding');
export const IonLabel = /*@__PURE__*/createReactComponent<JSX.IonLabel, HTMLIonLabelElement>('ion-label');
export const IonList = /*@__PURE__*/createReactComponent<JSX.IonList, HTMLIonListElement>('ion-list');
export const IonListHeader = /*@__PURE__*/createReactComponent<JSX.IonListHeader, HTMLIonListHeaderElement>('ion-list-header');
export const IonMenu = /*@__PURE__*/createReactComponent<JSX.IonMenu, HTMLIonMenuElement>('ion-menu');
export const IonMenuButton = /*@__PURE__*/createReactComponent<JSX.IonMenuButton, HTMLIonMenuButtonElement>('ion-menu-button');
export const IonMenuToggle = /*@__PURE__*/createReactComponent<JSX.IonMenuToggle, HTMLIonMenuToggleElement>('ion-menu-toggle');
export const IonNote = /*@__PURE__*/createReactComponent<JSX.IonNote, HTMLIonNoteElement>('ion-note');
export const IonPicker = /*@__PURE__*/createReactComponent<JSX.IonPicker, HTMLIonPickerElement>('ion-picker');
export const IonPickerColumn = /*@__PURE__*/createReactComponent<JSX.IonPickerColumn, HTMLIonPickerColumnElement>('ion-picker-column');
export const IonNav = /*@__PURE__*/createReactComponent<JSX.IonNav, HTMLIonNavElement>('ion-nav');
export const IonProgressBar = /*@__PURE__*/createReactComponent<JSX.IonProgressBar, HTMLIonProgressBarElement>('ion-progress-bar');
export const IonRadio = /*@__PURE__*/createReactComponent<JSX.IonRadio, HTMLIonRadioElement>('ion-radio');
export const IonRadioGroup = /*@__PURE__*/createReactComponent<JSX.IonRadioGroup, HTMLIonRadioGroupElement>('ion-radio-group');
export const IonRange = /*@__PURE__*/createReactComponent<JSX.IonRange, HTMLIonRangeElement>('ion-range');
export const IonRefresher = /*@__PURE__*/createReactComponent<JSX.IonRefresher, HTMLIonRefresherElement>('ion-refresher');
export const IonRefresherContent = /*@__PURE__*/createReactComponent<JSX.IonRefresherContent, HTMLIonRefresherContentElement>('ion-refresher-content');
export const IonReorder = /*@__PURE__*/createReactComponent<JSX.IonReorder, HTMLIonReorderElement>('ion-reorder');
export const IonReorderGroup = /*@__PURE__*/createReactComponent<JSX.IonReorderGroup, HTMLIonReorderGroupElement>('ion-reorder-group');
export const IonRippleEffect = /*@__PURE__*/createReactComponent<JSX.IonRippleEffect, HTMLIonRippleEffectElement>('ion-ripple-effect');
export const IonRouterOutlet = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet, HTMLIonRouterOutletElement>('ion-router-outlet');
export const IonRow = /*@__PURE__*/createReactComponent<JSX.IonRow, HTMLIonRowElement>('ion-row');
export const IonSearchbar = /*@__PURE__*/createReactComponent<JSX.IonSearchbar, HTMLIonSearchbarElement>('ion-searchbar');
export const IonSegment = /*@__PURE__*/createReactComponent<JSX.IonSegment, HTMLIonSegmentElement>('ion-segment');
export const IonSegmentButton = /*@__PURE__*/createReactComponent<JSX.IonSegmentButton, HTMLIonSegmentButtonElement>('ion-segment-button');
export const IonSelect = /*@__PURE__*/createReactComponent<JSX.IonSelect, HTMLIonSelectElement>('ion-select');
export const IonSelectOption = /*@__PURE__*/createReactComponent<JSX.IonSelectOption, HTMLIonSelectOptionElement>('ion-select-option');
export const IonSelectPopover = /*@__PURE__*/createReactComponent<JSX.IonSelectPopover, HTMLIonSelectPopoverElement>('ion-select-popover');
export const IonSkeletonText = /*@__PURE__*/createReactComponent<JSX.IonSkeletonText, HTMLIonSkeletonTextElement>('ion-skeleton-text');
export const IonSlide = /*@__PURE__*/createReactComponent<JSX.IonSlide, HTMLIonSlideElement>('ion-slide');
export const IonSlides = /*@__PURE__*/createReactComponent<JSX.IonSlides, HTMLIonSlidesElement>('ion-slides');
export const IonSpinner = /*@__PURE__*/createReactComponent<JSX.IonSpinner, HTMLIonSpinnerElement>('ion-spinner');
export const IonSplitPane = /*@__PURE__*/createReactComponent<JSX.IonSplitPane, HTMLIonSplitPaneElement>('ion-split-pane');
export const IonText = /*@__PURE__*/createReactComponent<JSX.IonText, HTMLIonTextElement>('ion-text');
export const IonTextarea = /*@__PURE__*/createReactComponent<JSX.IonTextarea, HTMLIonTextareaElement>('ion-textarea');
export const IonThumbnail = /*@__PURE__*/createReactComponent<JSX.IonThumbnail, HTMLIonThumbnailElement>('ion-thumbnail');
export const IonTitle = /*@__PURE__*/createReactComponent<JSX.IonTitle, HTMLIonTitleElement>('ion-title');
export const IonToggle = /*@__PURE__*/createReactComponent<JSX.IonToggle, HTMLIonToggleElement>('ion-toggle');
export const IonToolbar = /*@__PURE__*/createReactComponent<JSX.IonToolbar, HTMLIonToolbarElement>('ion-toolbar');
export const IonVirtualScroll = /*@__PURE__*/createReactComponent<JSX.IonVirtualScroll, HTMLIonVirtualScrollElement>('ion-virtual-scroll');

View File

@ -0,0 +1,83 @@
export const attachEventProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => {
// add any classes in className to the class list
const className = getClassName(node.classList, newProps, oldProps);
if (className !== '') {
node.className = className;
}
Object.keys(newProps).forEach(name => {
if (name === 'children' || name === 'style' || name === 'ref' || name === 'className') {
return;
}
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
const eventName = name.substring(2);
const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1);
if (!isCoveredByReact(eventNameLc)) {
syncEvent(node, eventNameLc, newProps[name]);
}
} else {
(node as any)[name] = newProps[name];
}
});
};
export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => {
// map the classes to Maps for performance
const currentClasses = arrayToMap(classList);
const incomingPropClasses = arrayToMap(newProps.className ? newProps.className.split(' ') : []);
const oldPropClasses = arrayToMap(oldProps.className ? oldProps.className.split(' ') : []);
const finalClassNames: string[] = [];
// loop through each of the current classes on the component
// to see if it should be a part of the classNames added
currentClasses.forEach(currentClass => {
if (incomingPropClasses.has(currentClass)) {
// add it as its already included in classnames coming in from newProps
finalClassNames.push(currentClass);
incomingPropClasses.delete(currentClass);
} else if (!oldPropClasses.has(currentClass)) {
// add it as it has NOT been removed by user
finalClassNames.push(currentClass);
}
});
incomingPropClasses.forEach(s => finalClassNames.push(s));
return finalClassNames.join(' ');
};
/**
* Checks if an event is supported in the current execution environment.
* @license Modernizr 3.0.0pre (Custom Build) | MIT
*/
export const isCoveredByReact = (eventNameSuffix: string, doc: Document = document) => {
const eventName = 'on' + eventNameSuffix;
let isSupported = eventName in doc;
if (!isSupported) {
const element = doc.createElement('div');
element.setAttribute(eventName, 'return;');
isSupported = typeof (element as any)[eventName] === 'function';
}
return isSupported;
};
export const syncEvent = (node: Element, eventName: string, newEventHandler: (e: Event) => any) => {
const eventStore = (node as any).__events || ((node as any).__events = {});
const oldEventHandler = eventStore[eventName];
// Remove old listener so they don't double up.
if (oldEventHandler) {
node.removeEventListener(eventName, oldEventHandler);
}
// Bind new listener.
node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) {
if (newEventHandler) { newEventHandler.call(this, e); }
});
};
const arrayToMap = (arr: string[] | DOMTokenList) => {
const map = new Map<string, string>();
(arr as string[]).forEach((s: string) => map.set(s, s));
return map;
};

View File

@ -0,0 +1,27 @@
import { Platforms, getPlatforms as getPlatformsCore, isPlatform as isPlatformCore } from '@ionic/core';
import React from 'react';
export const dashToPascalCase = (str: string) => str.toLowerCase().split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join('');
export type IonicReactExternalProps<PropType, ElementType> = PropType & {
ref?: React.RefObject<ElementType>;
children?: React.ReactNode;
};
export const createForwardRef = <PropType, ElementType>(ReactComponent: any, displayName: string) => {
const forwardRef = (props: IonicReactExternalProps<PropType, ElementType>, ref: React.Ref<ElementType>) => {
return <ReactComponent {...props} forwardedRef={ref} />;
};
forwardRef.displayName = displayName;
return React.forwardRef(forwardRef);
};
export * from './attachEventProps';
export const isPlatform = (platform: Platforms) => {
return isPlatformCore(window, platform);
};
export const getPlatforms = () => {
return getPlatformsCore(window);
};