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}
/>
);