diff --git a/core/package-lock.json b/core/package-lock.json index 628dc15a80..fb6c3fe53a 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -18,6 +18,7 @@ "@jest/core": "^26.6.3", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", + "@stencil/react-output-target": "^0.0.12", "@stencil/sass": "1.3.2", "@stencil/vue-output-target": "^0.5.1", "@types/jest": "^26.0.20", @@ -1367,6 +1368,15 @@ "npm": ">=6.0.0" } }, + "node_modules/@stencil/react-output-target": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@stencil/react-output-target/-/react-output-target-0.0.12.tgz", + "integrity": "sha512-X/lWAI/FW4tg/pjwe5UWy8KbRk2vWcWR+S6tBqNzKO6pKD6qr60dfajN13EO9nnm5hGr48FP1m/M8kqFbjpZrg==", + "dev": true, + "peerDependencies": { + "@stencil/core": ">=1.8.0" + } + }, "node_modules/@stencil/sass": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.3.2.tgz", @@ -15010,6 +15020,12 @@ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.6.0.tgz", "integrity": "sha512-QsxWayZyusnqSZrlCl81R71rA3KqFjVVQSH4E0rGN15F1GdQaFonKlHLyCOLKLig1zzC+DQkLLiUuocexuvdeQ==" }, + "@stencil/react-output-target": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@stencil/react-output-target/-/react-output-target-0.0.12.tgz", + "integrity": "sha512-X/lWAI/FW4tg/pjwe5UWy8KbRk2vWcWR+S6tBqNzKO6pKD6qr60dfajN13EO9nnm5hGr48FP1m/M8kqFbjpZrg==", + "dev": true + }, "@stencil/sass": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.3.2.tgz", diff --git a/core/package.json b/core/package.json index eebee14122..57a1c0a596 100644 --- a/core/package.json +++ b/core/package.json @@ -40,6 +40,7 @@ "@jest/core": "^26.6.3", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", + "@stencil/react-output-target": "^0.0.12", "@stencil/sass": "1.3.2", "@stencil/vue-output-target": "^0.5.1", "@types/jest": "^26.0.20", diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 330d22b4a3..e09ecdb3d3 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -1,6 +1,7 @@ import { Config } from '@stencil/core'; import { sass } from '@stencil/sass'; import { vueOutputTarget } from '@stencil/vue-output-target'; +import { reactOutputTarget } from '@stencil/react-output-target'; // @ts-ignore import { apiSpecGenerator } from './scripts/api-spec-generator'; @@ -61,6 +62,40 @@ export const config: Config = { }) ], outputTargets: [ + reactOutputTarget({ + componentCorePackage: '@ionic/core', + includePolyfills: false, + includeDefineCustomElements: false, + proxiesFile: '../packages/react/src/components/proxies.ts', + excludeComponents: [ + // Routing + 'ion-router', + 'ion-route', + 'ion-route-redirect', + 'ion-router-link', + 'ion-router-outlet', + 'ion-back-button', + 'ion-tab-button', + 'ion-tabs', + 'ion-tab-bar', + 'ion-button', + 'ion-card', + 'ion-fab-button', + 'ion-item', + 'ion-item-option', + + // Overlays + 'ion-action-sheet', + 'ion-alert', + 'ion-loading', + 'ion-modal', + 'ion-picker', + 'ion-popover', + 'ion-toast', + + 'ion-icon' + ] + }), vueOutputTarget({ componentCorePackage: '@ionic/core', includeImportCustomElements: true, diff --git a/packages/react-router/test-app/package-lock.json b/packages/react-router/test-app/package-lock.json index fc2757fed5..4c0411e87f 100644 --- a/packages/react-router/test-app/package-lock.json +++ b/packages/react-router/test-app/package-lock.json @@ -77,7 +77,7 @@ "style-loader": "0.23.1", "terser-webpack-plugin": "2.3.4", "ts-pnp": "1.1.5", - "typescript": "3.7.4", + "typescript": "^3.9.5", "url-loader": "2.3.0", "wait-on": "^5.3.0", "webpack": "4.41.5", @@ -19603,9 +19603,10 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "node_modules/typescript": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz", - "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -37499,9 +37500,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz", - "integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==" + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "undefsafe": { "version": "2.0.3", diff --git a/packages/react-router/test-app/package.json b/packages/react-router/test-app/package.json index 3bf7a3833b..4376c5be71 100644 --- a/packages/react-router/test-app/package.json +++ b/packages/react-router/test-app/package.json @@ -72,7 +72,7 @@ "style-loader": "0.23.1", "terser-webpack-plugin": "2.3.4", "ts-pnp": "1.1.5", - "typescript": "3.7.4", + "typescript": "^3.9.5", "url-loader": "2.3.0", "wait-on": "^5.3.0", "webpack": "4.41.5", diff --git a/packages/react/src/components/IonIcon.tsx b/packages/react/src/components/IonIcon.tsx index b26e960978..a9c912a426 100644 --- a/packages/react/src/components/IonIcon.tsx +++ b/packages/react/src/components/IonIcon.tsx @@ -4,8 +4,8 @@ import { NavContext } from '../contexts/NavContext'; import { IonicReactProps } from './IonicReactProps'; import { IonIconInner } from './inner-proxies'; +import { deprecationWarning } from './react-component-lib/utils/dev'; import { createForwardRef, isPlatform } from './utils'; -import { deprecationWarning } from './utils/dev'; interface IonIconProps { ariaLabel?: string; diff --git a/packages/react/src/components/__tests__/createComponent.spec.tsx b/packages/react/src/components/__tests__/createComponent.spec.tsx index e7f5d45b5c..b288efb613 100644 --- a/packages/react/src/components/__tests__/createComponent.spec.tsx +++ b/packages/react/src/components/__tests__/createComponent.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { JSX } from '@ionic/core'; -import { createReactComponent } from '../createComponent'; +import { createReactComponent } from '../react-component-lib'; import { render, fireEvent, cleanup, RenderResult } from '@testing-library/react'; import { IonButton } from '../index'; diff --git a/packages/react/src/components/__tests__/utils.spec.ts b/packages/react/src/components/__tests__/utils.spec.ts index 467637df2d..3714aa2a85 100644 --- a/packages/react/src/components/__tests__/utils.spec.ts +++ b/packages/react/src/components/__tests__/utils.spec.ts @@ -1,4 +1,4 @@ -import * as utils from '../utils'; +import * as utils from '../react-component-lib/utils'; import '@testing-library/jest-dom/extend-expect'; describe('isCoveredByReact', () => { diff --git a/packages/react/src/components/createControllerComponent.tsx b/packages/react/src/components/createControllerComponent.tsx index 045c2a9a63..595f820218 100644 --- a/packages/react/src/components/createControllerComponent.tsx +++ b/packages/react/src/components/createControllerComponent.tsx @@ -1,7 +1,7 @@ import { OverlayEventDetail } from '@ionic/core'; import React from 'react'; -import { attachProps, setRef } from './utils'; +import { attachProps, setRef } from './react-component-lib/utils'; interface OverlayBase extends HTMLElement { present: () => Promise; diff --git a/packages/react/src/components/createInlineOverlayComponent.tsx b/packages/react/src/components/createInlineOverlayComponent.tsx index ef39a6c697..f0c654850c 100644 --- a/packages/react/src/components/createInlineOverlayComponent.tsx +++ b/packages/react/src/components/createInlineOverlayComponent.tsx @@ -4,10 +4,12 @@ import React from 'react'; import { attachProps, camelToDashCase, - createForwardRef, dashToPascalCase, isCoveredByReact, mergeRefs, +} from './react-component-lib/utils'; +import { + createForwardRef } from './utils'; type InlineOverlayState = { diff --git a/packages/react/src/components/createOverlayComponent.tsx b/packages/react/src/components/createOverlayComponent.tsx index d2fc23a329..3bfa077367 100644 --- a/packages/react/src/components/createOverlayComponent.tsx +++ b/packages/react/src/components/createOverlayComponent.tsx @@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core'; import React from 'react'; import ReactDOM from 'react-dom'; -import { attachProps, setRef } from './utils'; +import { attachProps, setRef } from './react-component-lib/utils'; interface OverlayElement extends HTMLElement { present: () => Promise; diff --git a/packages/react/src/components/createComponent.tsx b/packages/react/src/components/createRoutingComponent.tsx similarity index 82% rename from packages/react/src/components/createComponent.tsx rename to packages/react/src/components/createRoutingComponent.tsx index ec2d84842b..4931db4add 100644 --- a/packages/react/src/components/createComponent.tsx +++ b/packages/react/src/components/createRoutingComponent.tsx @@ -8,10 +8,12 @@ import { RouterDirection } from '../models/RouterDirection'; import { attachProps, camelToDashCase, - createForwardRef, dashToPascalCase, isCoveredByReact, mergeRefs, +} from './react-component-lib/utils'; +import { + createForwardRef } from './utils'; interface IonicReactInternalProps extends React.HTMLAttributes { @@ -24,9 +26,8 @@ interface IonicReactInternalProps extends React.HTMLAttributes( - tagName: string, - routerLinkComponent = false +export const createRoutingComponent = ( + tagName: string ) => { const displayName = dashToPascalCase(tagName); const ReactComponent = class extends React.Component> { @@ -86,21 +87,19 @@ export const createReactComponent = ( style, }; - if (routerLinkComponent) { - if (this.props.routerLink && !this.props.href) { - newProps.href = this.props.routerLink; - } - if (newProps.onClick) { - const oldClick = newProps.onClick; - newProps.onClick = (e: React.MouseEvent) => { - oldClick(e); - if (!e.defaultPrevented) { - this.handleClick(e); - } - }; - } else { - newProps.onClick = this.handleClick; - } + if (this.props.routerLink && !this.props.href) { + newProps.href = this.props.routerLink; + } + if (newProps.onClick) { + const oldClick = newProps.onClick; + newProps.onClick = (e: React.MouseEvent) => { + oldClick(e); + if (!e.defaultPrevented) { + this.handleClick(e); + } + }; + } else { + newProps.onClick = this.handleClick; } return React.createElement(tagName, newProps, children); diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 103889dd27..5cebaf112b 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -63,6 +63,7 @@ export { ToastButton } from '@ionic/core'; export * from './proxies'; +export * from './routing-proxies'; // createControllerComponent export { IonAlert } from './IonAlert'; diff --git a/packages/react/src/components/inner-proxies.ts b/packages/react/src/components/inner-proxies.ts index b297cd6e19..884b1b2792 100644 --- a/packages/react/src/components/inner-proxies.ts +++ b/packages/react/src/components/inner-proxies.ts @@ -1,7 +1,7 @@ import { JSX } from '@ionic/core'; import { JSX as IoniconsJSX } from 'ionicons'; -import { /*@__PURE__*/ createReactComponent } from './createComponent'; +import { /*@__PURE__*/ createReactComponent } from './react-component-lib'; export const IonTabButtonInner = /*@__PURE__*/ createReactComponent< JSX.IonTabButton & { onIonTabButtonClick?: (e: CustomEvent) => void }, diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index 78a630b305..ca7840b3a3 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -1,255 +1,77 @@ -import { JSX } from '@ionic/core'; +/* eslint-disable */ +/* tslint:disable */ +/* auto-generated react proxies */ +import { createReactComponent } from './react-component-lib'; -import { createReactComponent } from './createComponent'; -import { HrefProps } from './hrefprops'; +import type { JSX } from '@ionic/core'; -// ionic/core -export const IonApp = /*@__PURE__*/ createReactComponent('ion-app'); -export const IonTab = /*@__PURE__*/ createReactComponent('ion-tab'); -export const IonRouterLink = /*@__PURE__*/ createReactComponent< - HrefProps, - HTMLIonRouterLinkElement ->('ion-router-link', true); -export const IonAccordion = /*@__PURE__*/ createReactComponent( - 'ion-accordion' -); -export const IonAccordionGroup = /*@__PURE__*/ createReactComponent( - 'ion-accordion-group' -); -export const IonAvatar = /*@__PURE__*/ createReactComponent( - 'ion-avatar' -); -export const IonBackdrop = /*@__PURE__*/ createReactComponent< - JSX.IonBackdrop, - HTMLIonBackdropElement ->('ion-backdrop'); -export const IonBadge = /*@__PURE__*/ createReactComponent( - 'ion-badge' -); -export const IonBreadcrumb = /*@__PURE__*/ createReactComponent( - 'ion-breadcrumb' -); -export const IonBreadcrumbs = /*@__PURE__*/ createReactComponent( - 'ion-breadcrumbs' -); -export const IonButton = /*@__PURE__*/ createReactComponent< - HrefProps, - HTMLIonButtonElement ->('ion-button', true); -export const IonButtons = /*@__PURE__*/ createReactComponent( - 'ion-buttons' -); -export const IonCard = /*@__PURE__*/ createReactComponent< - HrefProps, - 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('ion-col'); -export const IonContent = /*@__PURE__*/ createReactComponent( - 'ion-content' -); -export const IonChip = /*@__PURE__*/ createReactComponent( - 'ion-chip' -); -export const IonDatetime = /*@__PURE__*/ createReactComponent< - JSX.IonDatetime, - HTMLIonDatetimeElement ->('ion-datetime'); -export const IonFab = /*@__PURE__*/ createReactComponent('ion-fab'); -export const IonFabButton = /*@__PURE__*/ createReactComponent< - HrefProps, - HTMLIonFabButtonElement ->('ion-fab-button', true); -export const IonFabList = /*@__PURE__*/ createReactComponent( - 'ion-fab-list' -); -export const IonFooter = /*@__PURE__*/ createReactComponent( - 'ion-footer' -); -export const IonGrid = /*@__PURE__*/ createReactComponent( - 'ion-grid' -); -export const IonHeader = /*@__PURE__*/ createReactComponent( - 'ion-header' -); -export const IonImg = /*@__PURE__*/ createReactComponent('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( - 'ion-input' -); -export const IonItem = /*@__PURE__*/ createReactComponent< - HrefProps, - 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< - HrefProps, - 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( - 'ion-label' -); -export const IonList = /*@__PURE__*/ createReactComponent( - 'ion-list' -); -export const IonListHeader = /*@__PURE__*/ createReactComponent< - JSX.IonListHeader, - HTMLIonListHeaderElement ->('ion-list-header'); -export const IonMenu = /*@__PURE__*/ createReactComponent( - '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( - 'ion-note' -); -export const IonPickerColumn = /*@__PURE__*/ createReactComponent< - JSX.IonPickerColumn, - HTMLIonPickerColumnElement ->('ion-picker-column'); -export const IonNav = /*@__PURE__*/ createReactComponent('ion-nav'); -export const IonProgressBar = /*@__PURE__*/ createReactComponent< - JSX.IonProgressBar, - HTMLIonProgressBarElement ->('ion-progress-bar'); -export const IonRadio = /*@__PURE__*/ createReactComponent( - 'ion-radio' -); -export const IonRadioGroup = /*@__PURE__*/ createReactComponent< - JSX.IonRadioGroup, - HTMLIonRadioGroupElement ->('ion-radio-group'); -export const IonRange = /*@__PURE__*/ createReactComponent( - '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( - '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 IonRow = /*@__PURE__*/ createReactComponent('ion-row'); -export const IonSearchbar = /*@__PURE__*/ createReactComponent< - JSX.IonSearchbar, - HTMLIonSearchbarElement ->('ion-searchbar'); -export const IonSegment = /*@__PURE__*/ createReactComponent( - 'ion-segment' -); -export const IonSegmentButton = /*@__PURE__*/ createReactComponent< - JSX.IonSegmentButton, - HTMLIonSegmentButtonElement ->('ion-segment-button'); -export const IonSelect = /*@__PURE__*/ createReactComponent( - '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( - 'ion-slide' -); -export const IonSlides = /*@__PURE__*/ createReactComponent( - 'ion-slides' -); -export const IonSpinner = /*@__PURE__*/ createReactComponent( - 'ion-spinner' -); -export const IonSplitPane = /*@__PURE__*/ createReactComponent< - JSX.IonSplitPane, - HTMLIonSplitPaneElement ->('ion-split-pane'); -export const IonText = /*@__PURE__*/ createReactComponent( - '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( - 'ion-title' -); -export const IonToggle = /*@__PURE__*/ createReactComponent( - 'ion-toggle' -); -export const IonToolbar = /*@__PURE__*/ createReactComponent( - 'ion-toolbar' -); -export const IonVirtualScroll = /*@__PURE__*/ createReactComponent< - JSX.IonVirtualScroll, - HTMLIonVirtualScrollElement ->('ion-virtual-scroll'); + + +export const IonAccordion = /*@__PURE__*/createReactComponent('ion-accordion'); +export const IonAccordionGroup = /*@__PURE__*/createReactComponent('ion-accordion-group'); +export const IonApp = /*@__PURE__*/createReactComponent('ion-app'); +export const IonAvatar = /*@__PURE__*/createReactComponent('ion-avatar'); +export const IonBackdrop = /*@__PURE__*/createReactComponent('ion-backdrop'); +export const IonBadge = /*@__PURE__*/createReactComponent('ion-badge'); +export const IonBreadcrumb = /*@__PURE__*/createReactComponent('ion-breadcrumb'); +export const IonBreadcrumbs = /*@__PURE__*/createReactComponent('ion-breadcrumbs'); +export const IonButtons = /*@__PURE__*/createReactComponent('ion-buttons'); +export const IonCardContent = /*@__PURE__*/createReactComponent('ion-card-content'); +export const IonCardHeader = /*@__PURE__*/createReactComponent('ion-card-header'); +export const IonCardSubtitle = /*@__PURE__*/createReactComponent('ion-card-subtitle'); +export const IonCardTitle = /*@__PURE__*/createReactComponent('ion-card-title'); +export const IonCheckbox = /*@__PURE__*/createReactComponent('ion-checkbox'); +export const IonChip = /*@__PURE__*/createReactComponent('ion-chip'); +export const IonCol = /*@__PURE__*/createReactComponent('ion-col'); +export const IonContent = /*@__PURE__*/createReactComponent('ion-content'); +export const IonDatetime = /*@__PURE__*/createReactComponent('ion-datetime'); +export const IonFab = /*@__PURE__*/createReactComponent('ion-fab'); +export const IonFabList = /*@__PURE__*/createReactComponent('ion-fab-list'); +export const IonFooter = /*@__PURE__*/createReactComponent('ion-footer'); +export const IonGrid = /*@__PURE__*/createReactComponent('ion-grid'); +export const IonHeader = /*@__PURE__*/createReactComponent('ion-header'); +export const IonImg = /*@__PURE__*/createReactComponent('ion-img'); +export const IonInfiniteScroll = /*@__PURE__*/createReactComponent('ion-infinite-scroll'); +export const IonInfiniteScrollContent = /*@__PURE__*/createReactComponent('ion-infinite-scroll-content'); +export const IonInput = /*@__PURE__*/createReactComponent('ion-input'); +export const IonItemDivider = /*@__PURE__*/createReactComponent('ion-item-divider'); +export const IonItemGroup = /*@__PURE__*/createReactComponent('ion-item-group'); +export const IonItemOptions = /*@__PURE__*/createReactComponent('ion-item-options'); +export const IonItemSliding = /*@__PURE__*/createReactComponent('ion-item-sliding'); +export const IonLabel = /*@__PURE__*/createReactComponent('ion-label'); +export const IonList = /*@__PURE__*/createReactComponent('ion-list'); +export const IonListHeader = /*@__PURE__*/createReactComponent('ion-list-header'); +export const IonMenu = /*@__PURE__*/createReactComponent('ion-menu'); +export const IonMenuButton = /*@__PURE__*/createReactComponent('ion-menu-button'); +export const IonMenuToggle = /*@__PURE__*/createReactComponent('ion-menu-toggle'); +export const IonNav = /*@__PURE__*/createReactComponent('ion-nav'); +export const IonNavLink = /*@__PURE__*/createReactComponent('ion-nav-link'); +export const IonNote = /*@__PURE__*/createReactComponent('ion-note'); +export const IonProgressBar = /*@__PURE__*/createReactComponent('ion-progress-bar'); +export const IonRadio = /*@__PURE__*/createReactComponent('ion-radio'); +export const IonRadioGroup = /*@__PURE__*/createReactComponent('ion-radio-group'); +export const IonRange = /*@__PURE__*/createReactComponent('ion-range'); +export const IonRefresher = /*@__PURE__*/createReactComponent('ion-refresher'); +export const IonRefresherContent = /*@__PURE__*/createReactComponent('ion-refresher-content'); +export const IonReorder = /*@__PURE__*/createReactComponent('ion-reorder'); +export const IonReorderGroup = /*@__PURE__*/createReactComponent('ion-reorder-group'); +export const IonRippleEffect = /*@__PURE__*/createReactComponent('ion-ripple-effect'); +export const IonRow = /*@__PURE__*/createReactComponent('ion-row'); +export const IonSearchbar = /*@__PURE__*/createReactComponent('ion-searchbar'); +export const IonSegment = /*@__PURE__*/createReactComponent('ion-segment'); +export const IonSegmentButton = /*@__PURE__*/createReactComponent('ion-segment-button'); +export const IonSelect = /*@__PURE__*/createReactComponent('ion-select'); +export const IonSelectOption = /*@__PURE__*/createReactComponent('ion-select-option'); +export const IonSkeletonText = /*@__PURE__*/createReactComponent('ion-skeleton-text'); +export const IonSlide = /*@__PURE__*/createReactComponent('ion-slide'); +export const IonSlides = /*@__PURE__*/createReactComponent('ion-slides'); +export const IonSpinner = /*@__PURE__*/createReactComponent('ion-spinner'); +export const IonSplitPane = /*@__PURE__*/createReactComponent('ion-split-pane'); +export const IonTab = /*@__PURE__*/createReactComponent('ion-tab'); +export const IonText = /*@__PURE__*/createReactComponent('ion-text'); +export const IonTextarea = /*@__PURE__*/createReactComponent('ion-textarea'); +export const IonThumbnail = /*@__PURE__*/createReactComponent('ion-thumbnail'); +export const IonTitle = /*@__PURE__*/createReactComponent('ion-title'); +export const IonToggle = /*@__PURE__*/createReactComponent('ion-toggle'); +export const IonToolbar = /*@__PURE__*/createReactComponent('ion-toolbar'); +export const IonVirtualScroll = /*@__PURE__*/createReactComponent('ion-virtual-scroll'); diff --git a/packages/react/src/components/react-component-lib/createComponent.tsx b/packages/react/src/components/react-component-lib/createComponent.tsx new file mode 100644 index 0000000000..b1fe50be6b --- /dev/null +++ b/packages/react/src/components/react-component-lib/createComponent.tsx @@ -0,0 +1,93 @@ +import React from 'react'; + +import { + attachProps, + createForwardRef, + dashToPascalCase, + isCoveredByReact, + mergeRefs, +} from './utils'; + +export interface HTMLStencilElement extends HTMLElement { + componentOnReady(): Promise; +} + +interface StencilReactInternalProps extends React.HTMLAttributes { + forwardedRef: React.RefObject; + ref?: React.Ref; +} + +export const createReactComponent = < + PropType, + ElementType extends HTMLStencilElement, + ContextStateType = {}, + ExpandedPropsTypes = {} +>( + tagName: string, + ReactComponentContext?: React.Context, + manipulatePropsFunction?: ( + originalProps: StencilReactInternalProps, + propsToPass: any, + ) => ExpandedPropsTypes, +) => { + const displayName = dashToPascalCase(tagName); + + const ReactComponent = class extends React.Component> { + componentEl!: ElementType; + + setComponentElRef = (element: ElementType) => { + this.componentEl = element; + }; + + constructor(props: StencilReactInternalProps) { + super(props); + } + + componentDidMount() { + this.componentDidUpdate(this.props); + } + + componentDidUpdate(prevProps: StencilReactInternalProps) { + attachProps(this.componentEl, this.props, prevProps); + } + + render() { + const { children, forwardedRef, style, className, ref, ...cProps } = this.props; + + let propsToPass = Object.keys(cProps).reduce((acc, name) => { + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + const eventName = name.substring(2).toLowerCase(); + if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { + (acc as any)[name] = (cProps as any)[name]; + } + } else { + (acc as any)[name] = (cProps as any)[name]; + } + return acc; + }, {}); + + if (manipulatePropsFunction) { + propsToPass = manipulatePropsFunction(this.props, propsToPass); + } + + const newProps: Omit, 'forwardedRef'> = { + ...propsToPass, + ref: mergeRefs(forwardedRef, this.setComponentElRef), + style, + }; + + return React.createElement(tagName, newProps, children); + } + + static get displayName() { + return displayName; + } + }; + + // If context was passed to createReactComponent then conditionally add it to the Component Class + if (ReactComponentContext) { + ReactComponent.contextType = ReactComponentContext; + } + + return createForwardRef(ReactComponent, displayName); +}; diff --git a/packages/react/src/components/react-component-lib/createOverlayComponent.tsx b/packages/react/src/components/react-component-lib/createOverlayComponent.tsx new file mode 100644 index 0000000000..c02d64c5bb --- /dev/null +++ b/packages/react/src/components/react-component-lib/createOverlayComponent.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { OverlayEventDetail } from './interfaces'; +import { StencilReactForwardedRef, attachProps, setRef } from './utils'; + +interface OverlayElement extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export interface ReactOverlayProps { + children?: React.ReactNode; + isOpen: boolean; + onDidDismiss?: (event: CustomEvent) => void; + onDidPresent?: (event: CustomEvent) => void; + onWillDismiss?: (event: CustomEvent) => void; + onWillPresent?: (event: CustomEvent) => void; +} + +export const createOverlayComponent = < + OverlayComponent extends object, + OverlayType extends OverlayElement +>( + displayName: string, + controller: { create: (options: any) => Promise } +) => { + const didDismissEventName = `on${displayName}DidDismiss`; + const didPresentEventName = `on${displayName}DidPresent`; + const willDismissEventName = `on${displayName}WillDismiss`; + const willPresentEventName = `on${displayName}WillPresent`; + + type Props = OverlayComponent & + ReactOverlayProps & { + forwardedRef?: StencilReactForwardedRef; + }; + + let isDismissing = false; + + class Overlay extends React.Component { + overlay?: OverlayType; + el!: HTMLDivElement; + + constructor(props: Props) { + super(props); + if (typeof document !== 'undefined') { + this.el = document.createElement('div'); + } + this.handleDismiss = this.handleDismiss.bind(this); + } + + static get displayName() { + return displayName; + } + + componentDidMount() { + if (this.props.isOpen) { + this.present(); + } + } + + componentWillUnmount() { + if (this.overlay) { + this.overlay.dismiss(); + } + } + + handleDismiss(event: CustomEvent>) { + if (this.props.onDidDismiss) { + this.props.onDidDismiss(event); + } + setRef(this.props.forwardedRef, null) + } + + shouldComponentUpdate(nextProps: Props) { + // Check if the overlay component is about to dismiss + if (this.overlay && nextProps.isOpen !== this.props.isOpen && nextProps.isOpen === false) { + isDismissing = true; + } + + return true; + } + + async componentDidUpdate(prevProps: Props) { + if (this.overlay) { + attachProps(this.overlay, this.props, prevProps); + } + + 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(); + isDismissing = false; + + /** + * Now that the overlay is dismissed + * we need to render again so that any + * inner components will be unmounted + */ + this.forceUpdate(); + } + } + + async present(prevProps?: Props) { + const { + children, + isOpen, + onDidDismiss, + onDidPresent, + onWillDismiss, + onWillPresent, + ...cProps + } = this.props; + const elementProps = { + ...cProps, + ref: this.props.forwardedRef, + [didDismissEventName]: this.handleDismiss, + [didPresentEventName]: (e: CustomEvent) => + this.props.onDidPresent && this.props.onDidPresent(e), + [willDismissEventName]: (e: CustomEvent) => + this.props.onWillDismiss && this.props.onWillDismiss(e), + [willPresentEventName]: (e: CustomEvent) => + this.props.onWillPresent && this.props.onWillPresent(e), + }; + + this.overlay = await controller.create({ + ...elementProps, + component: this.el, + componentProps: {}, + }); + + setRef(this.props.forwardedRef, this.overlay); + attachProps(this.overlay, elementProps, prevProps); + + await this.overlay.present(); + } + + render() { + /** + * Continue to render the component even when + * overlay is dismissing otherwise component + * will be hidden before animation is done. + */ + return ReactDOM.createPortal(this.props.isOpen || isDismissing ? this.props.children : null, this.el); + } + } + + return React.forwardRef((props, ref) => { + return ; + }); +}; diff --git a/packages/react/src/components/react-component-lib/index.ts b/packages/react/src/components/react-component-lib/index.ts new file mode 100644 index 0000000000..85e81ad196 --- /dev/null +++ b/packages/react/src/components/react-component-lib/index.ts @@ -0,0 +1,2 @@ +export { createReactComponent } from './createComponent'; +export { createOverlayComponent } from './createOverlayComponent'; diff --git a/packages/react/src/components/react-component-lib/interfaces.ts b/packages/react/src/components/react-component-lib/interfaces.ts new file mode 100644 index 0000000000..92e5389c88 --- /dev/null +++ b/packages/react/src/components/react-component-lib/interfaces.ts @@ -0,0 +1,34 @@ +// General types important to applications using stencil built components +export interface EventEmitter { + emit: (data?: T) => CustomEvent; +} + +export interface StyleReactProps { + class?: string; + className?: string; + style?: { [key: string]: any }; +} + +export interface OverlayEventDetail { + data?: T; + role?: string; +} + +export interface OverlayInterface { + el: HTMLElement; + animated: boolean; + keyboardClose: boolean; + overlayIndex: number; + presented: boolean; + + enterAnimation?: any; + leaveAnimation?: any; + + didPresent: EventEmitter; + willPresent: EventEmitter; + willDismiss: EventEmitter; + didDismiss: EventEmitter; + + present(): Promise; + dismiss(data?: any, role?: string): Promise; +} diff --git a/packages/react/src/components/utils/attachProps.ts b/packages/react/src/components/react-component-lib/utils/attachProps.ts similarity index 98% rename from packages/react/src/components/utils/attachProps.ts rename to packages/react/src/components/react-component-lib/utils/attachProps.ts index 6cc962d264..de2cc499b2 100644 --- a/packages/react/src/components/utils/attachProps.ts +++ b/packages/react/src/components/react-component-lib/utils/attachProps.ts @@ -28,11 +28,10 @@ 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]); - } else { - (node as any)[name] = newProps[name]; } } }); diff --git a/packages/react/src/components/utils/case.ts b/packages/react/src/components/react-component-lib/utils/case.ts similarity index 100% rename from packages/react/src/components/utils/case.ts rename to packages/react/src/components/react-component-lib/utils/case.ts diff --git a/packages/react/src/components/utils/dev.ts b/packages/react/src/components/react-component-lib/utils/dev.ts similarity index 100% rename from packages/react/src/components/utils/dev.ts rename to packages/react/src/components/react-component-lib/utils/dev.ts diff --git a/packages/react/src/components/react-component-lib/utils/index.tsx b/packages/react/src/components/react-component-lib/utils/index.tsx new file mode 100644 index 0000000000..7557c1ed2f --- /dev/null +++ b/packages/react/src/components/react-component-lib/utils/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import type { StyleReactProps } from '../interfaces'; + +export type StencilReactExternalProps = PropType & + Omit, 'style'> & + StyleReactProps; + +// This will be replaced with React.ForwardedRef when react-output-target is upgraded to React v17 +export type StencilReactForwardedRef = ((instance: T | null) => void) | React.MutableRefObject | null; + +export const setRef = (ref: StencilReactForwardedRef | React.Ref | undefined, value: any) => { + if (typeof ref === 'function') { + ref(value) + } else if (ref != null) { + // Cast as a MutableRef so we can assign current + (ref as React.MutableRefObject).current = value + } +}; + +export const mergeRefs = ( + ...refs: (StencilReactForwardedRef | React.Ref | undefined)[] +): React.RefCallback => { + return (value: any) => { + refs.forEach(ref => { + setRef(ref, value) + }) + } +}; + +export const createForwardRef = ( + ReactComponent: any, + displayName: string, +) => { + const forwardRef = ( + props: StencilReactExternalProps, + ref: StencilReactForwardedRef, + ) => { + return ; + }; + forwardRef.displayName = displayName; + + return React.forwardRef(forwardRef); +}; + +export * from './attachProps'; +export * from './case'; diff --git a/packages/react/src/components/routing-proxies.ts b/packages/react/src/components/routing-proxies.ts new file mode 100644 index 0000000000..1175d4b968 --- /dev/null +++ b/packages/react/src/components/routing-proxies.ts @@ -0,0 +1,34 @@ +import type { JSX } from '@ionic/core'; + +import { createRoutingComponent } from './createRoutingComponent'; +import { HrefProps } from './hrefprops'; + +export const IonRouterLink = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonRouterLinkElement +>('ion-router-link'); + +export const IonButton = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonButtonElement +>('ion-button'); + +export const IonCard = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonCardElement +>('ion-card'); + +export const IonFabButton = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonFabButtonElement +>('ion-fab-button'); + +export const IonItem = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonItemElement +>('ion-item'); + +export const IonItemOption = /*@__PURE__*/ createRoutingComponent< + HrefProps, + HTMLIonItemOptionElement +>('ion-item-option'); diff --git a/packages/react/src/components/utils/index.tsx b/packages/react/src/components/utils/index.tsx index 7e04de8d2d..e52dd41651 100644 --- a/packages/react/src/components/utils/index.tsx +++ b/packages/react/src/components/utils/index.tsx @@ -27,28 +27,6 @@ export const createForwardRef = ( return React.forwardRef(forwardRef); }; -export const setRef = (ref: React.ForwardedRef | React.Ref | undefined, value: any) => { - if (typeof ref === 'function') { - ref(value) - } else if (ref != null) { - // Cast as a MutableRef so we can assign current - (ref as React.MutableRefObject).current = value - } -}; - -export const mergeRefs = ( - ...refs: (React.ForwardedRef | React.Ref | undefined)[] -): React.RefCallback => { - return (value: any) => { - refs.forEach(ref => { - setRef(ref, value) - }) - } -}; - -export * from './attachProps'; -export * from './case'; - export const isPlatform = (platform: Platforms) => { return isPlatformCore(window, platform); }; diff --git a/packages/react/src/hooks/useController.ts b/packages/react/src/hooks/useController.ts index fa596f3704..37f3c1dc77 100644 --- a/packages/react/src/hooks/useController.ts +++ b/packages/react/src/hooks/useController.ts @@ -1,7 +1,7 @@ import { OverlayEventDetail } from '@ionic/core'; import { useMemo, useRef } from 'react'; -import { attachProps } from '../components/utils'; +import { attachProps } from '../components/react-component-lib/utils'; import { HookOverlayOptions } from './HookOverlayOptions'; diff --git a/packages/react/src/hooks/useOverlay.ts b/packages/react/src/hooks/useOverlay.ts index 6365bc4829..21389c1ba6 100644 --- a/packages/react/src/hooks/useOverlay.ts +++ b/packages/react/src/hooks/useOverlay.ts @@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { attachProps } from '../components/utils'; +import { attachProps } from '../components/react-component-lib/utils'; import { HookOverlayOptions } from './HookOverlayOptions'; diff --git a/packages/react/src/routing/PageManager.tsx b/packages/react/src/routing/PageManager.tsx index 3b3538e806..a1a7ee4817 100644 --- a/packages/react/src/routing/PageManager.tsx +++ b/packages/react/src/routing/PageManager.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { mergeRefs } from '../components/utils'; +import { mergeRefs } from '../components/react-component-lib/utils'; import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext'; import { RouteInfo } from '../models';