mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
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:
7
packages/react/src/components/IonActionSheet.tsx
Normal file
7
packages/react/src/components/IonActionSheet.tsx
Normal 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);
|
5
packages/react/src/components/IonAlert.tsx
Normal file
5
packages/react/src/components/IonAlert.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { AlertOptions, alertController } from '@ionic/core';
|
||||
|
||||
import { createControllerComponent } from './createControllerComponent';
|
||||
|
||||
export const IonAlert = /*@__PURE__*/createControllerComponent<AlertOptions, HTMLIonAlertElement>('IonAlert', alertController);
|
5
packages/react/src/components/IonLoading.tsx
Normal file
5
packages/react/src/components/IonLoading.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { LoadingOptions, loadingController } from '@ionic/core';
|
||||
|
||||
import { createControllerComponent } from './createControllerComponent';
|
||||
|
||||
export const IonLoading = /*@__PURE__*/createControllerComponent<LoadingOptions, HTMLIonLoadingElement>('IonLoading', loadingController);
|
9
packages/react/src/components/IonModal.tsx
Normal file
9
packages/react/src/components/IonModal.tsx
Normal 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);
|
25
packages/react/src/components/IonPage.tsx
Normal file
25
packages/react/src/components/IonPage.tsx
Normal 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');
|
9
packages/react/src/components/IonPopover.tsx
Normal file
9
packages/react/src/components/IonPopover.tsx
Normal 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);
|
5
packages/react/src/components/IonToast.tsx
Normal file
5
packages/react/src/components/IonToast.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { ToastOptions, toastController } from '@ionic/core';
|
||||
|
||||
import { createControllerComponent } from './createControllerComponent';
|
||||
|
||||
export const IonToast = /*@__PURE__*/createControllerComponent<ToastOptions, HTMLIonToastElement>('IonToast', toastController);
|
4
packages/react/src/components/ReactProps.ts
Normal file
4
packages/react/src/components/ReactProps.ts
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
export interface ReactProps {
|
||||
className?: string;
|
||||
}
|
28
packages/react/src/components/__tests__/IonButton.spec.tsx
Normal file
28
packages/react/src/components/__tests__/IonButton.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
78
packages/react/src/components/__tests__/IonTabs.spec.tsx
Normal file
78
packages/react/src/components/__tests__/IonTabs.spec.tsx
Normal 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']);
|
||||
});
|
||||
});
|
135
packages/react/src/components/__tests__/createComponent.spec.tsx
Normal file
135
packages/react/src/components/__tests__/createComponent.spec.tsx
Normal 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();
|
||||
});
|
||||
})
|
||||
});
|
54
packages/react/src/components/__tests__/utils.spec.ts
Normal file
54
packages/react/src/components/__tests__/utils.spec.ts
Normal 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']);
|
||||
});
|
||||
|
||||
});
|
101
packages/react/src/components/createComponent.tsx
Normal file
101
packages/react/src/components/createComponent.tsx
Normal 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);
|
||||
};
|
67
packages/react/src/components/createControllerComponent.tsx
Normal file
67
packages/react/src/components/createControllerComponent.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
};
|
81
packages/react/src/components/createOverlayComponent.tsx
Normal file
81
packages/react/src/components/createOverlayComponent.tsx
Normal 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,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
46
packages/react/src/components/index.ts
Normal file
46
packages/react/src/components/index.ts
Normal 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);
|
6
packages/react/src/components/inner-proxies.ts
Normal file
6
packages/react/src/components/inner-proxies.ts
Normal 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');
|
39
packages/react/src/components/navigation/IonBackButton.tsx
Normal file
39
packages/react/src/components/navigation/IonBackButton.tsx
Normal 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;
|
||||
}
|
||||
})();
|
111
packages/react/src/components/navigation/IonTabBar.tsx
Normal file
111
packages/react/src/components/navigation/IonTabBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
89
packages/react/src/components/navigation/IonTabs.tsx
Normal file
89
packages/react/src/components/navigation/IonTabs.tsx
Normal 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;
|
||||
}
|
||||
})();
|
83
packages/react/src/components/proxies.ts
Normal file
83
packages/react/src/components/proxies.ts
Normal 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');
|
83
packages/react/src/components/utils/attachEventProps.ts
Normal file
83
packages/react/src/components/utils/attachEventProps.ts
Normal 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;
|
||||
};
|
27
packages/react/src/components/utils/index.tsx
Normal file
27
packages/react/src/components/utils/index.tsx
Normal 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);
|
||||
};
|
Reference in New Issue
Block a user