From f1c060bda31cfd825754efa27bfceea04696fe85 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 22 Nov 2023 09:51:52 +0100 Subject: [PATCH 01/14] migrate index.ios.tsx to TypeScript --- .../Composer/{index.ios.js => index.ios.tsx} | 114 ++++++++---------- src/libs/ComposerUtils/types.ts | 7 +- src/types/modules/react-native.d.ts | 5 +- 3 files changed, 60 insertions(+), 66 deletions(-) rename src/components/Composer/{index.ios.js => index.ios.tsx} (57%) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.tsx similarity index 57% rename from src/components/Composer/index.ios.js rename to src/components/Composer/index.ios.tsx index a1b8c1a4ffe6..ddcdd284f17f 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.tsx @@ -1,79 +1,75 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleSheet} from 'react-native'; -import _ from 'underscore'; +import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; +import {StyleProp, StyleSheet, TextInput, TextStyle} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; -import styles from '@styles/styles'; -import themeColors from '@styles/themes/default'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; -const propTypes = { +type ComposerProps = { /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, + shouldClear?: boolean; /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, + onClear?: () => void; /** Set focus to this component the first time it renders. * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, + autoFocus?: boolean; /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), + selection?: { + start: number; + end?: number; + }; /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, + isFullComposerAvailable?: boolean; /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, + maxLines?: number; /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, + setIsFullComposerAvailable?: () => void; /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize?: boolean; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - selection: { - start: 0, - end: 0, - }, - maxLines: undefined, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - style: null, + style: StyleProp; }; -function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { - const textInput = useRef(null); +function Composer( + { + shouldClear = false, + onClear = () => {}, + isDisabled = false, + maxLines, + isComposerFullSize = false, + setIsFullComposerAvailable = () => {}, + autoFocus = false, + isFullComposerAvailable = false, + style, + ...props + }: ComposerProps, + ref: ForwardedRef, +) { + // const textInput = useRef>>(); + const textInput = useRef(); + + const styles = useThemeStyles(); + const themeColors = useTheme(); /** * Set the TextInput Ref * @param {Element} el */ - const setTextInputRef = useCallback((el) => { + const setTextInputRef = useCallback((el: TextInput) => { textInput.current = el; - if (!_.isFunction(forwardedRef) || textInput.current === null) { + if (typeof ref !== 'function' || textInput.current === null) { return; } @@ -81,7 +77,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - forwardedRef(textInput.current); + ref(textInput.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -89,7 +85,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC if (!shouldClear) { return; } - textInput.current.clear(); + textInput.current?.clear(); onClear(); }, [shouldClear, onClear]); @@ -104,14 +100,13 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC return maxLines; }, [isComposerFullSize, maxLines]); - const composerStyles = useMemo(() => { - StyleSheet.flatten(props.style); - }, [props.style]); + const composerStyles = useMemo(() => StyleSheet.flatten(style), [style]); // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. // On Android the selection prop is required on the TextInput but this prop has issues on IOS - const propsToPass = _.omit(props, 'selection'); + const {selection, ...propsToPass} = props; + return ( ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} smartInsertDelete={false} - maxNumberOfLines={maxNumberOfLines} style={[composerStyles, styles.verticalAlignMiddle]} + maxNumberOfLines={maxNumberOfLines} + readOnly={isDisabled} + autoFocus={autoFocus} + isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} - readOnly={isDisabled} /> ); } -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -const ComposerWithRef = React.forwardRef((props, ref) => ( - -)); +Composer.displayName = 'Composer'; -ComposerWithRef.displayName = 'ComposerWithRef'; +const ComposerWithRef = React.forwardRef(Composer); export default ComposerWithRef; diff --git a/src/libs/ComposerUtils/types.ts b/src/libs/ComposerUtils/types.ts index a417d951ff51..41cc4044db7f 100644 --- a/src/libs/ComposerUtils/types.ts +++ b/src/libs/ComposerUtils/types.ts @@ -1,6 +1,9 @@ type ComposerProps = { - isFullComposerAvailable: boolean; - setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + isFullComposerAvailable?: boolean; + setIsFullComposerAvailable?: (isFullComposerAvailable: boolean) => void; + maxLines?: number; + isComposerFullSize?: boolean; + isDisabled?: boolean; }; export default ComposerProps; diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index 5d67da0b885e..7dff63b7dd40 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -283,7 +283,10 @@ declare module 'react-native' { enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; readOnly?: boolean; } - interface TextInputProps extends WebTextInputProps {} + interface TextInputProps extends WebTextInputProps { + smartInsertDelete?: boolean; + isFullComposerAvailable?: boolean; + } /** * Image From 32f4140c36aaef78033c958d28617e614d132883 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 22 Nov 2023 10:14:40 +0100 Subject: [PATCH 02/14] migrate index.android.js to TypeScript --- .../{index.android.js => index.android.tsx} | 106 ++++++++---------- 1 file changed, 47 insertions(+), 59 deletions(-) rename src/components/Composer/{index.android.js => index.android.tsx} (63%) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.tsx similarity index 63% rename from src/components/Composer/index.android.js rename to src/components/Composer/index.android.tsx index 278965cbc81a..8b8b7898d0c5 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.tsx @@ -1,78 +1,74 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleSheet} from 'react-native'; -import _ from 'underscore'; +import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; +import {StyleProp, StyleSheet, TextInput, TextStyle} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; import themeColors from '@styles/themes/default'; -const propTypes = { +type ComposerProps = { /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, + maxLines: number; /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, + shouldClear: boolean; /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, + onClear: () => void; /** Set focus to this component the first time it renders. * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, + autoFocus: boolean; /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, + isDisabled: boolean; /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), + selection: { + start: number; + end?: number; + }; /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, + isFullComposerAvailable: boolean; /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, + setIsFullComposerAvailable: () => void; /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize: boolean; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - selection: { - start: 0, - end: 0, - }, - maxLines: undefined, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - style: null, + style: StyleProp; }; -function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { - const textInput = useRef(null); +function Composer( + { + shouldClear = false, + onClear = () => {}, + isDisabled = false, + maxLines, + isComposerFullSize = false, + setIsFullComposerAvailable = () => {}, + style, + autoFocus = false, + selection = { + start: 0, + end: 0, + }, + isFullComposerAvailable = false, + ...props + }: ComposerProps, + ref: ForwardedRef, +) { + const textInput = useRef(); /** * Set the TextInput Ref * @param {Element} el */ - const setTextInputRef = useCallback((el) => { + const setTextInputRef = useCallback((el: TextInput) => { textInput.current = el; - if (!_.isFunction(forwardedRef) || textInput.current === null) { + if (typeof ref !== 'function' || textInput.current === null) { return; } @@ -80,7 +76,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - forwardedRef(textInput.current); + ref(textInput.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -88,7 +84,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC if (!shouldClear) { return; } - textInput.current.clear(); + textInput.current?.clear(); onClear(); }, [shouldClear, onClear]); @@ -103,9 +99,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC return maxLines; }, [isComposerFullSize, maxLines]); - const composerStyles = useMemo(() => { - StyleSheet.flatten(props.style); - }, [props.style]); + const composerStyles = useMemo(() => StyleSheet.flatten(style), [style]); return ( ); } -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -const ComposerWithRef = React.forwardRef((props, ref) => ( - -)); +Composer.displayName = 'ComposerWithRef'; -ComposerWithRef.displayName = 'ComposerWithRef'; +const ComposerWithRef = React.forwardRef(Composer); export default ComposerWithRef; From ef6cbd67cc7b194ad2660d5cf3b3420da6c424c4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 24 Nov 2023 12:14:05 +0100 Subject: [PATCH 03/14] start migrating index.js --- .../Composer/{index.js => index.tsx} | 260 +++++++++--------- src/styles/StyleUtils.ts | 2 +- 2 files changed, 127 insertions(+), 135 deletions(-) rename src/components/Composer/{index.js => index.tsx} (70%) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.tsx similarity index 70% rename from src/components/Composer/index.js rename to src/components/Composer/index.tsx index cbf8a6e40abd..3b105feb904d 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.tsx @@ -1,16 +1,24 @@ +import {useNavigation} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef, useState, ClipboardEvent} from 'react'; import {flushSync} from 'react-dom'; -import {StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import { + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + TextInputKeyPressEventData, + TextInputProps, + TextInputSelectionChangeEventData, + TextStyle, + View, +} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigation from '@components/withNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import compose from '@libs/compose'; import * as ComposerUtils from '@libs/ComposerUtils'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; @@ -20,110 +28,83 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { +type ComposerProps = { /** Maximum number of lines in the text input */ - maxLines: PropTypes.number, + maxLines?: number; /** The default value of the comment box */ - defaultValue: PropTypes.string, + defaultValue?: string; /** The value of the comment box */ - value: PropTypes.string, + value?: string; /** Number of lines for the comment */ - numberOfLines: PropTypes.number, + numberOfLines?: number; /** Callback method to update number of lines for the comment */ - onNumberOfLinesChange: PropTypes.func, + onNumberOfLinesChange?: (numberOfLines: number) => void; /** Callback method to handle pasting a file */ - onPasteFile: PropTypes.func, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, + onPasteFile?: (file?: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, + style?: StyleProp; /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, + shouldClear?: boolean; /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, + onClear?: () => void; /** Whether or not this TextInput is disabled. */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Set focus to this component the first time it renders. Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, + autoFocus?: boolean; /** Update selection position on change */ - onSelectionChange: PropTypes.func, + onSelectionChange?: (event: NativeSyntheticEvent) => void; /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), + selection?: { + start: number; + end?: number; + }; /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, + isFullComposerAvailable?: boolean; /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, + setIsFullComposerAvailable?: () => void; /** Should we calculate the caret position */ - shouldCalculateCaretPosition: PropTypes.bool, + shouldCalculateCaretPosition?: boolean; /** Function to check whether composer is covered up or not */ - checkComposerVisibility: PropTypes.func, + checkComposerVisibility?: () => boolean; /** Whether this is the report action compose */ - isReportActionCompose: PropTypes.bool, + isReportActionCompose?: boolean; /** Whether the sull composer is open */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize?: boolean; - ...withLocalizePropTypes, -}; + onKeyPress?: (event: NativeSyntheticEvent) => void; -const defaultProps = { - defaultValue: undefined, - value: undefined, - numberOfLines: 0, - onNumberOfLinesChange: () => {}, - maxLines: -1, - onPasteFile: () => {}, - shouldClear: false, - onClear: () => {}, - style: null, - isDisabled: false, - autoFocus: false, - forwardedRef: null, - onSelectionChange: () => {}, - selection: { - start: 0, - end: 0, - }, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - shouldCalculateCaretPosition: false, - checkComposerVisibility: () => false, - isReportActionCompose: false, - isComposerFullSize: false, + onFocus?: (event: NativeSyntheticEvent) => void; }; /** * Retrieves the characters from the specified cursor position up to the next space or new line. * - * @param {string} str - The input string. - * @param {number} cursorPos - The position of the cursor within the input string. - * @returns {string} - The substring from the cursor position up to the next space or new line. + * @param str - The input string. + * @param cursorPos - The position of the cursor within the input string. + * @returns - The substring from the cursor position up to the next space or new line. * If no space or new line is found, returns the substring from the cursor position to the end of the input string. */ -const getNextChars = (str, cursorPos) => { +const getNextChars = (str: string, cursorPos: number) => { // Get the substring starting from the cursor position const substr = str.substring(cursorPos); @@ -140,40 +121,53 @@ const getNextChars = (str, cursorPos) => { // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat -function Composer({ - value, - defaultValue, - maxLines, - onKeyPress, - style, - shouldClear, - autoFocus, - translate, - isFullComposerAvailable, - shouldCalculateCaretPosition, - numberOfLines: numberOfLinesProp, - isDisabled, - forwardedRef, - navigation, - onClear, - onPasteFile, - onSelectionChange, - onNumberOfLinesChange, - setIsFullComposerAvailable, - checkComposerVisibility, - selection: selectionProp, - isReportActionCompose, - isComposerFullSize, - ...props -}) { +function Composer( + { + value, + defaultValue, + maxLines = -1, + onKeyPress = () => {}, + style, + shouldClear = false, + autoFocus = false, + isFullComposerAvailable = false, + shouldCalculateCaretPosition = false, + numberOfLines: numberOfLinesProp = 0, + isDisabled = false, + onClear = () => {}, + onPasteFile = () => {}, + onSelectionChange = () => {}, + onNumberOfLinesChange = () => {}, + setIsFullComposerAvailable = () => {}, + checkComposerVisibility = () => false, + selection: selectionProp = { + start: 0, + end: 0, + }, + isReportActionCompose = false, + isComposerFullSize = false, + ...props + }: ComposerProps, + ref: ForwardedRef>>, +) { + console.log('*** REF ***', ref) const theme = useTheme(); const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); - const textRef = useRef(null); - const textInput = useRef(null); + const navigation = useNavigation(); + // const textRef = useRef(null); + const textRef = useRef(null); + // const textInput = useRef(null); + const textInput = useRef(null); const initialValue = defaultValue ? `${defaultValue}` : `${value || ''}`; const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); - const [selection, setSelection] = useState({ + const [selection, setSelection] = useState< + | { + start: number; + end?: number; + } + | undefined + >({ start: initialValue.length, end: initialValue.length, }); @@ -185,7 +179,7 @@ function Composer({ if (!shouldClear) { return; } - textInput.current.clear(); + textInput.current?.clear(); setNumberOfLines(1); onClear(); }, [shouldClear, onClear]); @@ -193,7 +187,7 @@ function Composer({ useEffect(() => { setSelection((prevSelection) => { if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { - return; + return prevSelection; } return selectionProp; }); @@ -202,20 +196,24 @@ function Composer({ /** * Adds the cursor position to the selection change event. * - * @param {Event} event + * @param event */ - const addCursorPositionToSelectionChange = (event) => { + // const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { + // const addCursorPositionToSelectionChange = (event: BaseSyntheticEvent) => { + // const addCursorPositionToSelectionChange = (event: SyntheticEvent) => { + // const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { + const addCursorPositionToSelectionChange = (event: React.ChangeEvent & NativeSyntheticEvent) => { if (shouldCalculateCaretPosition) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); - setCaretContent(getNextChars(value, event.nativeEvent.selection.start)); + setCaretContent(getNextChars(value ?? '', event.nativeEvent.selection.start)); }); const selectionValue = { start: event.nativeEvent.selection.start, end: event.nativeEvent.selection.end, - positionX: textRef.current.offsetLeft - CONST.SPACE_CHARACTER_WIDTH, - positionY: textRef.current.offsetTop, + positionX: (textRef.current?.offsetLeft ?? 0) - CONST.SPACE_CHARACTER_WIDTH, + positionY: textRef.current?.offsetTop, }; onSelectionChange({ nativeEvent: { @@ -233,12 +231,12 @@ function Composer({ * Set pasted text to clipboard * @param {String} text */ - const paste = useCallback((text) => { + const paste = useCallback((text?: string) => { try { document.execCommand('insertText', false, text); // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. - textInput.current.blur(); - textInput.current.focus(); + textInput.current?.blur(); + textInput.current?.focus(); // eslint-disable-next-line no-empty } catch (e) {} }, []); @@ -249,7 +247,7 @@ function Composer({ * @param {String} html - pasted HTML */ const handlePastedHTML = useCallback( - (html) => { + (html: string) => { const parser = new ExpensiMark(); paste(parser.htmlToMarkdown(html)); }, @@ -262,8 +260,8 @@ function Composer({ * @param {ClipboardEvent} event */ const handlePastePlainText = useCallback( - (event) => { - const plainText = event.clipboardData.getData('text/plain'); + (event: ClipboardEvent) => { + const plainText = event.clipboardData?.getData('text/plain'); paste(plainText); }, [paste], @@ -276,9 +274,11 @@ function Composer({ * @param {ClipboardEvent} event */ const handlePaste = useCallback( - (event) => { + // (event: ClipboardEvent) => { + (event: unknown) => { + console.log('*** PASTE EVENT ***', event); const isVisible = checkComposerVisibility(); - const isFocused = textInput.current.isFocused(); + const isFocused = textInput.current?.isFocused(); if (!(isVisible || isFocused)) { return; @@ -287,29 +287,28 @@ function Composer({ if (textInput.current !== event.target) { // To make sure the composer does not capture paste events from other inputs, we check where the event originated // If it did originate in another input, we return early to prevent the composer from handling the paste - const isTargetInput = event.target.nodeName === 'INPUT' || event.target.nodeName === 'TEXTAREA' || event.target.contentEditable === 'true'; + const isTargetInput = event.target?.nodeName === 'INPUT' || event.target?.nodeName === 'TEXTAREA' || event.target?.contentEditable === 'true'; if (isTargetInput) { return; } - textInput.current.focus(); + textInput.current?.focus(); } event.preventDefault(); - const {files, types} = event.clipboardData; const TEXT_HTML = 'text/html'; // If paste contains files, then trigger file management - if (files.length > 0) { + if (event.clipboardData?.files.length && event.clipboardData.files.length > 0) { // Prevent the default so we do not post the file name into the text box - onPasteFile(event.clipboardData.files[0]); + onPasteFile(event.clipboardData?.files[0]); return; } // If paste contains HTML - if (types.includes(TEXT_HTML)) { - const pastedHTML = event.clipboardData.getData(TEXT_HTML); + if (event.clipboardData?.types.includes(TEXT_HTML)) { + const pastedHTML = event.clipboardData?.getData(TEXT_HTML); const domparser = new DOMParser(); const embeddedImages = domparser.parseFromString(pastedHTML, TEXT_HTML).images; @@ -339,7 +338,7 @@ function Composer({ return; } // we reset the height to 0 to get the correct scrollHeight - textInput.current.style.height = 0; + textInput.current.style.height = '0'; const computedStyle = window.getComputedStyle(textInput.current); const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); @@ -365,8 +364,8 @@ function Composer({ const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); - if (_.isFunction(forwardedRef)) { - forwardedRef(textInput.current); + if (typeof ref === 'function') { + ref(textInput.current); } if (textInput.current) { @@ -385,7 +384,7 @@ function Composer({ }, []); const handleKeyPress = useCallback( - (e) => { + (e: NativeSyntheticEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed if (!onKeyPress || isEnterWhileComposition(e)) { return; @@ -418,7 +417,8 @@ function Composer({ ); - const inputStyleMemo = useMemo( + // const inputStyleMemo: StyleProp>> = useMemo( + const inputStyleMemo: StyleProp = useMemo( () => [ // We are hiding the scrollbar to prevent it from reducing the text input width, // so we can get the correct scroll height while calculating the number of lines. @@ -438,11 +438,13 @@ function Composer({ autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - ref={(el) => (textInput.current = el)} + // ref={(el) => (textInput.current = el)} + // ref={textInput} + ref={ref} selection={selection} style={inputStyleMemo} value={value} - forwardedRef={forwardedRef} + // forwardedRef={ref} defaultValue={defaultValue} autoFocus={autoFocus} /* eslint-disable-next-line react/jsx-props-no-spreading */ @@ -459,7 +461,7 @@ function Composer({ textInput.current.focus(); }); - if (props.onFocus) { + if (props?.onFocus) { props.onFocus(e); } }} @@ -469,18 +471,8 @@ function Composer({ ); } -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; Composer.displayName = 'Composer'; -const ComposerWithRef = React.forwardRef((props, ref) => ( - -)); - -ComposerWithRef.displayName = 'ComposerWithRef'; +const ComposerWithRef = React.forwardRef(Composer); -export default compose(withLocalize, withNavigation)(ComposerWithRef); +export default ComposerWithRef; diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index e93d88c3eb53..eadb6a730b60 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1178,7 +1178,7 @@ function getMentionTextColor(isOurMention: boolean): string { /** * Returns padding vertical based on number of lines */ -function getComposeTextAreaPadding(numberOfLines: number, isComposerFullSize: boolean): ViewStyle { +function getComposeTextAreaPadding(numberOfLines: number, isComposerFullSize: boolean): ViewStyle & TextStyle { let paddingValue = 5; // Issue #26222: If isComposerFullSize paddingValue will always be 5 to prevent padding jumps when adding multiple lines. if (!isComposerFullSize) { From bc23a016a8a8462aa998759ef729e238895649da Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 24 Nov 2023 13:22:03 +0100 Subject: [PATCH 04/14] fix forwarded ref --- src/components/Composer/index.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3b105feb904d..c1df36c380f6 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,6 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef, useState, ClipboardEvent} from 'react'; +import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef, useState, CLipboardEvent} from 'react'; import {flushSync} from 'react-dom'; import { NativeSyntheticEvent, @@ -150,7 +150,6 @@ function Composer( }: ComposerProps, ref: ForwardedRef>>, ) { - console.log('*** REF ***', ref) const theme = useTheme(); const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); @@ -158,8 +157,9 @@ function Composer( // const textRef = useRef(null); const textRef = useRef(null); // const textInput = useRef(null); - const textInput = useRef(null); - const initialValue = defaultValue ? `${defaultValue}` : `${value || ''}`; + // const textInput = useRef(null); + const textInput = useRef(); + const initialValue = defaultValue ? `${defaultValue}` : `${value ?? ''}`; const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { @@ -275,7 +275,7 @@ function Composer( */ const handlePaste = useCallback( // (event: ClipboardEvent) => { - (event: unknown) => { + (event: any) => { console.log('*** PASTE EVENT ***', event); const isVisible = checkComposerVisibility(); const isFocused = textInput.current?.isFocused(); @@ -438,13 +438,10 @@ function Composer( autoComplete="off" autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={theme.placeholderText} - // ref={(el) => (textInput.current = el)} - // ref={textInput} - ref={ref} + ref={(el: TextInput & HTMLTextAreaElement) => (textInput.current = el)} selection={selection} style={inputStyleMemo} value={value} - // forwardedRef={ref} defaultValue={defaultValue} autoFocus={autoFocus} /* eslint-disable-next-line react/jsx-props-no-spreading */ From 3b27bf8fe079622739f6992c32f261e6127809f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 24 Nov 2023 10:34:59 -0300 Subject: [PATCH 05/14] Fixes in refs and events --- src/components/Composer/index.tsx | 50 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index c1df36c380f6..4204f4615148 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -1,9 +1,11 @@ import {useNavigation} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef, useState, CLipboardEvent} from 'react'; +import React, {BaseSyntheticEvent, ForwardedRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; import { + DimensionValue, NativeSyntheticEvent, + Text as RNText, StyleProp, StyleSheet, TextInput, @@ -154,10 +156,7 @@ function Composer( const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const navigation = useNavigation(); - // const textRef = useRef(null); - const textRef = useRef(null); - // const textInput = useRef(null); - // const textInput = useRef(null); + const textRef = useRef(null); const textInput = useRef(); const initialValue = defaultValue ? `${defaultValue}` : `${value ?? ''}`; const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); @@ -198,32 +197,33 @@ function Composer( * * @param event */ - // const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { - // const addCursorPositionToSelectionChange = (event: BaseSyntheticEvent) => { - // const addCursorPositionToSelectionChange = (event: SyntheticEvent) => { - // const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { - const addCursorPositionToSelectionChange = (event: React.ChangeEvent & NativeSyntheticEvent) => { + const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { + const webEvent = event as BaseSyntheticEvent; + if (shouldCalculateCaretPosition) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { - setValueBeforeCaret(event.target.value.slice(0, event.nativeEvent.selection.start)); - setCaretContent(getNextChars(value ?? '', event.nativeEvent.selection.start)); + setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); + setCaretContent(getNextChars(value ?? '', webEvent.nativeEvent.selection.start)); }); const selectionValue = { - start: event.nativeEvent.selection.start, - end: event.nativeEvent.selection.end, + start: webEvent.nativeEvent.selection.start, + end: webEvent.nativeEvent.selection.end, positionX: (textRef.current?.offsetLeft ?? 0) - CONST.SPACE_CHARACTER_WIDTH, positionY: textRef.current?.offsetTop, }; + onSelectionChange({ + ...webEvent, nativeEvent: { + ...webEvent.nativeEvent, selection: selectionValue, }, }); setSelection(selectionValue); } else { - onSelectionChange(event); - setSelection(event.nativeEvent.selection); + onSelectionChange(webEvent); + setSelection(webEvent.nativeEvent.selection); } }; @@ -270,13 +270,9 @@ function Composer( /** * Check the paste event for an attachment, parse the data and call onPasteFile from props with the selected file, * Otherwise, convert pasted HTML to Markdown and set it on the composer. - * - * @param {ClipboardEvent} event */ const handlePaste = useCallback( - // (event: ClipboardEvent) => { - (event: any) => { - console.log('*** PASTE EVENT ***', event); + (event: ClipboardEvent) => { const isVisible = checkComposerVisibility(); const isFocused = textInput.current?.isFocused(); @@ -285,9 +281,11 @@ function Composer( } if (textInput.current !== event.target) { + const eventTarget = event.target as HTMLInputElement | HTMLTextAreaElement | null; + // To make sure the composer does not capture paste events from other inputs, we check where the event originated // If it did originate in another input, we return early to prevent the composer from handling the paste - const isTargetInput = event.target?.nodeName === 'INPUT' || event.target?.nodeName === 'TEXTAREA' || event.target?.contentEditable === 'true'; + const isTargetInput = eventTarget?.nodeName === 'INPUT' || eventTarget?.nodeName === 'TEXTAREA' || eventTarget?.contentEditable === 'true'; if (isTargetInput) { return; } @@ -334,7 +332,7 @@ function Composer( * divide by line height to get the total number of rows for the textarea. */ const updateNumberOfLines = useCallback(() => { - if (textInput.current === null) { + if (!textInput.current) { return; } // we reset the height to 0 to get the correct scrollHeight @@ -364,7 +362,7 @@ function Composer( const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); - if (typeof ref === 'function') { + if (typeof ref === 'function' && textInput.current) { ref(textInput.current); } @@ -386,7 +384,7 @@ function Composer( const handleKeyPress = useCallback( (e: NativeSyntheticEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed - if (!onKeyPress || isEnterWhileComposition(e)) { + if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; } onKeyPress(e); @@ -404,7 +402,7 @@ function Composer( > {`${valueBeforeCaret} `} Date: Mon, 27 Nov 2023 14:40:18 +0100 Subject: [PATCH 06/14] remove unsupported multiline prop --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4204f4615148..589cca48e34c 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -401,7 +401,7 @@ function Composer( }} > {`${valueBeforeCaret} `} From dc94cd73aaacd4128605068bce03d2981cbae9ae Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 27 Nov 2023 15:17:57 +0100 Subject: [PATCH 07/14] remove mutliline prop --- src/components/Composer/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 589cca48e34c..6bf3fa11de60 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -400,10 +400,7 @@ function Composer( opacity: 0, }} > - + {`${valueBeforeCaret} `} Date: Mon, 27 Nov 2023 15:41:53 +0100 Subject: [PATCH 08/14] cleanup the code --- src/components/Composer/index.android.tsx | 4 +--- src/components/Composer/index.ios.tsx | 4 ---- src/components/Composer/index.tsx | 8 -------- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index 8b8b7898d0c5..cf10fd2d46d3 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -37,7 +37,6 @@ type ComposerProps = { isComposerFullSize: boolean; /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types style: StyleProp; }; @@ -90,7 +89,6 @@ function Composer( /** * Set maximum number of lines - * @return {Number} */ const maxNumberOfLines = useMemo(() => { if (isComposerFullSize) { @@ -125,7 +123,7 @@ function Composer( ); } -Composer.displayName = 'ComposerWithRef'; +Composer.displayName = 'Composer'; const ComposerWithRef = React.forwardRef(Composer); diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index ddcdd284f17f..b696fcc1a5eb 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -38,7 +38,6 @@ type ComposerProps = { isComposerFullSize?: boolean; /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types style: StyleProp; }; @@ -57,7 +56,6 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - // const textInput = useRef>>(); const textInput = useRef(); const styles = useThemeStyles(); @@ -65,7 +63,6 @@ function Composer( /** * Set the TextInput Ref - * @param {Element} el */ const setTextInputRef = useCallback((el: TextInput) => { textInput.current = el; @@ -91,7 +88,6 @@ function Composer( /** * Set maximum number of lines - * @return {Number} */ const maxNumberOfLines = useMemo(() => { if (isComposerFullSize) { diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 6bf3fa11de60..6ba1d0b9dc8c 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -194,8 +194,6 @@ function Composer( /** * Adds the cursor position to the selection change event. - * - * @param event */ const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent) => { const webEvent = event as BaseSyntheticEvent; @@ -229,7 +227,6 @@ function Composer( /** * Set pasted text to clipboard - * @param {String} text */ const paste = useCallback((text?: string) => { try { @@ -243,8 +240,6 @@ function Composer( /** * Manually place the pasted HTML into Composer - * - * @param {String} html - pasted HTML */ const handlePastedHTML = useCallback( (html: string) => { @@ -256,8 +251,6 @@ function Composer( /** * Paste the plaintext content into Composer. - * - * @param {ClipboardEvent} event */ const handlePastePlainText = useCallback( (event: ClipboardEvent) => { @@ -412,7 +405,6 @@ function Composer( ); - // const inputStyleMemo: StyleProp>> = useMemo( const inputStyleMemo: StyleProp = useMemo( () => [ // We are hiding the scrollbar to prevent it from reducing the text input width, From 205469a752957d27fa3f87d1de99c0f2060ccea3 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 28 Nov 2023 08:39:08 +0100 Subject: [PATCH 09/14] apply suggested improvements --- src/components/Composer/index.android.tsx | 11 +--- src/components/Composer/index.ios.tsx | 43 +------------ src/components/Composer/index.tsx | 76 +---------------------- src/components/Composer/types.ts | 73 ++++++++++++++++++++++ src/styles/StyleUtils.ts | 2 +- 5 files changed, 83 insertions(+), 122 deletions(-) create mode 100644 src/components/Composer/types.ts diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index cf10fd2d46d3..e0d2027b0efb 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -3,6 +3,7 @@ import {StyleProp, StyleSheet, TextInput, TextStyle} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; import themeColors from '@styles/themes/default'; +import {TextSelection} from './types'; type ComposerProps = { /** Maximum number of lines in the text input */ @@ -22,10 +23,7 @@ type ComposerProps = { isDisabled: boolean; /** Selection Object */ - selection: { - start: number; - end?: number; - }; + selection: TextSelection; /** Whether the full composer can be opened */ isFullComposerAvailable: boolean; @@ -63,7 +61,6 @@ function Composer( /** * Set the TextInput Ref - * @param {Element} el */ const setTextInputRef = useCallback((el: TextInput) => { textInput.current = el; @@ -125,6 +122,4 @@ function Composer( Composer.displayName = 'Composer'; -const ComposerWithRef = React.forwardRef(Composer); - -export default ComposerWithRef; +export default React.forwardRef(Composer); diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index b696fcc1a5eb..3d18b7357976 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -1,45 +1,10 @@ import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleProp, StyleSheet, TextInput, TextStyle} from 'react-native'; +import {StyleSheet, TextInput} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; - -type ComposerProps = { - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear?: boolean; - - /** When the input has cleared whoever owns this input should know about it */ - onClear?: () => void; - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus?: boolean; - - /** Prevent edits and interactions like focus for this input. */ - isDisabled?: boolean; - - /** Selection Object */ - selection?: { - start: number; - end?: number; - }; - - /** Whether the full composer can be opened */ - isFullComposerAvailable?: boolean; - - /** Maximum number of lines in the text input */ - maxLines?: number; - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable?: () => void; - - /** Whether the composer is full size */ - isComposerFullSize?: boolean; - - /** General styles to apply to the text input */ - style: StyleProp; -}; +import {ComposerProps} from './types'; function Composer( { @@ -124,6 +89,4 @@ function Composer( Composer.displayName = 'Composer'; -const ComposerWithRef = React.forwardRef(Composer); - -export default ComposerWithRef; +export default React.forwardRef(Composer); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 6ba1d0b9dc8c..93b4ef6e380e 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -9,7 +9,6 @@ import { StyleProp, StyleSheet, TextInput, - TextInputFocusEventData, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData, @@ -29,74 +28,7 @@ import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; - -type ComposerProps = { - /** Maximum number of lines in the text input */ - maxLines?: number; - - /** The default value of the comment box */ - defaultValue?: string; - - /** The value of the comment box */ - value?: string; - - /** Number of lines for the comment */ - numberOfLines?: number; - - /** Callback method to update number of lines for the comment */ - onNumberOfLinesChange?: (numberOfLines: number) => void; - - /** Callback method to handle pasting a file */ - onPasteFile?: (file?: File) => void; - - /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types - style?: StyleProp; - - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear?: boolean; - - /** When the input has cleared whoever owns this input should know about it */ - onClear?: () => void; - - /** Whether or not this TextInput is disabled. */ - isDisabled?: boolean; - - /** Set focus to this component the first time it renders. - Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus?: boolean; - - /** Update selection position on change */ - onSelectionChange?: (event: NativeSyntheticEvent) => void; - - /** Selection Object */ - selection?: { - start: number; - end?: number; - }; - - /** Whether the full composer can be opened */ - isFullComposerAvailable?: boolean; - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable?: () => void; - - /** Should we calculate the caret position */ - shouldCalculateCaretPosition?: boolean; - - /** Function to check whether composer is covered up or not */ - checkComposerVisibility?: () => boolean; - - /** Whether this is the report action compose */ - isReportActionCompose?: boolean; - - /** Whether the sull composer is open */ - isComposerFullSize?: boolean; - - onKeyPress?: (event: NativeSyntheticEvent) => void; - - onFocus?: (event: NativeSyntheticEvent) => void; -}; +import {ComposerProps} from './types'; /** * Retrieves the characters from the specified cursor position up to the next space or new line. @@ -106,7 +38,7 @@ type ComposerProps = { * @returns - The substring from the cursor position up to the next space or new line. * If no space or new line is found, returns the substring from the cursor position to the end of the input string. */ -const getNextChars = (str: string, cursorPos: number) => { +const getNextChars = (str: string, cursorPos: number): string => { // Get the substring starting from the cursor position const substr = str.substring(cursorPos); @@ -457,6 +389,4 @@ function Composer( Composer.displayName = 'Composer'; -const ComposerWithRef = React.forwardRef(Composer); - -export default ComposerWithRef; +export default React.forwardRef(Composer); diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts new file mode 100644 index 000000000000..6d1e75fc7882 --- /dev/null +++ b/src/components/Composer/types.ts @@ -0,0 +1,73 @@ +import {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; + +type TextSelection = { + start: number; + end?: number; +}; + +type ComposerProps = { + /** Maximum number of lines in the text input */ + maxLines?: number; + + /** The default value of the comment box */ + defaultValue?: string; + + /** The value of the comment box */ + value?: string; + + /** Number of lines for the comment */ + numberOfLines?: number; + + /** Callback method to update number of lines for the comment */ + onNumberOfLinesChange?: (numberOfLines: number) => void; + + /** Callback method to handle pasting a file */ + onPasteFile?: (file?: File) => void; + + /** General styles to apply to the text input */ + // eslint-disable-next-line react/forbid-prop-types + style?: StyleProp; + + /** If the input should clear, it actually gets intercepted instead of .clear() */ + shouldClear?: boolean; + + /** When the input has cleared whoever owns this input should know about it */ + onClear?: () => void; + + /** Whether or not this TextInput is disabled. */ + isDisabled?: boolean; + + /** Set focus to this component the first time it renders. + Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ + autoFocus?: boolean; + + /** Update selection position on change */ + onSelectionChange?: (event: NativeSyntheticEvent) => void; + + /** Selection Object */ + selection?: TextSelection; + + /** Whether the full composer can be opened */ + isFullComposerAvailable?: boolean; + + /** Allow the full composer to be opened */ + setIsFullComposerAvailable?: () => void; + + /** Should we calculate the caret position */ + shouldCalculateCaretPosition?: boolean; + + /** Function to check whether composer is covered up or not */ + checkComposerVisibility?: () => boolean; + + /** Whether this is the report action compose */ + isReportActionCompose?: boolean; + + /** Whether the sull composer is open */ + isComposerFullSize?: boolean; + + onKeyPress?: (event: NativeSyntheticEvent) => void; + + onFocus?: (event: NativeSyntheticEvent) => void; +}; + +export type {TextSelection, ComposerProps}; diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index bce4756dd883..5f8984775231 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1179,7 +1179,7 @@ function getMentionTextColor(isOurMention: boolean): string { /** * Returns padding vertical based on number of lines */ -function getComposeTextAreaPadding(numberOfLines: number, isComposerFullSize: boolean): ViewStyle & TextStyle { +function getComposeTextAreaPadding(numberOfLines: number, isComposerFullSize: boolean): TextStyle { let paddingValue = 5; // Issue #26222: If isComposerFullSize paddingValue will always be 5 to prevent padding jumps when adding multiple lines. if (!isComposerFullSize) { From d70b49dd466b341a93b9986ea4227966f0a3221f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 28 Nov 2023 11:07:50 +0100 Subject: [PATCH 10/14] apply improvements, unify ComposerProps type --- src/components/Composer/index.android.tsx | 41 ++----------------- src/components/Composer/index.ios.tsx | 4 +- src/components/Composer/index.tsx | 4 +- src/components/Composer/types.ts | 2 +- src/libs/ComposerUtils/types.ts | 9 ---- .../updateIsFullComposerAvailable.ts | 4 +- .../updateNumberOfLines/types.ts | 2 +- src/types/modules/react-native.d.ts | 1 + 8 files changed, 13 insertions(+), 54 deletions(-) delete mode 100644 src/libs/ComposerUtils/types.ts diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index e0d2027b0efb..29aca7de6242 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -1,42 +1,9 @@ import React, {ForwardedRef, useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleProp, StyleSheet, TextInput, TextStyle} from 'react-native'; +import {StyleSheet, TextInput} from 'react-native'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; import themeColors from '@styles/themes/default'; -import {TextSelection} from './types'; - -type ComposerProps = { - /** Maximum number of lines in the text input */ - maxLines: number; - - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: boolean; - - /** When the input has cleared whoever owns this input should know about it */ - onClear: () => void; - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: boolean; - - /** Prevent edits and interactions like focus for this input. */ - isDisabled: boolean; - - /** Selection Object */ - selection: TextSelection; - - /** Whether the full composer can be opened */ - isFullComposerAvailable: boolean; - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable: () => void; - - /** Whether the composer is full size */ - isComposerFullSize: boolean; - - /** General styles to apply to the text input */ - style: StyleProp; -}; +import {ComposerProps} from './types'; function Composer( { @@ -57,7 +24,7 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - const textInput = useRef(); + const textInput = useRef(null); /** * Set the TextInput Ref @@ -110,12 +77,12 @@ function Composer( maxNumberOfLines={maxNumberOfLines} textAlignVertical="center" style={[composerStyles]} - readOnly={isDisabled} autoFocus={autoFocus} selection={selection} isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} + readOnly={isDisabled} /> ); } diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index 3d18b7357976..5f435d4e5109 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -21,7 +21,7 @@ function Composer( }: ComposerProps, ref: ForwardedRef, ) { - const textInput = useRef(); + const textInput = useRef(null); const styles = useThemeStyles(); const themeColors = useTheme(); @@ -78,11 +78,11 @@ function Composer( smartInsertDelete={false} style={[composerStyles, styles.verticalAlignMiddle]} maxNumberOfLines={maxNumberOfLines} - readOnly={isDisabled} autoFocus={autoFocus} isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} + readOnly={isDisabled} /> ); } diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 93b4ef6e380e..80df88b2dc3a 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -89,7 +89,7 @@ function Composer( const {windowWidth} = useWindowDimensions(); const navigation = useNavigation(); const textRef = useRef(null); - const textInput = useRef(); + const textInput = useRef<(HTMLTextAreaElement & TextInput) | null>(null); const initialValue = defaultValue ? `${defaultValue}` : `${value ?? ''}`; const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< @@ -118,7 +118,7 @@ function Composer( useEffect(() => { setSelection((prevSelection) => { if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { - return prevSelection; + return; } return selectionProp; }); diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 6d1e75fc7882..bce777a87287 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -51,7 +51,7 @@ type ComposerProps = { isFullComposerAvailable?: boolean; /** Allow the full composer to be opened */ - setIsFullComposerAvailable?: () => void; + setIsFullComposerAvailable?: (value: boolean) => void; /** Should we calculate the caret position */ shouldCalculateCaretPosition?: boolean; diff --git a/src/libs/ComposerUtils/types.ts b/src/libs/ComposerUtils/types.ts deleted file mode 100644 index 41cc4044db7f..000000000000 --- a/src/libs/ComposerUtils/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -type ComposerProps = { - isFullComposerAvailable?: boolean; - setIsFullComposerAvailable?: (isFullComposerAvailable: boolean) => void; - maxLines?: number; - isComposerFullSize?: boolean; - isDisabled?: boolean; -}; - -export default ComposerProps; diff --git a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts index 761abb8c9c8f..64c526484760 100644 --- a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts +++ b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts @@ -1,5 +1,5 @@ +import {ComposerProps} from '@components/Composer/types'; import CONST from '@src/CONST'; -import ComposerProps from './types'; /** * Update isFullComposerAvailable if needed @@ -8,7 +8,7 @@ import ComposerProps from './types'; function updateIsFullComposerAvailable(props: ComposerProps, numberOfLines: number) { const isFullComposerAvailable = numberOfLines >= CONST.COMPOSER.FULL_COMPOSER_MIN_LINES; if (isFullComposerAvailable !== props.isFullComposerAvailable) { - props.setIsFullComposerAvailable(isFullComposerAvailable); + props.setIsFullComposerAvailable?.(isFullComposerAvailable); } } diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts index b0f9ba48ddc2..c582ef499a7e 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/types.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/types.ts @@ -1,5 +1,5 @@ import {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; -import ComposerProps from '@libs/ComposerUtils/types'; +import {ComposerProps} from '@components/Composer/types'; type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent) => void; diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index 7dff63b7dd40..27cc16b912d5 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -284,6 +284,7 @@ declare module 'react-native' { readOnly?: boolean; } interface TextInputProps extends WebTextInputProps { + // TODO: remove once the app is updated to RN 0.73 smartInsertDelete?: boolean; isFullComposerAvailable?: boolean; } From 40de00cfd6c7a1fa5639a4fedfe6bb1a393245a6 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 5 Dec 2023 08:49:01 +0100 Subject: [PATCH 11/14] remove ref from if statement --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 8d020e084dc1..cb33bcc7e6aa 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -288,7 +288,7 @@ function Composer( const unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste)); const unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste)); - if (typeof ref === 'function' && textInput.current) { + if (typeof ref === 'function') { ref(textInput.current); } From 76c733439f6d81b1605cdbcc2ea0d3b415658294 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 5 Dec 2023 09:08:19 +0100 Subject: [PATCH 12/14] add optional chaining to onFocus call --- src/components/Composer/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index cb33bcc7e6aa..913a5f5a235e 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -378,9 +378,8 @@ function Composer( textInput.current.focus(); }); - if (props?.onFocus) { - props.onFocus(e); - } + + props.onFocus?.(e); }} /> {shouldCalculateCaretPosition && renderElementForCaretPosition} From 0a91ea7c204e5552193ad3413d97a38dfae24dbf Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 11 Dec 2023 08:18:52 +0100 Subject: [PATCH 13/14] move selection destructuring to the top --- src/components/Composer/index.ios.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index cc5067ce7b08..2f320e6657f0 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -17,6 +17,10 @@ function Composer( autoFocus = false, isFullComposerAvailable = false, style, + // On native layers we like to have the Text Input not focused so the + // user can read new chats without the keyboard in the way of the view. + // On Android the selection prop is required on the TextInput but this prop has issues on IOS + selection, ...props }: ComposerProps, ref: ForwardedRef, @@ -63,11 +67,6 @@ function Composer( const composerStyles = useMemo(() => StyleSheet.flatten(style), [style]); - // On native layers we like to have the Text Input not focused so the - // user can read new chats without the keyboard in the way of the view. - // On Android the selection prop is required on the TextInput but this prop has issues on IOS - const {selection, ...propsToPass} = props; - return ( ); From 1c26c3e1324146b3b3b4bc8370836ff9df0e3754 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 18 Dec 2023 13:51:03 +0100 Subject: [PATCH 14/14] change variable names --- src/components/Composer/index.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 5e14717a241c..4ff5c6dbd75f 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -22,24 +22,24 @@ import {ComposerProps} from './types'; /** * Retrieves the characters from the specified cursor position up to the next space or new line. * - * @param str - The input string. - * @param cursorPos - The position of the cursor within the input string. + * @param inputString - The input string. + * @param cursorPosition - The position of the cursor within the input string. * @returns - The substring from the cursor position up to the next space or new line. * If no space or new line is found, returns the substring from the cursor position to the end of the input string. */ -const getNextChars = (str: string, cursorPos: number): string => { +const getNextChars = (inputString: string, cursorPosition: number): string => { // Get the substring starting from the cursor position - const substr = str.substring(cursorPos); + const subString = inputString.substring(cursorPosition); // Find the index of the next space or new line character - const spaceIndex = substr.search(/[ \n]/); + const spaceIndex = subString.search(/[ \n]/); if (spaceIndex === -1) { - return substr; + return subString; } // If there is a space or new line, return the substring up to the space or new line - return substr.substring(0, spaceIndex); + return subString.substring(0, spaceIndex); }; // Enable Markdown parsing. @@ -89,8 +89,6 @@ function Composer( } | undefined >({ - // start: initialValue.length, - // end: initialValue.length, start: selectionProp.start, end: selectionProp.end, });