import type { AnimationBuilder } from '@ionic/core/components'; import React, { createElement } from 'react'; import { NavContext } from '../contexts/NavContext'; import type { RouterOptions } from '../models'; import type { RouterDirection } from '../models/RouterDirection'; import { attachProps, camelToDashCase, dashToPascalCase, defineCustomElement, isCoveredByReact, mergeRefs, } from './react-component-lib/utils'; import { createForwardRef } from './utils'; // TODO(FW-2959): types interface IonicReactInternalProps extends React.HTMLAttributes { forwardedRef?: React.ForwardedRef; href?: string; routerLink?: string; ref?: React.Ref; routerDirection?: RouterDirection; routerOptions?: RouterOptions; routerAnimation?: AnimationBuilder; } export const createRoutingComponent = (tagName: string, customElement?: any) => { defineCustomElement(tagName, customElement); const displayName = dashToPascalCase(tagName); const ReactComponent = class extends React.Component> { context!: React.ContextType; ref: React.RefObject; stableMergedRefs: React.RefCallback; constructor(props: IonicReactInternalProps) { super(props); // Create a local ref to to attach props to the wrapped element. this.ref = React.createRef(); // React refs must be stable (not created inline). this.stableMergedRefs = mergeRefs(this.ref, this.props.forwardedRef); } componentDidMount() { this.componentDidUpdate(this.props); } componentDidUpdate(prevProps: IonicReactInternalProps) { const node = this.ref.current! as HTMLElement; attachProps(node, this.props, prevProps); } private handleClick = (e: React.MouseEvent) => { const { routerLink, routerDirection, routerOptions, routerAnimation } = this.props; if (routerLink !== undefined) { e.preventDefault(); this.context.navigate(routerLink, routerDirection, undefined, routerAnimation, routerOptions); } }; render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars 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]; } } else if (['string', 'boolean', 'number'].includes(typeof (cProps as any)[name])) { (acc as any)[camelToDashCase(name)] = (cProps as any)[name]; } return acc; }, {}); const newProps: IonicReactInternalProps = { ...propsToPass, ref: this.stableMergedRefs, style, }; 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 createElement(tagName, newProps, children); } static get displayName() { return displayName; } static get contextType() { return NavContext; } }; return createForwardRef(ReactComponent, displayName); };