diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ddab159714fc..74c3a86a18c7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1297,25 +1297,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.12.2): - - glog - - hermes-engine - - RCT-Folly (= 2022.05.16.00) - - RCTRequired - - RCTTypeSafety - - React-Codegen - - React-Core - - React-debug - - React-Fabric - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - react-native-launch-arguments (4.0.2): - React - react-native-netinfo (11.2.1): @@ -2150,7 +2131,6 @@ DEPENDENCIES: - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) - - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -2349,8 +2329,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-image-picker" react-native-key-command: :path: "../node_modules/react-native-key-command" - react-native-keyboard-controller: - :path: "../node_modules/react-native-keyboard-controller" react-native-launch-arguments: :path: "../node_modules/react-native-launch-arguments" react-native-netinfo: @@ -2557,7 +2535,6 @@ SPEC CHECKSUMS: react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 - react-native-keyboard-controller: 47c01b0741ae5fc84e53cf282e61cfa5c2edb19b react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa diff --git a/jest/setup.ts b/jest/setup.ts index f11a8a4ed631..416306ce8426 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -53,6 +53,3 @@ jest.mock('react-native-sound', () => { jest.mock('react-native-share', () => ({ default: jest.fn(), })); - -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); diff --git a/package-lock.json b/package-lock.json index 0c7c6d2a11ce..2555138f9545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,6 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", @@ -31892,16 +31891,6 @@ "version": "5.0.1", "license": "MIT" }, - "node_modules/react-native-keyboard-controller": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.12.2.tgz", - "integrity": "sha512-10Sy0+neSHGJxOmOxrUJR8TQznnrQ+jTFQtM1PP6YnblNQeAw1eOa+lO6YLGenRr5WuNSMZbks/3Ay0e2yMKLw==", - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-reanimated": ">=2.3.0" - } - }, "node_modules/react-native-launch-arguments": { "version": "4.0.2", "license": "MIT", diff --git a/package.json b/package.json index 313d9c169a61..e30e0142bcbe 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,6 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "^1.12.2", "react-native-launch-arguments": "^4.0.2", "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", diff --git a/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch b/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch deleted file mode 100644 index 1a5b4c40477b..000000000000 --- a/patches/react-native+0.73.4+016+iOS-textinput-onscroll-event.patch +++ /dev/null @@ -1,70 +0,0 @@ -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -index 88ae3f3..497569a 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp -@@ -36,6 +36,54 @@ static jsi::Value textInputMetricsPayload( - return payload; - }; - -+static jsi::Value textInputMetricsScrollPayload( -+ jsi::Runtime& runtime, -+ const TextInputMetrics& textInputMetrics) { -+ auto payload = jsi::Object(runtime); -+ -+ { -+ auto contentOffset = jsi::Object(runtime); -+ contentOffset.setProperty(runtime, "x", textInputMetrics.contentOffset.x); -+ contentOffset.setProperty(runtime, "y", textInputMetrics.contentOffset.y); -+ payload.setProperty(runtime, "contentOffset", contentOffset); -+ } -+ -+ { -+ auto contentInset = jsi::Object(runtime); -+ contentInset.setProperty(runtime, "top", textInputMetrics.contentInset.top); -+ contentInset.setProperty( -+ runtime, "left", textInputMetrics.contentInset.left); -+ contentInset.setProperty( -+ runtime, "bottom", textInputMetrics.contentInset.bottom); -+ contentInset.setProperty( -+ runtime, "right", textInputMetrics.contentInset.right); -+ payload.setProperty(runtime, "contentInset", contentInset); -+ } -+ -+ { -+ auto contentSize = jsi::Object(runtime); -+ contentSize.setProperty( -+ runtime, "width", textInputMetrics.contentSize.width); -+ contentSize.setProperty( -+ runtime, "height", textInputMetrics.contentSize.height); -+ payload.setProperty(runtime, "contentSize", contentSize); -+ } -+ -+ { -+ auto layoutMeasurement = jsi::Object(runtime); -+ layoutMeasurement.setProperty( -+ runtime, "width", textInputMetrics.layoutMeasurement.width); -+ layoutMeasurement.setProperty( -+ runtime, "height", textInputMetrics.layoutMeasurement.height); -+ payload.setProperty(runtime, "layoutMeasurement", layoutMeasurement); -+ } -+ -+ payload.setProperty(runtime, "zoomScale", textInputMetrics.zoomScale ?: 1); -+ -+ -+ return payload; -+ }; -+ - static jsi::Value textInputMetricsContentSizePayload( - jsi::Runtime& runtime, - const TextInputMetrics& textInputMetrics) { -@@ -140,7 +188,9 @@ void TextInputEventEmitter::onKeyPressSync( - - void TextInputEventEmitter::onScroll( - const TextInputMetrics& textInputMetrics) const { -- dispatchTextInputEvent("scroll", textInputMetrics); -+ dispatchEvent("scroll", [textInputMetrics](jsi::Runtime& runtime) { -+ return textInputMetricsScrollPayload(runtime, textInputMetrics); -+ }); - } - - void TextInputEventEmitter::dispatchTextInputEvent( diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch.patch deleted file mode 100644 index 3c8034354481..000000000000 --- a/patches/react-native-keyboard-controller+1.12.2.patch.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt -index 83884d8..5d9e989 100644 ---- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt -+++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt -@@ -99,12 +99,12 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R - } - - private fun goToEdgeToEdge(edgeToEdge: Boolean) { -- reactContext.currentActivity?.let { -- WindowCompat.setDecorFitsSystemWindows( -- it.window, -- !edgeToEdge, -- ) -- } -+ // reactContext.currentActivity?.let { -+ // WindowCompat.setDecorFitsSystemWindows( -+ // it.window, -+ // !edgeToEdge, -+ // ) -+ // } - } - - private fun setupKeyboardCallbacks() { -@@ -158,13 +158,13 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R - // region State managers - private fun enable() { - this.goToEdgeToEdge(true) -- this.setupWindowInsets() -+ // this.setupWindowInsets() - this.setupKeyboardCallbacks() - } - - private fun disable() { - this.goToEdgeToEdge(false) -- this.setupWindowInsets() -+ // this.setupWindowInsets() - this.removeKeyboardCallbacks() - } - // endregion \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1ce17ea095bd..9eda57816e9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,6 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; -import {KeyboardProvider} from 'react-native-keyboard-controller'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; @@ -85,7 +84,6 @@ function App({url}: AppProps) { FullScreenContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, - KeyboardProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 1d6c3a92faa9..09f72f17f000 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1246,8 +1246,6 @@ const CONST = { MAX_AMOUNT_OF_SUGGESTIONS: 20, MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5, HERE_TEXT: '@here', - SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38, - BIG_SCREEN_SUGGESTION_WIDTH: 300, }, COMPOSER_MAX_HEIGHT: 125, CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15, diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts deleted file mode 100644 index 5bb671c5edac..000000000000 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ios.ts +++ /dev/null @@ -1,5 +0,0 @@ -function getBottomSuggestionPadding(): number { - return 16; -} - -export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts deleted file mode 100644 index 3ad9bbe7b152..000000000000 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -function getBottomSuggestionPadding(): number { - return 0; -} - -export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx deleted file mode 100644 index 9848d77e479e..000000000000 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.native.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import {Portal} from '@gorhom/portal'; -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import getBottomSuggestionPadding from './getBottomSuggestionPadding'; -import type {AutoCompleteSuggestionsPortalProps} from './types'; - -function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps) { - const StyleUtils = useStyleUtils(); - const styles = useMemo(() => StyleUtils.getBaseAutoCompleteSuggestionContainerStyle({left, width, bottom: bottom + getBottomSuggestionPadding()}), [StyleUtils, left, width, bottom]); - - if (!width) { - return null; - } - - return ( - - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - width={width} - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - /> - - - ); -} - -AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; - -export default AutoCompleteSuggestionsPortal; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx deleted file mode 100644 index 2d1d533c2859..000000000000 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import type {ReactElement} from 'react'; -import ReactDOM from 'react-dom'; -import {View} from 'react-native'; -import BaseAutoCompleteSuggestions from '@components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import getBottomSuggestionPadding from './getBottomSuggestionPadding'; -import type {AutoCompleteSuggestionsPortalProps} from './types'; - -/** - * On the mobile-web platform, when long-pressing on auto-complete suggestions, - * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). - * The desired pattern for all platforms is to do nothing on long-press. - * On the native platform, tapping on auto-complete suggestions will not blur the main input. - */ - -function AutoCompleteSuggestionsPortal({left = 0, width = 0, bottom = 0, ...props}: AutoCompleteSuggestionsPortalProps): ReactElement | null | false { - const StyleUtils = useStyleUtils(); - - const bodyElement = document.querySelector('body'); - - const componentToRender = ( - - width={width} - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - /> - ); - - return ( - !!width && - bodyElement && - ReactDOM.createPortal( - {componentToRender}, - bodyElement, - ) - ); -} - -AutoCompleteSuggestionsPortal.displayName = 'AutoCompleteSuggestionsPortal'; - -export default AutoCompleteSuggestionsPortal; -export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts deleted file mode 100644 index 61fa3e8dcd48..000000000000 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {AutoCompleteSuggestionsProps} from '@components/AutoCompleteSuggestions/types'; - -type ExternalProps = Omit, 'measureParentContainerAndReportCursor'>; - -type AutoCompleteSuggestionsPortalProps = ExternalProps & { - left: number; - width: number; - bottom: number; - measuredHeightOfSuggestionRows: number; -}; - -// eslint-disable-next-line import/prefer-default-export -export type {AutoCompleteSuggestionsPortalProps}; diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 70d70a8c1844..4c11f1f0e35c 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,32 +1,49 @@ import type {ReactElement} from 'react'; import React, {useCallback, useEffect, useRef} from 'react'; import {FlatList} from 'react-native-gesture-handler'; -import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import type {AutoCompleteSuggestionsPortalProps} from './AutoCompleteSuggestionsPortal'; -import type {RenderSuggestionMenuItemProps} from './types'; +import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; -type ExternalProps = Omit, 'left' | 'bottom'>; +const measureHeightOfSuggestionRows = (numRows: number, isSuggestionPickerLarge: boolean): number => { + if (isSuggestionPickerLarge) { + if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { + // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available + return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + if (numRows > 2) { + // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible + return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; +}; + +/** + * On the mobile-web platform, when long-pressing on auto-complete suggestions, + * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). + * The desired pattern for all platforms is to do nothing on long-press. + * On the native platform, tapping on auto-complete suggestions will not blur the main input. + */ function BaseAutoCompleteSuggestions({ - highlightedSuggestionIndex = 0, + highlightedSuggestionIndex, onSelect, accessibilityLabelExtractor, renderSuggestionMenuItem, suggestions, + isSuggestionPickerLarge, keyExtractor, - measuredHeightOfSuggestionRows, -}: ExternalProps) { +}: AutoCompleteSuggestionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); - const prevRowHeightRef = useRef(measuredHeightOfSuggestionRows); - const fadeInOpacity = useSharedValue(0); const scrollRef = useRef>(null); /** * Render a suggestion menu item component. @@ -39,6 +56,7 @@ function BaseAutoCompleteSuggestions({ onMouseDown={(e) => e.preventDefault()} onPress={() => onSelect(index)} onLongPress={() => {}} + shouldUseHapticsOnLongPress={false} accessibilityLabel={accessibilityLabelExtractor(item, index)} > {renderSuggestionMenuItem(item, index)} @@ -48,45 +66,26 @@ function BaseAutoCompleteSuggestions({ ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - - const animatedStyles = useAnimatedStyle(() => ({ - opacity: fadeInOpacity.value, - ...StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value), - })); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); useEffect(() => { - if (measuredHeightOfSuggestionRows === prevRowHeightRef.current) { - fadeInOpacity.value = withTiming(1, { - duration: 70, - easing: Easing.inOut(Easing.ease), - }); - rowHeight.value = measuredHeightOfSuggestionRows; - } else { - fadeInOpacity.value = 1; - rowHeight.value = withTiming(measuredHeightOfSuggestionRows, { - duration: 100, - easing: Easing.bezier(0.25, 0.1, 0.25, 1), - }); - } - - prevRowHeightRef.current = measuredHeightOfSuggestionRows; - }, [suggestions.length, rowHeight, measuredHeightOfSuggestionRows, prevRowHeightRef, fadeInOpacity]); + rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { + duration: 100, + easing: Easing.inOut(Easing.ease), + }); + }, [suggestions.length, isSuggestionPickerLarge, rowHeight]); useEffect(() => { if (!scrollRef.current) { return; } - // When using cursor control (moving the cursor with the space bar on the keyboard) on Android, moving the cursor too fast may cause an error. - try { - scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); - } catch (e) { - // eslint-disable-next-line no-console - } + scrollRef.current.scrollToIndex({index: highlightedSuggestionIndex, animated: true}); }, [highlightedSuggestionIndex]); return ( { if (DeviceCapabilities.hasHoverSupport()) { return; diff --git a/src/components/AutoCompleteSuggestions/index.native.tsx b/src/components/AutoCompleteSuggestions/index.native.tsx new file mode 100644 index 000000000000..fbfa7d953581 --- /dev/null +++ b/src/components/AutoCompleteSuggestions/index.native.tsx @@ -0,0 +1,17 @@ +import {Portal} from '@gorhom/portal'; +import React from 'react'; +import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; + +function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { + return ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + {...props} /> + + ); +} + +AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; + +export default AutoCompleteSuggestions; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 8634d6dd0ca0..c7f2aaea4d82 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -1,135 +1,39 @@ -import React, {useEffect} from 'react'; -import useKeyboardState from '@hooks/useKeyboardState'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import CONST from '@src/CONST'; -import AutoCompleteSuggestionsPortal from './AutoCompleteSuggestionsPortal'; -import type {AutoCompleteSuggestionsProps, MeasureParentContainerAndCursor} from './types'; +import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; +import type {AutoCompleteSuggestionsProps} from './types'; -const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): number => { - if (canBeBig) { - if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { - // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available - return CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - if (numRows > 2) { - // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible - return CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; -}; -function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean { - return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall; -} - -/** - * On the mobile-web platform, when long-pressing on auto-complete suggestions, - * we need to prevent focus shifting to avoid blurring the main input (which makes the suggestions picker close and fires the onSelect callback). - * The desired pattern for all platforms is to do nothing on long-press. - * On the native platform, tapping on auto-complete suggestions will not blur the main input. - */ -function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) { - const containerRef = React.useRef(null); - const isInitialRender = React.useRef(true); - const isSuggestionAboveRef = React.useRef(false); - const leftValue = React.useRef(0); - const prevLeftValue = React.useRef(0); - const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); - const [suggestionHeight, setSuggestionHeight] = React.useState(0); - const [containerState, setContainerState] = React.useState({ +function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const StyleUtils = useStyleUtils(); + const {windowHeight, windowWidth} = useWindowDimensions(); + const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); - const StyleUtils = useStyleUtils(); - const insets = useSafeAreaInsets(); - const {keyboardHeight} = useKeyboardState(); - const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); - useEffect(() => { - const container = containerRef.current; - if (!container) { - return () => {}; - } - container.onpointerdown = (e) => { - if (DeviceCapabilities.hasHoverSupport()) { - return; - } - e.preventDefault(); - }; - return () => (container.onpointerdown = null); - }, []); - - const suggestionsLength = props.suggestions.length; - - useEffect(() => { - if (!measureParentContainerAndReportCursor) { + React.useEffect(() => { + if (!measureParentContainer) { return; } + measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); + }, [measureParentContainer, windowHeight, windowWidth]); - measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => { - const xCoordinatesOfCursor = x + cursorCoordinates.x; - const leftValueForBigScreen = - xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth - ? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH - : xCoordinatesOfCursor; - - let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset); - const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; - - const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); - const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - - const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen; - // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup - const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150; - if (isInitialRender.current || isAdjustmentNeeded) { - isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall); - leftValue.current = newLeftValue; - isInitialRender.current = false; - prevLeftValue.current = newLeftValue; - } - - let measuredHeight = 0; - if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) { - // calculation for big suggestion box above the cursor - measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) { - // calculation for small suggestion box above the cursor - measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false); - } else { - // calculation for big suggestion box below the cursor - measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } - setSuggestionHeight(measuredHeight); - setContainerState({ - left: leftValue.current, - bottom: bottomValue, - width: widthValue, - }); - }); - }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]); - - if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) { - return null; - } - return ( - // eslint-disable-next-line react/jsx-props-no-spreading {...props} - left={containerState.left} - width={containerState.width} - bottom={containerState.bottom} - measuredHeightOfSuggestionRows={suggestionHeight} /> ); + + const bodyElement = document.querySelector('body'); + + return ( + !!width && bodyElement && ReactDOM.createPortal({componentToRender}, bodyElement) + ); } AutoCompleteSuggestions.displayName = 'AutoCompleteSuggestions'; diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 48bb6b713032..61d614dcf2e4 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,15 +1,6 @@ import type {ReactElement} from 'react'; -type MeasureParentContainerAndCursor = { - x: number; - y: number; - width: number; - height: number; - scrollValue: number; - cursorCoordinates: {x: number; y: number}; -}; - -type MeasureParentContainerAndCursorCallback = (props: MeasureParentContainerAndCursor) => void; +type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; type RenderSuggestionMenuItemProps = { item: TSuggestion; @@ -40,8 +31,8 @@ type AutoCompleteSuggestionsProps = { /** create accessibility label for each item */ accessibilityLabelExtractor: (item: TSuggestion, index: number) => string; - /** Measures the parent container's position and dimensions. Also add a cursor coordinates */ - measureParentContainerAndReportCursor?: (props: MeasureParentContainerAndCursorCallback) => void; + /** Meaures the parent container's position and dimensions. */ + measureParentContainer?: (callback: MeasureParentContainerCallback) => void; }; -export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps, MeasureParentContainerAndCursorCallback, MeasureParentContainerAndCursor}; +export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3a8a4e724948..5bd8aa9175d3 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -91,8 +91,6 @@ function Composer( | { start: number; end?: number; - positionX?: number; - positionY?: number; } | undefined >({ diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9c7a5a215c1c..0ff91111bd07 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -3,12 +3,6 @@ import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelection type TextSelection = { start: number; end?: number; - positionX?: number; - positionY?: number; -}; -type CustomSelectionChangeEvent = NativeSyntheticEvent & { - positionX?: number; - positionY?: number; }; type ComposerProps = TextInputProps & { @@ -51,7 +45,7 @@ type ComposerProps = TextInputProps & { autoFocus?: boolean; /** Update selection position on change */ - onSelectionChange?: (event: CustomSelectionChangeEvent) => void; + onSelectionChange?: (event: NativeSyntheticEvent) => void; /** Selection Object */ selection?: TextSelection; @@ -81,4 +75,4 @@ type ComposerProps = TextInputProps & { isGroupPolicyReport?: boolean; }; -export type {TextSelection, ComposerProps, CustomSelectionChangeEvent}; +export type {TextSelection, ComposerProps}; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 3781507b544c..1c0306741048 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -7,9 +7,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; -import type {MeasureParentContainerAndCursorCallback} from './AutoCompleteSuggestions/types'; import Text from './Text'; +type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; + type EmojiSuggestionsProps = { /** The index of the highlighted emoji */ highlightedEmojiIndex?: number; @@ -32,8 +33,8 @@ type EmojiSuggestionsProps = { /** Stores user's preferred skin tone */ preferredSkinToneIndex: number; - /** Measures the parent container's position and dimensions. Also add cursor coordinates */ - measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; + /** Meaures the parent container's position and dimensions. */ + measureParentContainer: (callback: MeasureParentContainerCallback) => void; }; /** @@ -41,15 +42,7 @@ type EmojiSuggestionsProps = { */ const keyExtractor = (item: Emoji, index: number): string => `${item.name}+${index}}`; -function EmojiSuggestions({ - emojis, - onSelect, - prefix, - isEmojiPickerLarge, - preferredSkinToneIndex, - highlightedEmojiIndex = 0, - measureParentContainerAndReportCursor = () => {}, -}: EmojiSuggestionsProps) { +function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); /** @@ -92,7 +85,7 @@ function EmojiSuggestions({ onSelect={onSelect} isSuggestionPickerLarge={isEmojiPickerLarge} accessibilityLabelExtractor={keyExtractor} - measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} + measureParentContainer={measureParentContainer} /> ); } diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 1142a90c87d1..877133b196cc 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,4 +1,5 @@ import React, {useCallback} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -7,7 +8,6 @@ import getStyledTextArray from '@libs/GetStyledTextArray'; import CONST from '@src/CONST'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; -import type {MeasureParentContainerAndCursorCallback} from './AutoCompleteSuggestions/types'; import Avatar from './Avatar'; import Text from './Text'; @@ -53,8 +53,8 @@ type MentionSuggestionsProps = { * When this value is false, the suggester will have a height of 2.5 items. When this value is true, the height can be up to 5 items. */ isMentionPickerLarge: boolean; - /** Measures the parent container's position and dimensions. Also add cursor coordinates */ - measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; + /** Measures the parent container's position and dimensions. */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; }; /** @@ -62,7 +62,7 @@ type MentionSuggestionsProps = { */ const keyExtractor = (item: Mention) => item.alternateText; -function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainerAndReportCursor = () => {}}: MentionSuggestionsProps) { +function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainer = () => {}}: MentionSuggestionsProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -148,7 +148,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe onSelect={onSelect} isSuggestionPickerLarge={isMentionPickerLarge} accessibilityLabelExtractor={keyExtractor} - measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} + measureParentContainer={measureParentContainer} /> ); } diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 7fc0299fa393..04d857a8faeb 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -1,4 +1,3 @@ -import type {TextSelection} from '@components/Composer/types'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; type Selection = { @@ -9,7 +8,7 @@ type Selection = { /** * Replace substring between selection with a text. */ -function insertText(text: string, selection: TextSelection, textToInsert: string): string { +function insertText(text: string, selection: Selection, textToInsert: string): string { return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 37b9184a3250..382b45a3951b 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1,4 +1,3 @@ -import {PortalHost} from '@gorhom/portal'; import {useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import lodashIsEqual from 'lodash/isEqual'; @@ -7,6 +6,7 @@ import type {FlatList, ViewStyle} from 'react-native'; import {InteractionManager, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useOnyx, withOnyx} from 'react-native-onyx'; +import type {LayoutChangeEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import Banner from '@components/Banner'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -269,6 +269,7 @@ function ReportScreen({ }, [route, reportActionIDFromRoute]); const [isBannerVisible, setIsBannerVisible] = useState(true); + const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); const wasReportAccessibleRef = useRef(false); @@ -594,7 +595,8 @@ function ReportScreen({ }; }, [report, didSubscribeToReportLeavingEvents, reportIDFromRoute]); - const onListLayout = useCallback(() => { + const onListLayout = useCallback((event: LayoutChangeEvent) => { + setListHeight((prev) => event.nativeEvent?.layout?.height ?? prev); if (!markReadyForHydration) { return; } @@ -733,12 +735,12 @@ function ReportScreen({ policy={policy} pendingAction={reportPendingAction} isComposerFullSize={!!isComposerFullSize} + listHeight={listHeight} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} /> ) : null} - diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 46477964d17b..fd0eaa32d20e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -9,19 +9,15 @@ import type { TextInput, TextInputFocusEventData, TextInputKeyPressEventData, - TextInputScrollEventData, + TextInputSelectionChangeEventData, } from 'react-native'; import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; -import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; import type {useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; -import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; import Composer from '@components/Composer'; -import type {CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; @@ -42,10 +38,9 @@ import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; -import getCursorPosition from '@pages/home/report/ReportActionCompose/getCursorPosition'; -import getScrollPosition from '@pages/home/report/ReportActionCompose/getScrollPosition'; import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; @@ -138,6 +133,9 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** Function to measure the parent container */ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + /** The height of the list */ + listHeight: number; + /** Whether the scroll is likely to trigger a layout */ isScrollLikelyLayoutTriggered: RefObject; @@ -250,6 +248,7 @@ function ComposerWithSuggestions( handleSendMessage, shouldShowComposeInput, measureParentContainer = () => {}, + listHeight, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, @@ -272,9 +271,6 @@ function ComposerWithSuggestions( const isFocused = useIsFocused(); const navigation = useNavigation(); const emojisPresentBefore = useRef([]); - const mobileInputScrollPosition = useRef(0); - const cursorPositionValue = useSharedValue({x: 0, y: 0}); - const tag = useSharedValue(-1); const draftComment = getDraftComment(reportID) ?? ''; const [value, setValue] = useState(() => { if (draftComment) { @@ -299,7 +295,7 @@ function ComposerWithSuggestions( const valueRef = useRef(value); valueRef.current = value; - const [selection, setSelection] = useState(() => ({start: 0, end: 0, positionX: 0, positionY: 0})); + const [selection, setSelection] = useState(() => ({start: 0, end: 0})); const [composerHeight, setComposerHeight] = useState(0); @@ -308,6 +304,12 @@ function ComposerWithSuggestions( const syncSelectionWithOnChangeTextRef = useRef(null); + const suggestions = suggestionsRef.current?.getSuggestions() ?? []; + + const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); + + const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); + // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -387,9 +389,9 @@ function ComposerWithSuggestions( if (currentIndex < newText.length) { startIndex = currentIndex; - const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection?.end ?? 0); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection.end); // if text is getting pasted over find length of common suffix and subtract it from new text length - if (commonSuffixLength > 0 || (selection?.end ?? 0) - selection.start > 0) { + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; } else { endIndex = currentIndex + newText.length; @@ -436,18 +438,16 @@ function ComposerWithSuggestions( emojisPresentBefore.current = emojis; setValue(newCommentConverted); if (commentValue !== newComment) { - const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), cursorPosition ?? 0); + const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition ?? 0); if (commentWithSpaceInserted !== newComment && isIOSNative) { syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; } - setSelection((prevSelection) => ({ + setSelection({ start: position, end: position, - positionX: prevSelection.positionX, - positionY: prevSelection.positionY, - })); + }); } commentRef.current = newCommentConverted; @@ -490,7 +490,7 @@ function ComposerWithSuggestions( debouncedSaveReportComment.cancel(); isCommentPendingSaved.current = false; - setSelection({start: 0, end: 0, positionX: 0, positionY: 0}); + setSelection({start: 0, end: 0}); updateComment(''); setTextInputShouldClear(true); if (isComposerFullSize) { @@ -569,27 +569,22 @@ function ComposerWithSuggestions( ); const onSelectionChange = useCallback( - (e: CustomSelectionChangeEvent) => { - if (!textInputRef.current?.isFocused()) { + (e: NativeSyntheticEvent) => { + if (textInputRef.current?.isFocused() && suggestionsRef.current?.onSelectionChange?.(e)) { return; } - suggestionsRef.current?.onSelectionChange?.(e); setSelection(e.nativeEvent.selection); }, [suggestionsRef], ); - const hideSuggestionMenu = useCallback( - (e: NativeSyntheticEvent) => { - mobileInputScrollPosition.current = e?.nativeEvent?.contentOffset?.y ?? 0; - if (!suggestionsRef.current || isScrollLikelyLayoutTriggered.current) { - return; - } - suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); - }, - [suggestionsRef, isScrollLikelyLayoutTriggered], - ); + const hideSuggestionMenu = useCallback(() => { + if (!suggestionsRef.current || isScrollLikelyLayoutTriggered.current) { + return; + } + suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); + }, [suggestionsRef, isScrollLikelyLayoutTriggered]); const setShouldBlockSuggestionCalcToFalse = useCallback(() => { if (!suggestionsRef.current) { @@ -745,43 +740,6 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - tag.value = findNodeHandle(textInputRef.current) ?? -1; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useFocusedInputHandler( - { - onSelectionChange: (event) => { - 'worklet'; - - if (event.target === tag.value) { - cursorPositionValue.value = { - x: event.selection.end.x, - y: event.selection.end.y, - }; - } - }, - }, - [], - ); - const measureParentContainerAndReportCursor = useCallback( - (callback: MeasureParentContainerAndCursorCallback) => { - const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef}); - const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.value, positionOnWeb: selection}); - measureParentContainer((x, y, width, height) => { - callback({ - x, - y, - width, - height, - scrollValue, - cursorCoordinates: {x: xPosition, y: yPosition}, - }); - }); - }, - [measureParentContainer, cursorPositionValue, selection], - ); - return ( <> @@ -822,9 +780,12 @@ function ComposerWithSuggestions( & { + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string) => void; @@ -110,6 +111,7 @@ function ReportActionCompose({ pendingAction, report, reportID, + listHeight = 0, shouldShowComposeInput = true, isReportReadyForDisplay = true, isEmptyChat, @@ -382,6 +384,7 @@ function ReportActionCompose({ {shouldShowReportRecipientLocalTime && hasReportRecipient && } + { if (value.length === 0 && isComposerFullSize) { Report.setIsComposerFullSize(reportID, false); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index b08ee77745db..b23c0be72592 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,5 +1,6 @@ import type {ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import EmojiSuggestions from '@components/EmojiSuggestions'; @@ -54,7 +55,7 @@ function SuggestionEmoji( updateComment, isAutoSuggestionPickerLarge, resetKeyboardInput, - measureParentContainerAndReportCursor, + measureParentContainer, isComposerFocused, }: SuggestionEmojiProps, ref: ForwardedRef, @@ -149,8 +150,8 @@ function SuggestionEmoji( * Calculates and cares about the content of an Emoji Suggester */ const calculateEmojiSuggestion = useCallback( - (selectionEnd?: number) => { - if (!selectionEnd || shouldBlockCalc.current || !value) { + (selectionEnd: number) => { + if (shouldBlockCalc.current || !value) { shouldBlockCalc.current = false; resetSuggestions(); return; @@ -184,6 +185,18 @@ function SuggestionEmoji( calculateEmojiSuggestion(selection.end); }, [selection, calculateEmojiSuggestion, isComposerFocused]); + const onSelectionChange = useCallback( + (e: NativeSyntheticEvent) => { + /** + * we pass here e.nativeEvent.selection.end directly to calculateEmojiSuggestion + * because in other case calculateEmojiSuggestion will have an old calculation value + * of suggestion instead of current one + */ + calculateEmojiSuggestion(e.nativeEvent.selection.end); + }, + [calculateEmojiSuggestion], + ); + const setShouldBlockSuggestionCalc = useCallback( (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; @@ -197,12 +210,13 @@ function SuggestionEmoji( ref, () => ({ resetSuggestions, + onSelectionChange, triggerHotkeyActions, setShouldBlockSuggestionCalc, updateShouldShowSuggestionMenuToFalse, getSuggestions, }), - [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], + [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], ); if (!isEmojiSuggestionsMenuVisible) { @@ -217,7 +231,7 @@ function SuggestionEmoji( onSelect={insertSelectedEmoji} preferredSkinToneIndex={preferredSkinTone} isEmojiPickerLarge={!!isAutoSuggestionPickerLarge} - measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} + measureParentContainer={measureParentContainer} /> ); } diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 0b430a519812..3b9d6ee12c12 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -57,7 +57,7 @@ type SuggestionPersonalDetailsList = Record< >; function SuggestionMention( - {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainerAndReportCursor, isComposerFocused, isGroupPolicyReport, policyID}: SuggestionProps, + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused, isGroupPolicyReport, policyID}: SuggestionProps, ref: ForwardedRef, ) { const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; @@ -317,8 +317,8 @@ function SuggestionMention( ); const calculateMentionSuggestion = useCallback( - (selectionEnd?: number) => { - if (!selectionEnd || shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) { + (selectionEnd: number) => { + if (shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) { shouldBlockCalc.current = false; resetSuggestions(); return; @@ -432,7 +432,7 @@ function SuggestionMention( prefix={suggestionValues.mentionPrefix} onSelect={insertSelectedMention} isMentionPickerLarge={!!isAutoSuggestionPickerLarge} - measureParentContainerAndReportCursor={measureParentContainerAndReportCursor} + measureParentContainer={measureParentContainer} /> ); } diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index f82b38c3e154..8ebd52f62428 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -1,15 +1,18 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; -import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; -import type {TextSelection} from '@components/Composer/types'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import usePrevious from '@hooks/usePrevious'; import type {SuggestionsRef} from './ReportActionCompose'; import SuggestionEmoji from './SuggestionEmoji'; import SuggestionMention from './SuggestionMention'; +type Selection = { + start: number; + end: number; +}; + type SuggestionProps = { /** The current input value */ value: string; @@ -18,16 +21,19 @@ type SuggestionProps = { setValue: (newValue: string) => void; /** The current selection value */ - selection: TextSelection; + selection: Selection; /** Callback to update the current selection */ - setSelection: (newSelection: TextSelection) => void; + setSelection: (newSelection: Selection) => void; /** Callback to update the comment draft */ updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; - /** Measures the parent container's position and dimensions. Also add cursor coordinates */ - measureParentContainerAndReportCursor: (callback: MeasureParentContainerAndCursorCallback) => void; + /** Meaures the parent container's position and dimensions. */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + + /** Whether the composer is expanded */ + isComposerFullSize: boolean; /** Report composer focus state */ isComposerFocused?: boolean; @@ -55,13 +61,15 @@ type SuggestionProps = { */ function Suggestions( { + isComposerFullSize, value, setValue, selection, setSelection, updateComment, + composerHeight, resetKeyboardInput, - measureParentContainerAndReportCursor, + measureParentContainer, isAutoSuggestionPickerLarge = true, isComposerFocused, isGroupPolicyReport, @@ -111,7 +119,6 @@ function Suggestions( const onSelectionChange = useCallback((e: NativeSyntheticEvent) => { const emojiHandler = suggestionEmojiRef.current?.onSelectionChange?.(e); - suggestionMentionRef.current?.onSelectionChange?.(e); return emojiHandler; }, []); @@ -150,9 +157,11 @@ function Suggestions( setValue, setSelection, selection, + isComposerFullSize, updateComment, + composerHeight, isAutoSuggestionPickerLarge, - measureParentContainerAndReportCursor, + measureParentContainer, isComposerFocused, isGroupPolicyReport, policyID, diff --git a/src/pages/home/report/ReportActionCompose/getCursorPosition/index.native.ts b/src/pages/home/report/ReportActionCompose/getCursorPosition/index.native.ts deleted file mode 100644 index 5107e2c37362..000000000000 --- a/src/pages/home/report/ReportActionCompose/getCursorPosition/index.native.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {CursorPositionParamsType, PositionType} from './types'; - -function getCursorPosition({positionOnMobile}: CursorPositionParamsType): PositionType { - return { - x: positionOnMobile?.x ?? 0, - y: positionOnMobile?.y ?? 0, - }; -} - -export default getCursorPosition; diff --git a/src/pages/home/report/ReportActionCompose/getCursorPosition/index.ts b/src/pages/home/report/ReportActionCompose/getCursorPosition/index.ts deleted file mode 100644 index e1619b4cd45c..000000000000 --- a/src/pages/home/report/ReportActionCompose/getCursorPosition/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {CursorPositionParamsType, PositionType} from './types'; - -function getCursorPosition({positionOnWeb}: CursorPositionParamsType): PositionType { - const x = positionOnWeb?.positionX ?? 0; - const y = positionOnWeb?.positionY ?? 0; - return {x, y}; -} - -export default getCursorPosition; diff --git a/src/pages/home/report/ReportActionCompose/getCursorPosition/types.ts b/src/pages/home/report/ReportActionCompose/getCursorPosition/types.ts deleted file mode 100644 index 424e71377ecd..000000000000 --- a/src/pages/home/report/ReportActionCompose/getCursorPosition/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -type PositionType = { - x: number; - y: number; -}; - -type CursorPositionParamsType = { - positionOnMobile?: PositionType; - positionOnWeb?: {positionX?: number; positionY?: number}; -}; - -type GetCursorPositionType = (params: CursorPositionParamsType) => PositionType; - -export type {PositionType, CursorPositionParamsType, GetCursorPositionType}; diff --git a/src/pages/home/report/ReportActionCompose/getScrollPosition/index.native.ts b/src/pages/home/report/ReportActionCompose/getScrollPosition/index.native.ts deleted file mode 100644 index 2045549959b8..000000000000 --- a/src/pages/home/report/ReportActionCompose/getScrollPosition/index.native.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type {GetScrollPositionType, TextInputScrollProps} from './types'; - -function getScrollPosition({mobileInputScrollPosition}: TextInputScrollProps): GetScrollPositionType { - if (!mobileInputScrollPosition.current) { - return { - scrollValue: 0, - }; - } - return { - scrollValue: mobileInputScrollPosition.current, - }; -} - -export default getScrollPosition; diff --git a/src/pages/home/report/ReportActionCompose/getScrollPosition/index.ts b/src/pages/home/report/ReportActionCompose/getScrollPosition/index.ts deleted file mode 100644 index 0d9fb701cabd..000000000000 --- a/src/pages/home/report/ReportActionCompose/getScrollPosition/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {GetScrollPositionType, TextInputScrollProps} from './types'; - -function getScrollPosition({textInputRef}: TextInputScrollProps): GetScrollPositionType { - let scrollValue = 0; - if (textInputRef?.current) { - if ('scrollTop' in textInputRef.current) { - scrollValue = textInputRef.current.scrollTop; - } - } - return {scrollValue}; -} - -export default getScrollPosition; diff --git a/src/pages/home/report/ReportActionCompose/getScrollPosition/types.ts b/src/pages/home/report/ReportActionCompose/getScrollPosition/types.ts deleted file mode 100644 index abb48e2cc079..000000000000 --- a/src/pages/home/report/ReportActionCompose/getScrollPosition/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {TextInput} from 'react-native'; - -type TextInputScrollProps = { - mobileInputScrollPosition: React.RefObject; - textInputRef: React.RefObject; -}; - -type GetScrollPositionType = {scrollValue: number}; - -export type {TextInputScrollProps, GetScrollPositionType}; diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index bff2d322120b..745d4b7dac8e 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -52,6 +52,9 @@ type ReportFooterProps = { /** The pending action when we are adding a chat */ pendingAction?: PendingAction; + /** Height of the list which the composer is part of */ + listHeight?: number; + /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; @@ -74,6 +77,7 @@ function ReportFooter({ policy, isEmptyChat = true, isReportReadyForDisplay = true, + listHeight = 0, isComposerFullSize = false, onComposerBlur, onComposerFocus, @@ -211,6 +215,7 @@ function ReportFooter({ lastReportAction={lastReportAction} pendingAction={pendingAction} isComposerFullSize={isComposerFullSize} + listHeight={listHeight} isReportReadyForDisplay={isReportReadyForDisplay} /> @@ -227,6 +232,7 @@ export default memo( (prevProps, nextProps) => lodashIsEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && + prevProps.listHeight === nextProps.listHeight && prevProps.isComposerFullSize === nextProps.isComposerFullSize && prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && diff --git a/src/styles/index.ts b/src/styles/index.ts index b031e665594f..7120a6b56aac 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -266,8 +266,10 @@ const styles = (theme: ThemeColors) => borderWidth: 1, borderColor: theme.border, justifyContent: 'center', - overflow: 'hidden', boxShadow: variables.popoverMenuShadow, + position: 'absolute', + left: 0, + right: 0, paddingVertical: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING, }, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 44c40e17d60e..77c10c4d298b 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -845,7 +845,7 @@ type GetBaseAutoCompleteSuggestionContainerStyleParams = { */ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetBaseAutoCompleteSuggestionContainerStyleParams): ViewStyle { return { - position: 'absolute', + ...positioning.pFixed, bottom, left, width, @@ -863,7 +863,11 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle const borderWidth = 2; const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING + (shouldPreventScroll ? borderWidth : 0); + // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, + // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { + overflow: 'hidden', + top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + (shouldPreventScroll ? 0 : borderWidth)), height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 28987e6b58ed..ba634f1173a1 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -95,6 +95,7 @@ function ReportActionComposeWrapper() { disabled={false} report={LHNTestUtils.getFakeReport()} isComposerFullSize + listHeight={200} /> );