import React, { createElement } from 'react'; import { attachProps, camelToDashCase, 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, defineCustomElement?: () => void ) => { if (defineCustomElement !== undefined) { defineCustomElement(); } 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: any, name) => { const value = (cProps as any)[name]; if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { const eventName = name.substring(2).toLowerCase(); if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { acc[name] = value; } } else { // we should only render strings, booleans, and numbers as attrs in html. // objects, functions, arrays etc get synced via properties on mount. const type = typeof value; if (type === 'string' || type === 'boolean' || type === 'number') { acc[camelToDashCase(name)] = value; } } return acc; }, {} as ExpandedPropsTypes); if (manipulatePropsFunction) { propsToPass = manipulatePropsFunction(this.props, propsToPass); } const newProps: Omit, 'forwardedRef'> = { ...propsToPass, ref: mergeRefs(forwardedRef, this.setComponentElRef), style, }; /** * We use createElement here instead of * React.createElement to work around a * bug in Vite (https://github.com/vitejs/vite/issues/6104). * React.createElement causes all elements to be rendered * as instead of the actual Web Component. */ return 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); };