feat(react): React Router Enhancements (#21693)

This commit is contained in:
Ely Lucas
2020-07-07 11:02:05 -06:00
committed by GitHub
parent a0735b97bf
commit c171ccbd37
245 changed files with 26872 additions and 1126 deletions

View File

@ -1,6 +1,7 @@
import React from 'react';
import { NavContext } from '../contexts/NavContext';
import PageManager from '../routing/PageManager';
import { IonicReactProps } from './IonicReactProps';
import { createForwardRef } from './utils';
@ -14,28 +15,29 @@ interface IonPageInternalProps extends IonPageProps {
class IonPageInternal extends React.Component<IonPageInternalProps> {
context!: React.ContextType<typeof NavContext>;
ref: React.RefObject<HTMLDivElement>;
constructor(props: IonPageInternalProps) {
super(props);
this.ref = this.props.forwardedRef || React.createRef();
}
componentDidMount() {
if (this.context && this.ref && this.ref.current) {
if (this.context.hasIonicRouter()) {
this.context.registerIonPage(this.ref.current);
}
}
}
render() {
const { className, children, forwardedRef, ...props } = this.props;
return (
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
{children}
</div>
this.context.hasIonicRouter() ? (
<PageManager
className={className ? `${className}` : ''}
routeInfo={this.context.routeInfo}
forwardedRef={forwardedRef}
{...props}
>
{children}
</PageManager>
) : (
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={forwardedRef} {...props}>
{children}
</div>
)
);
}

View File

@ -0,0 +1,38 @@
import React from 'react';
import { NavContext } from '../contexts/NavContext';
export interface IonRedirectProps {
path?: string;
exact?: boolean;
to: string;
routerOptions?: unknown;
}
interface IonRedirectState {
}
export class IonRedirect extends React.PureComponent<IonRedirectProps, IonRedirectState> {
context!: React.ContextType<typeof NavContext>;
render() {
const IonRedirectInner = this.context.getIonRedirect();
if (!this.context.hasIonicRouter() || !IonRedirect) {
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRedirect>');
return null;
}
return (
<IonRedirectInner {...this.props} />
);
}
static get contextType() {
return NavContext;
}
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import { NavContext } from '../contexts/NavContext';
export interface IonRouteProps {
path?: string;
exact?: boolean;
show?: boolean;
render: (props?: any) => JSX.Element;
disableIonPageManagement?: boolean;
}
interface IonRouteState {
}
export class IonRoute extends React.PureComponent<IonRouteProps, IonRouteState> {
context!: React.ContextType<typeof NavContext>;
render() {
const IonRouteInner = this.context.getIonRoute();
if (!this.context.hasIonicRouter() || !IonRoute) {
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRoute>');
return null;
}
return (
<IonRouteInner {...this.props} />
);
}
static get contextType() {
return NavContext;
}
}

View File

@ -0,0 +1,24 @@
import { AnimationBuilder } from '@ionic/core';
import React, { useContext } from 'react';
import { RouteAction, RouterDirection, RouterOptions } from '../models';
import { RouteInfo } from '../models/RouteInfo';
export interface IonRouterContextState {
routeInfo: RouteInfo;
push: (pathname: string, routerDirection?: RouterDirection, routeAction?: RouteAction, routerOptions?: RouterOptions, animationBuilder?: AnimationBuilder) => void;
back: (animationBuilder?: AnimationBuilder) => void;
canGoBack: () => boolean;
}
export const IonRouterContext = React.createContext<IonRouterContextState>({
routeInfo: undefined as any,
push: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
back: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
canGoBack: () => { throw new Error('An Ionic Router is required for IonRouterContext'); }
});
export function useIonRouter() {
const context = useContext(IonRouterContext);
return context;
}

View File

@ -3,35 +3,56 @@ import { JSX as LocalJSX } from '@ionic/core';
import React from 'react';
import { NavContext } from '../contexts/NavContext';
import OutletPageManager from '../routing/OutletPageManager';
import { IonicReactProps } from './IonicReactProps';
import { IonRouterOutletInner } from './inner-proxies';
import { createForwardRef } from './utils';
type Props = LocalJSX.IonRouterOutlet & {
basePath?: string;
ref?: React.RefObject<any>;
ionPage?: boolean;
};
type InternalProps = Props & {
interface InternalProps extends Props {
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
};
}
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {
interface InternalState {
}
class IonRouterOutletContainer extends React.Component<InternalProps, InternalState> {
context!: React.ContextType<typeof NavContext>;
constructor(props: InternalProps) {
super(props);
}
render() {
const StackManager = this.context.getStackManager();
const { children, forwardedRef, ...props } = this.props;
return (
this.context.hasIonicRouter() ? (
<StackManager>
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
{this.props.children}
</IonRouterOutletInner>
</StackManager>
props.ionPage ? (
<OutletPageManager
StackManager={StackManager}
routeInfo={this.context.routeInfo}
{...props}
>
{children}
</OutletPageManager>
) : (
<StackManager routeInfo={this.context.routeInfo}>
<IonRouterOutletInner {...props}>
{children}
</IonRouterOutletInner>
</StackManager>
)
) : (
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
<IonRouterOutletInner ref={forwardedRef} {...this.props}>
{this.props.children}
</IonRouterOutletInner>
)
@ -41,6 +62,6 @@ const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Compone
static get contextType() {
return NavContext;
}
})();
}
export const IonRouterOutlet = createForwardRef<Props & IonicReactProps, HTMLIonRouterOutletElement>(IonRouterOutletContainer, 'IonRouterOutlet');

View File

@ -10,7 +10,7 @@ describe('IonTabs', () => {
const { container } = render(
<IonTabs>
<IonRouterOutlet></IonRouterOutlet>
<IonTabBar slot="bottom" currentPath={'/'}>
<IonTabBar slot="bottom">
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>
<IonIcon name="schedule"></IonIcon>
@ -44,7 +44,7 @@ describe('IonTabs', () => {
const { container } = render(
<IonTabs>
<IonRouterOutlet></IonRouterOutlet>
<IonTabBar slot="bottom" currentPath={'/'}>
<IonTabBar slot="bottom">
{false &&
<IonTabButton tab="schedule">
<IonLabel>Schedule</IonLabel>

View File

@ -3,11 +3,11 @@ import '@testing-library/jest-dom/extend-expect';
describe('isCoveredByReact', () => {
it('should identify standard events as covered by React', () => {
expect(utils.isCoveredByReact('click', document)).toEqual(true);
expect(utils.isCoveredByReact('click')).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);
expect(utils.isCoveredByReact('change')).toEqual(true);
expect(utils.isCoveredByReact('ionchange')).toEqual(false);
});
});

View File

@ -1,10 +1,12 @@
import { AnimationBuilder } from '@ionic/core';
import React from 'react';
import ReactDom from 'react-dom';
import { NavContext } from '../contexts/NavContext';
import { RouterOptions } from '../models';
import { RouterDirection } from '../models/RouterDirection';
import { RouterDirection } from './hrefprops';
import { attachProps, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
import { attachProps, camelToDashCase, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
forwardedRef?: React.Ref<ElementType>;
@ -12,6 +14,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
routerLink?: string;
ref?: React.Ref<any>;
routerDirection?: RouterDirection;
routerOptions?: RouterOptions;
routerAnimation?: AnimationBuilder;
}
export const createReactComponent = <PropType, ElementType>(
@ -36,10 +40,10 @@ export const createReactComponent = <PropType, ElementType>(
}
private handleClick = (e: React.MouseEvent<PropType>) => {
const { routerLink, routerDirection } = this.props;
const { routerLink, routerDirection, routerOptions, routerAnimation } = this.props;
if (routerLink !== undefined) {
e.preventDefault();
this.context.navigate(routerLink, routerDirection);
this.context.navigate(routerLink, routerDirection, undefined, routerAnimation, routerOptions);
}
}
@ -52,6 +56,8 @@ export const createReactComponent = <PropType, ElementType>(
if (isCoveredByReact(eventName)) {
(acc as any)[name] = (cProps as any)[name];
}
} else if (typeof (cProps as any)[name] === 'string') {
(acc as any)[camelToDashCase(name)] = (cProps as any)[name];
}
return acc;
}, {});

View File

@ -33,11 +33,13 @@ export const createOverlayComponent = <OverlayComponent extends object, OverlayT
class Overlay extends React.Component<Props> {
overlay?: OverlayType;
el: HTMLDivElement;
el!: HTMLDivElement;
constructor(props: Props) {
super(props);
this.el = document.createElement('div');
if (typeof document !== 'undefined') {
this.el = document.createElement('div');
}
this.handleDismiss = this.handleDismiss.bind(this);
}

View File

@ -1,6 +1,11 @@
export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none';
import { AnimationBuilder } from '@ionic/core';
import { RouterOptions } from '../models';
import { RouterDirection } from '../models/RouterDirection';
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
routerLink?: string;
routerDirection?: RouterDirection;
routerOptions?: RouterOptions;
routerAnimation?: AnimationBuilder;
};

View File

@ -21,13 +21,17 @@ export { IonPage } from './IonPage';
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
export { IonTabs } from './navigation/IonTabs';
export { IonTabBar } from './navigation/IonTabBar';
export { IonTabButton } from './navigation/IonTabButton';
export { IonBackButton } from './navigation/IonBackButton';
export { IonRouterOutlet } from './IonRouterOutlet';
export { IonIcon } from './IonIcon';
export * from './IonRoute';
export * from './IonRedirect';
export * from './IonRouterContext';
// Utils
export { isPlatform, getPlatforms, getConfig } from './utils';
export { RouterDirection } from './hrefprops';
export { isPlatform, getPlatforms, getConfig, ionRenderToString } from './utils';
export * from './hrefprops';
// Ionic Animations
export { CreateAnimation } from './CreateAnimation';
@ -51,4 +55,6 @@ addIcons({
// TODO: defineCustomElements() is asyncronous
// We need to use the promise
defineCustomElements(window);
if (typeof window !== 'undefined') {
defineCustomElements(window);
}

View File

@ -3,9 +3,10 @@ import { JSX as IoniconsJSX } from 'ionicons';
import { /*@__PURE__*/ createReactComponent } from './createComponent';
export const IonTabButtonInner = /*@__PURE__*/createReactComponent<JSX.IonTabButton & { onIonTabButtonClick?: (e: CustomEvent) => void; }, HTMLIonTabButtonElement>('ion-tab-button');
export const IonTabBarInner = /*@__PURE__*/createReactComponent<JSX.IonTabBar, HTMLIonTabBarElement>('ion-tab-bar');
export const IonBackButtonInner = /*@__PURE__*/createReactComponent<Omit<JSX.IonBackButton, 'icon'>, HTMLIonBackButtonElement>('ion-back-button');
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet, HTMLIonRouterOutletElement>('ion-router-outlet');
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet & { setRef?: (val: HTMLIonRouterOutletElement) => void; }, HTMLIonRouterOutletElement>('ion-router-outlet');
// ionicons
export const IonIconInner = /*@__PURE__*/createReactComponent<IoniconsJSX.IonIcon, HTMLIonIconElement>('ion-icon');

View File

@ -17,10 +17,10 @@ export const IonBackButton = /*@__PURE__*/(() => class extends React.Component<P
context!: React.ContextType<typeof NavContext>;
clickButton = (e: React.MouseEvent) => {
const defaultHref = this.props.defaultHref;
const { defaultHref, routerAnimation } = this.props;
if (this.context.hasIonicRouter()) {
e.stopPropagation();
this.context.goBack(defaultHref);
this.context.goBack(defaultHref, routerAnimation);
} else if (defaultHref !== undefined) {
window.location.href = defaultHref;
}

View File

@ -2,25 +2,31 @@ import { JSX as LocalJSX } from '@ionic/core';
import React, { useContext } from 'react';
import { NavContext } from '../../contexts/NavContext';
import { RouteInfo } from '../../models';
import { IonicReactProps } from '../IonicReactProps';
import { IonTabBarInner } from '../inner-proxies';
import { IonTabButton } from '../proxies';
import { createForwardRef } from '../utils';
import { IonTabButton } from './IonTabButton';
type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & {
onIonTabsDidChange?: (event: CustomEvent<{ tab: string; }>) => void;
onIonTabsWillChange?: (event: CustomEvent<{ tab: string; }>) => void;
currentPath?: string;
slot?: 'bottom' | 'top';
style?: { [key: string]: string; };
};
interface InternalProps extends IonTabBarProps {
forwardedRef?: React.RefObject<HTMLIonIconElement>;
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
routeInfo: RouteInfo;
}
interface TabUrls {
originalHref: string;
currentHref: string;
originalRouteOptions?: unknown;
currentRouteOptions?: unknown;
}
interface IonTabBarState {
@ -34,12 +40,13 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
constructor(props: InternalProps) {
super(props);
const tabs: { [key: string]: TabUrls; } = {};
React.Children.forEach((props as any).children, (child: any) => {
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href
currentHref: child.props.href,
originalRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
currentRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
};
}
});
@ -48,7 +55,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
const activeTab = tabKeys
.find(key => {
const href = tabs[key].originalHref;
return props.currentPath!.startsWith(href);
return props.routeInfo!.pathname.startsWith(href);
}) || tabKeys[0];
this.state = {
@ -59,71 +66,74 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
this.onTabButtonClick = this.onTabButtonClick.bind(this);
this.renderTabButton = this.renderTabButton.bind(this);
this.setActiveTabOnContext = this.setActiveTabOnContext.bind(this);
this.selectTab = this.selectTab.bind(this);
}
setActiveTabOnContext = (_tab: string) => { };
selectTab(tab: string) {
const tabUrl = this.state.tabs[tab];
if (tabUrl) {
this.onTabButtonClick(new CustomEvent('ionTabButtonClick', {
detail: {
href: tabUrl.currentHref,
tab,
selected: tab === this.state.activeTab
}
}));
return true;
}
return false;
}
static getDerivedStateFromProps(props: IonTabBarProps, state: IonTabBarState) {
static getDerivedStateFromProps(props: InternalProps, state: IonTabBarState) {
const tabs = { ...state.tabs };
const activeTab = Object.keys(state.tabs)
const tabKeys = Object.keys(state.tabs);
const activeTab = tabKeys
.find(key => {
const href = state.tabs[key].originalHref;
return props.currentPath!.startsWith(href);
return props.routeInfo!.pathname.startsWith(href);
});
// Check to see if the tab button href has changed, and if so, update it in the tabs state
React.Children.forEach((props as any).children, (child: any) => {
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
const tab = tabs[child.props.tab];
if (tab.originalHref !== child.props.href) {
if (!tab || (tab.originalHref !== child.props.href)) {
tabs[child.props.tab] = {
originalHref: child.props.href,
currentHref: child.props.href
currentHref: child.props.href,
originalRouteOptions: child.props.routeOptions,
currentRouteOptions: child.props.routeOptions
};
}
}
});
if (!(activeTab === undefined || (activeTab === state.activeTab && state.tabs[activeTab].currentHref === props.currentPath))) {
tabs[activeTab] = {
originalHref: tabs[activeTab].originalHref,
currentHref: props.currentPath!
};
const { activeTab: prevActiveTab } = state;
if (activeTab && prevActiveTab) {
const prevHref = state.tabs[prevActiveTab].currentHref;
const prevRouteOptions = state.tabs[prevActiveTab].currentRouteOptions;
if (activeTab !== prevActiveTab || (prevHref !== props.routeInfo?.pathname || prevRouteOptions !== props.routeInfo?.routeOptions)) {
tabs[activeTab] = {
originalHref: tabs[activeTab].originalHref,
currentHref: props.routeInfo!.pathname + (props.routeInfo!.search || ''),
originalRouteOptions: tabs[activeTab].originalRouteOptions,
currentRouteOptions: props.routeInfo?.routeOptions
};
if (props.routeInfo.routeAction === 'pop') {
// If navigating back and the tabs change, set the prev tab back to its original href
tabs[prevActiveTab] = {
originalHref: tabs[prevActiveTab].originalHref,
currentHref: tabs[prevActiveTab].originalHref,
originalRouteOptions: tabs[prevActiveTab].originalRouteOptions,
currentRouteOptions: tabs[prevActiveTab].currentRouteOptions
};
}
}
}
activeTab && props.onSetCurrentTab(activeTab, props.routeInfo);
return {
activeTab,
tabs
};
}
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; }>) {
const originalHref = this.state.tabs[e.detail.tab].originalHref;
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; routeOptions: unknown; }>) {
const tappedTab = this.state.tabs[e.detail.tab];
const originalHref = tappedTab.originalHref;
const currentHref = e.detail.href;
const { activeTab: prevActiveTab } = this.state;
// this.props.onSetCurrentTab(e.detail.tab, this.props.routeInfo);
// Clicking the current tab will bring you back to the original href
if (prevActiveTab === e.detail.tab) {
if (originalHref === currentHref) {
this.context.navigate(originalHref, 'none');
} else {
this.context.navigate(originalHref, 'back', 'pop');
if (originalHref !== currentHref) {
this.context.resetTab(e.detail.tab, originalHref, tappedTab.originalRouteOptions);
}
} else {
if (this.props.onIonTabsWillChange) {
@ -132,19 +142,22 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
if (this.props.onIonTabsDidChange) {
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
}
this.setActiveTabOnContext(e.detail.tab);
this.context.navigate(currentHref, 'none');
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
}
}
private renderTabButton(activeTab: string | null | undefined) {
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onIonTabButtonClick: (e: CustomEvent) => void; }>) | null | undefined) => {
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onClick: (e: any) => void; routeOptions?: unknown; }>) | 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);
const href = (child.props.tab === activeTab) ? this.props.routeInfo?.pathname : (this.state.tabs[child.props.tab!].currentHref);
const routeOptions = (child.props.tab === activeTab) ? this.props.routeInfo?.routeOptions : (this.state.tabs[child.props.tab!].currentRouteOptions);
return React.cloneElement(child, {
href,
onIonTabButtonClick: this.onTabButtonClick
routeOptions,
onClick: this.onTabButtonClick
});
}
return null;
@ -171,7 +184,8 @@ const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({
<IonTabBarUnwrapped
ref={forwardedRef}
{...props as any}
currentPath={props.currentPath || context.currentPath}
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
onSetCurrentTab={context.setCurrentTab}
>
{props.children}
</IonTabBarUnwrapped>

View File

@ -0,0 +1,39 @@
import { JSX as LocalJSX } from '@ionic/core';
import React from 'react';
import { RouterOptions } from '../../models';
import { IonicReactProps } from '../IonicReactProps';
import { IonTabButtonInner } from '../inner-proxies';
type Props = LocalJSX.IonTabButton & IonicReactProps & {
routerOptions?: RouterOptions;
ref?: React.RefObject<HTMLIonTabButtonElement>;
onClick?: (e: any) => void;
};
export class IonTabButton extends React.Component<Props> {
constructor(props: Props) {
super(props);
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
}
handleIonTabButtonClick() {
if (this.props.onClick) {
this.props.onClick(new CustomEvent('ionTabButtonClick', {
detail: { tab: this.props.tab, href: this.props.href, routeOptions: this.props.routerOptions }
}));
}
}
render() {
const { onClick, ...rest } = this.props;
return (
<IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>
);
}
static get displayName() {
return 'IonTabButton';
}
}

View File

@ -2,14 +2,34 @@ import { JSX as LocalJSX } from '@ionic/core';
import React, { Fragment } from 'react';
import { NavContext } from '../../contexts/NavContext';
import PageManager from '../../routing/PageManager';
import { IonRouterOutlet } from '../IonRouterOutlet';
import { IonTabBar } from './IonTabBar';
import { IonTabsContext, IonTabsContextState } from './IonTabsContext';
class IonTabsElement extends HTMLDivElement {
constructor() {
super();
}
}
if (window && window.customElements) {
customElements.define('ion-tabs', IonTabsElement, { extends: 'div' });
}
declare global {
namespace JSX {
interface IntrinsicElements {
'ion-tabs': any;
}
}
}
type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
interface Props extends LocalJSX.IonTabs {
className?: string;
children: ChildFunction | React.ReactNode;
}
@ -71,7 +91,7 @@ export class IonTabs extends React.Component<Props> {
return;
}
if (child.type === IonRouterOutlet) {
outlet = child;
outlet = React.cloneElement(child, { tabs: true });
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
outlet = child.props.children[0];
}
@ -96,21 +116,34 @@ export class IonTabs extends React.Component<Props> {
throw new Error('IonTabs must contain an IonRouterOutlet');
}
if (!tabBar) {
// TODO, this is not required
throw new Error('IonTabs needs a IonTabBar');
}
const { className, ...props } = this.props;
return (
<IonTabsContext.Provider
value={this.ionTabContextState}
>
<div style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{outlet}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</div>
{this.context.hasIonicRouter() ? (
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
<ion-tabs className="ion-tabs" style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{outlet}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</ion-tabs>
</PageManager>
) : (
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
{tabBar.props.slot === 'top' ? tabBar : null}
<div style={tabsInner} className="tabs-inner">
{outlet}
</div>
{tabBar.props.slot === 'bottom' ? tabBar : null}
</div>
)}
</IonTabsContext.Provider >
);
}

View File

@ -6,7 +6,6 @@ import { HrefProps } from './hrefprops';
// 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<HrefProps<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');

View File

@ -21,7 +21,6 @@ export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}
syncEvent(node, eventNameLc, newProps[name]);
}
} else {
(node as any)[name] = newProps[name];
const propType = typeof newProps[name];
if (propType === 'string') {
node.setAttribute(camelToDashCase(name), newProps[name]);
@ -61,20 +60,24 @@ export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: a
* 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;
export const isCoveredByReact = (eventNameSuffix: string) => {
if (typeof document === 'undefined') {
return true;
} else {
const eventName = 'on' + eventNameSuffix;
let isSupported = eventName in document;
if (!isSupported) {
const element = doc.createElement('div');
element.setAttribute(eventName, 'return;');
isSupported = typeof (element as any)[eventName] === 'function';
if (!isSupported) {
const element = document.createElement('div');
element.setAttribute(eventName, 'return;');
isSupported = typeof (element as any)[eventName] === 'function';
}
return isSupported;
}
return isSupported;
};
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, eventName: string, newEventHandler?: (e: Event) => any) => {
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined; }; }, eventName: string, newEventHandler?: (e: Event) => any) => {
const eventStore = node.__events || (node.__events = {});
const oldEventHandler = eventStore[eventName];

View File

@ -16,6 +16,7 @@ export const createForwardRef = <PropType, ElementType>(ReactComponent: any, dis
export * from './attachProps';
export * from './case';
export * from './ionRenderToString';
export const isPlatform = (platform: Platforms) => {
return isPlatformCore(window, platform);

View File

@ -0,0 +1,37 @@
import { SerializeDocumentOptions, renderToString } from '@ionic/core/hydrate';
export async function ionRenderToString(html: string, userAgent: string, options: SerializeDocumentOptions = {}) {
const renderToStringOptions = Object.assign({}, {
clientHydrateAnnotations: false,
excludeComponents: [
// overlays
'ion-action-sheet',
'ion-alert',
'ion-loading',
'ion-modal',
'ion-picker',
'ion-popover',
'ion-toast',
// navigation
'ion-router',
'ion-route',
'ion-route-redirect',
'ion-router-link',
'ion-router-outlet',
// tabs
'ion-tabs',
'ion-tab',
// auxiliary
'ion-picker-column',
'ion-virtual-scroll'
],
userAgent
}, options);
const ionHtml = await renderToString(html, renderToStringOptions);
return ionHtml.html;
}