Skip to content

Commit

Permalink
Merge pull request Expensify#33643 from ruben-rebelo/ts-migration/HTM…
Browse files Browse the repository at this point in the history
…LEngineProvider-component

[TS migration] Migrate HTMLEngineProvider component
  • Loading branch information
nkuoch authored Jan 12, 2024
2 parents dcd6f5a + 7c3cfd3 commit 964548a
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -1,88 +1,90 @@
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
import _ from 'underscore';
import type {TextProps} from 'react-native';
import {HTMLContentModel, HTMLElementModel, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
import useThemeStyles from '@hooks/useThemeStyles';
import convertToLTR from '@libs/convertToLTR';
import FontUtils from '@styles/utils/FontUtils';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import * as HTMLEngineUtils from './htmlEngineUtils';
import htmlRenderers from './HTMLRenderers';

const propTypes = {
type BaseHTMLEngineProviderProps = ChildrenProps & {
/** Whether text elements should be selectable */
textSelectable: PropTypes.bool,
textSelectable?: boolean;

/** Handle line breaks according to the HTML standard (default on web) */
enableExperimentalBRCollapsing: PropTypes.bool,

children: PropTypes.node,
};

const defaultProps = {
textSelectable: false,
children: null,
enableExperimentalBRCollapsing: false,
enableExperimentalBRCollapsing?: boolean;
};

// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
// context to RenderHTMLSource components. See https://git.io/JRcZb
// Beware that each prop should be referentialy stable between renders to avoid
// costly invalidations and commits.
function BaseHTMLEngineProvider(props) {
function BaseHTMLEngineProvider({textSelectable = false, children, enableExperimentalBRCollapsing = false}: BaseHTMLEngineProviderProps) {
const styles = useThemeStyles();

// Declare nonstandard tags and their content model here
/* eslint-disable @typescript-eslint/naming-convention */
const customHTMLElementModels = useMemo(
() => ({
edited: defaultHTMLElementModels.span.extend({
edited: HTMLElementModel.fromCustomModel({
tagName: 'edited',
contentModel: HTMLContentModel.textual,
}),
'alert-text': defaultHTMLElementModels.div.extend({
'alert-text': HTMLElementModel.fromCustomModel({
tagName: 'alert-text',
mixedUAStyles: {...styles.formError, ...styles.mb0},
contentModel: HTMLContentModel.block,
}),
'muted-text': defaultHTMLElementModels.div.extend({
'muted-text': HTMLElementModel.fromCustomModel({
tagName: 'muted-text',
mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
contentModel: HTMLContentModel.block,
}),
comment: defaultHTMLElementModels.div.extend({
comment: HTMLElementModel.fromCustomModel({
tagName: 'comment',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.block,
}),
'email-comment': defaultHTMLElementModels.div.extend({
'email-comment': HTMLElementModel.fromCustomModel({
tagName: 'email-comment',
mixedUAStyles: {whiteSpace: 'normal'},
contentModel: HTMLContentModel.block,
}),
strong: defaultHTMLElementModels.span.extend({
strong: HTMLElementModel.fromCustomModel({
tagName: 'strong',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.textual,
}),
'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}),
'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
'next-step': defaultHTMLElementModels.span.extend({
'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
contentModel: HTMLContentModel.textual,
}),
'next-step-email': defaultHTMLElementModels.span.extend({tagName: 'next-step-email'}),
video: defaultHTMLElementModels.div.extend({
'next-step-email': HTMLElementModel.fromCustomModel({tagName: 'next-step-email', contentModel: HTMLContentModel.textual}),
video: HTMLElementModel.fromCustomModel({
tagName: 'video',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.block,
}),
}),
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16],
);
/* eslint-enable @typescript-eslint/naming-convention */

// We need to memoize this prop to make it referentially stable.
const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [props.textSelectable]);
const defaultTextProps: TextProps = useMemo(() => ({selectable: textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [textSelectable]);
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
return (
<TRenderEngineProvider
customHTMLElementModels={customHTMLElementModels}
baseStyle={styles.webViewStyles.baseFontStyle}
tagsStyles={styles.webViewStyles.tagStyles}
enableCSSInlineProcessing={false}
systemFonts={_.values(FontUtils.fontFamily.single)}
systemFonts={Object.values(FontUtils.fontFamily.single)}
domVisitors={{
// eslint-disable-next-line no-param-reassign
onText: (text) => (text.data = convertToLTR(text.data)),
Expand All @@ -91,18 +93,17 @@ function BaseHTMLEngineProvider(props) {
<RenderHTMLConfigProvider
defaultTextProps={defaultTextProps}
defaultViewProps={defaultViewProps}
// @ts-expect-error TODO: Remove this once HTMLRenderers (https://github.com/Expensify/App/issues/25154) is migrated to TypeScript.
renderers={htmlRenderers}
computeEmbeddedMaxWidth={HTMLEngineUtils.computeEmbeddedMaxWidth}
enableExperimentalBRCollapsing={props.enableExperimentalBRCollapsing}
enableExperimentalBRCollapsing={enableExperimentalBRCollapsing}
>
{props.children}
{children}
</RenderHTMLConfigProvider>
</TRenderEngineProvider>
);
}

BaseHTMLEngineProvider.displayName = 'BaseHTMLEngineProvider';
BaseHTMLEngineProvider.propTypes = propTypes;
BaseHTMLEngineProvider.defaultProps = defaultProps;

export default BaseHTMLEngineProvider;
15 changes: 0 additions & 15 deletions src/components/HTMLEngineProvider/htmlEnginePropTypes.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import lodashGet from 'lodash/get';
import type {TNode} from 'react-native-render-html';

type Predicate = (node: TNode) => boolean;

const MAX_IMG_DIMENSIONS = 512;

Expand All @@ -7,12 +9,12 @@ const MAX_IMG_DIMENSIONS = 512;
* is used by the HTML component in the default renderer for img tags to scale
* down images that would otherwise overflow horizontally.
*
* @param {string} tagName - The name of the tag for which max width should be constrained.
* @param {number} contentWidth - The content width provided to the HTML
* @param contentWidth - The content width provided to the HTML
* component.
* @returns {number} The minimum between contentWidth and MAX_IMG_DIMENSIONS
* @param tagName - The name of the tag for which max width should be constrained.
* @returns The minimum between contentWidth and MAX_IMG_DIMENSIONS
*/
function computeEmbeddedMaxWidth(tagName, contentWidth) {
function computeEmbeddedMaxWidth(contentWidth: number, tagName: string): number {
if (tagName === 'img') {
return Math.min(MAX_IMG_DIMENSIONS, contentWidth);
}
Expand All @@ -22,21 +24,15 @@ function computeEmbeddedMaxWidth(tagName, contentWidth) {
/**
* Check if tagName is equal to any of our custom tags wrapping chat comments.
*
* @param {string} tagName
* @returns {Boolean}
*/
function isCommentTag(tagName) {
function isCommentTag(tagName: string): boolean {
return tagName === 'email-comment' || tagName === 'comment';
}

/**
* Check if there is an ancestor node for which the predicate returns true.
*
* @param {TNode} tnode
* @param {Function} predicate
* @returns {Boolean}
*/
function isChildOfNode(tnode, predicate) {
function isChildOfNode(tnode: TNode, predicate: Predicate): boolean {
let currentNode = tnode.parent;
while (currentNode) {
if (predicate(currentNode)) {
Expand All @@ -50,21 +46,17 @@ function isChildOfNode(tnode, predicate) {
/**
* Check if there is an ancestor node with name 'comment'.
* Finding node with name 'comment' flags that we are rendering a comment.
* @param {TNode} tnode
* @returns {Boolean}
*/
function isChildOfComment(tnode) {
return isChildOfNode(tnode, (node) => isCommentTag(lodashGet(node, 'domNode.name', '')));
function isChildOfComment(tnode: TNode): boolean {
return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && isCommentTag(node.domNode.name));
}

/**
* Check if there is an ancestor node with the name 'h1'.
* Finding a node with the name 'h1' flags that we are rendering inside an h1 element.
* @param {TNode} tnode
* @returns {Boolean}
*/
function isChildOfH1(tnode) {
return isChildOfNode(tnode, (node) => lodashGet(node, 'domNode.name', '').toLowerCase() === 'h1');
function isChildOfH1(tnode: TNode): boolean {
return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1');
}

export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1};
22 changes: 0 additions & 22 deletions src/components/HTMLEngineProvider/index.js

This file was deleted.

20 changes: 0 additions & 20 deletions src/components/HTMLEngineProvider/index.native.js

This file was deleted.

11 changes: 11 additions & 0 deletions src/components/HTMLEngineProvider/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';

function HTMLEngineProvider({children}: ChildrenProps) {
return <BaseHTMLEngineProvider enableExperimentalBRCollapsing>{children}</BaseHTMLEngineProvider>;
}

HTMLEngineProvider.displayName = 'HTMLEngineProvider';

export default HTMLEngineProvider;
15 changes: 15 additions & 0 deletions src/components/HTMLEngineProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';

function HTMLEngineProvider({children}: ChildrenProps) {
const {isSmallScreenWidth} = useWindowDimensions();

return <BaseHTMLEngineProvider textSelectable={!DeviceCapabilities.canUseTouchScreen() || !isSmallScreenWidth}>{children}</BaseHTMLEngineProvider>;
}

HTMLEngineProvider.displayName = 'HTMLEngineProvider';

export default HTMLEngineProvider;

0 comments on commit 964548a

Please sign in to comment.