Skip to content

Commit b21efa7

Browse files
committed
fix: autogrow on iOS is handled by native platform
1 parent dad4352 commit b21efa7

File tree

5 files changed

+177
-111
lines changed

5 files changed

+177
-111
lines changed

src/components/TextInput/BaseTextInput/implementation/index.native.tsx

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Str} from 'expensify-common';
22
import type {ForwardedRef} from 'react';
33
import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
44
import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native';
5-
import {ActivityIndicator, StyleSheet, View} from 'react-native';
5+
import {ActivityIndicator, Platform, StyleSheet, View} from 'react-native';
66
import {useSharedValue, withSpring} from 'react-native-reanimated';
77
import Checkbox from '@components/Checkbox';
88
import FormHelpMessage from '@components/FormHelpMessage';
@@ -18,6 +18,7 @@ import type {BaseTextInputProps, BaseTextInputRef} from '@components/TextInput/B
1818
import * as styleConst from '@components/TextInput/styleConst';
1919
import TextInputClearButton from '@components/TextInput/TextInputClearButton';
2020
import TextInputLabel from '@components/TextInput/TextInputLabel';
21+
import TextInputMeasurement from '@components/TextInput/TextInputMeasurement';
2122
import useHtmlPaste from '@hooks/useHtmlPaste';
2223
import useLocalize from '@hooks/useLocalize';
2324
import useMarkdownStyle from '@hooks/useMarkdownStyle';
@@ -48,7 +49,7 @@ function BaseTextInput(
4849
forceActiveLabel = false,
4950
autoFocus = false,
5051
disableKeyboard = false,
51-
autoGrow = false,
52+
autoGrow: autoGrowProp = false,
5253
autoGrowExtraSpace = 0,
5354
autoGrowHeight = false,
5455
maxAutoGrowHeight,
@@ -77,6 +78,10 @@ function BaseTextInput(
7778
}: BaseTextInputProps,
7879
ref: ForwardedRef<BaseTextInputRef>,
7980
) {
81+
// For iOS, we don't need to measure the text input because it already has auto grow behavior
82+
// See TextInputMeasurement.ios.tsx for more details
83+
const autoGrow = Platform.OS !== 'ios' && autoGrowProp;
84+
8085
const InputComponent = InputComponentMap.get(type) ?? RNTextInput;
8186
const isMarkdownEnabled = type === 'markdown';
8287
const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight;
@@ -351,8 +356,8 @@ function BaseTextInput(
351356
placeholderTextColor={placeholderTextColor ?? theme.placeholderText}
352357
underlineColorAndroid="transparent"
353358
style={[
354-
styles.flex1,
355-
styles.w100,
359+
autoGrow && styles.flex1,
360+
autoGrow && styles.w100,
356361
inputStyle,
357362
(!hasLabel || isMultiline) && styles.pv0,
358363
inputPaddingLeft,
@@ -440,55 +445,20 @@ function BaseTextInput(
440445
/>
441446
)}
442447
</View>
443-
{!!contentWidth && (
444-
<View
445-
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
446-
onLayout={(e) => {
447-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
448-
return;
449-
}
450-
setTextInputWidth(e.nativeEvent.layout.width);
451-
setTextInputHeight(e.nativeEvent.layout.height);
452-
}}
453-
>
454-
<Text
455-
style={[
456-
inputStyle,
457-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
458-
{width: contentWidth},
459-
]}
460-
>
461-
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
462-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
463-
</Text>
464-
</View>
465-
)}
466-
{/*
467-
Text input component doesn't support auto grow by default.
468-
This text view is used to calculate width or height of the input value given textStyle in this component.
469-
This Text component is intentionally positioned out of the screen.
470-
*/}
471-
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
472-
<Text
473-
style={[
474-
inputStyle,
475-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
476-
styles.hiddenElementOutsideOfWindow,
477-
styles.visibilityHidden,
478-
]}
479-
onLayout={(e) => {
480-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
481-
return;
482-
}
483-
// Add +2 to width so that cursor is not cut off / covered at the end of text content
484-
setTextInputWidth(e.nativeEvent.layout.width + 2);
485-
setTextInputHeight(e.nativeEvent.layout.height);
486-
}}
487-
>
488-
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
489-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
490-
</Text>
491-
)}
448+
<TextInputMeasurement
449+
value={value}
450+
placeholder={placeholder}
451+
contentWidth={contentWidth}
452+
autoGrowHeight={autoGrowHeight}
453+
maxAutoGrowHeight={maxAutoGrowHeight}
454+
width={width}
455+
inputStyle={inputStyle}
456+
inputPaddingLeft={inputPaddingLeft}
457+
autoGrow={autoGrow}
458+
isAutoGrowHeightMarkdown={isAutoGrowHeightMarkdown}
459+
onSetTextInputWidth={setTextInputWidth}
460+
onSetTextInputHeight={setTextInputHeight}
461+
/>
492462
</>
493463
);
494464
}

src/components/TextInput/BaseTextInput/implementation/index.tsx

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import type {BaseTextInputProps, BaseTextInputRef} from '@components/TextInput/B
1919
import {ACTIVE_LABEL_SCALE, ACTIVE_LABEL_TRANSLATE_Y, INACTIVE_LABEL_SCALE, INACTIVE_LABEL_TRANSLATE_Y} from '@components/TextInput/styleConst';
2020
import TextInputClearButton from '@components/TextInput/TextInputClearButton';
2121
import TextInputLabel from '@components/TextInput/TextInputLabel';
22+
import TextInputMeasurement from '@components/TextInput/TextInputMeasurement';
2223
import useHtmlPaste from '@hooks/useHtmlPaste';
2324
import useLocalize from '@hooks/useLocalize';
2425
import useMarkdownStyle from '@hooks/useMarkdownStyle';
2526
import useStyleUtils from '@hooks/useStyleUtils';
2627
import useTheme from '@hooks/useTheme';
2728
import useThemeStyles from '@hooks/useThemeStyles';
28-
import {isMobileChrome, isMobileSafari, isSafari} from '@libs/Browser';
29+
import {isMobileChrome} from '@libs/Browser';
2930
import {scrollToRight} from '@libs/InputUtils';
3031
import isInputAutoFilled from '@libs/isInputAutoFilled';
3132
import variables from '@styles/variables';
@@ -458,63 +459,20 @@ function BaseTextInput(
458459
/>
459460
)}
460461
</View>
461-
{!!contentWidth && (
462-
<View
463-
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
464-
onLayout={(e) => {
465-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
466-
return;
467-
}
468-
setTextInputWidth(e.nativeEvent.layout.width);
469-
setTextInputHeight(e.nativeEvent.layout.height);
470-
}}
471-
>
472-
<Text
473-
style={[
474-
inputStyle,
475-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
476-
{width: contentWidth},
477-
]}
478-
>
479-
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
480-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
481-
</Text>
482-
</View>
483-
)}
484-
{/*
485-
Text input component doesn't support auto grow by default.
486-
We're using a hidden text input to achieve that.
487-
This text view is used to calculate width or height of the input value given textStyle in this component.
488-
This Text component is intentionally positioned out of the screen.
489-
*/}
490-
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
491-
// Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
492-
// Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628
493-
// For mobile Chrome, ensure proper display of the text selection handle (blue bubble down).
494-
// Reference: https://github.com/Expensify/App/issues/34921
495-
<Text
496-
style={[
497-
inputStyle,
498-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
499-
styles.hiddenElementOutsideOfWindow,
500-
styles.visibilityHidden,
501-
]}
502-
onLayout={(e) => {
503-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
504-
return;
505-
}
506-
let additionalWidth = 0;
507-
if (isMobileSafari() || isSafari() || isMobileChrome()) {
508-
additionalWidth = 2;
509-
}
510-
setTextInputWidth(e.nativeEvent.layout.width + additionalWidth);
511-
setTextInputHeight(e.nativeEvent.layout.height);
512-
}}
513-
>
514-
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
515-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
516-
</Text>
517-
)}
462+
<TextInputMeasurement
463+
value={value}
464+
placeholder={placeholder}
465+
contentWidth={contentWidth}
466+
autoGrowHeight={autoGrowHeight}
467+
maxAutoGrowHeight={maxAutoGrowHeight}
468+
width={width}
469+
inputStyle={inputStyle}
470+
inputPaddingLeft={inputPaddingLeft}
471+
autoGrow={autoGrow}
472+
isAutoGrowHeightMarkdown={isAutoGrowHeightMarkdown}
473+
onSetTextInputWidth={setTextInputWidth}
474+
onSetTextInputHeight={setTextInputHeight}
475+
/>
518476
</>
519477
);
520478
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function TextInputMeasurement() {
2+
return null;
3+
}
4+
5+
TextInputMeasurement.displayName = 'TextInputMeasurement';
6+
7+
export default TextInputMeasurement;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from 'react';
2+
import type {ViewStyle} from 'react-native';
3+
import {View} from 'react-native';
4+
import Text from '@components/Text';
5+
import useThemeStyles from '@hooks/useThemeStyles';
6+
import {isMobileChrome, isMobileSafari, isSafari} from '@libs/Browser';
7+
import type TextInputMeasurementProps from './types';
8+
9+
function TextInputMeasurement({
10+
value,
11+
placeholder,
12+
contentWidth,
13+
autoGrowHeight,
14+
maxAutoGrowHeight,
15+
width,
16+
inputStyle,
17+
inputPaddingLeft,
18+
autoGrow,
19+
isAutoGrowHeightMarkdown,
20+
onSetTextInputWidth,
21+
onSetTextInputHeight,
22+
}: TextInputMeasurementProps) {
23+
const styles = useThemeStyles();
24+
25+
return (
26+
<>
27+
{!!contentWidth && (
28+
<View
29+
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
30+
onLayout={(e) => {
31+
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
32+
return;
33+
}
34+
onSetTextInputWidth(e.nativeEvent.layout.width);
35+
onSetTextInputHeight(e.nativeEvent.layout.height);
36+
}}
37+
>
38+
<Text
39+
style={[
40+
inputStyle,
41+
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
42+
{width: contentWidth},
43+
]}
44+
>
45+
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
46+
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
47+
</Text>
48+
</View>
49+
)}
50+
{/*
51+
Text input component doesn't support auto grow by default.
52+
We're using a hidden text input to achieve that.
53+
This text view is used to calculate width or height of the input value given textStyle in this component.
54+
This Text component is intentionally positioned out of the screen.
55+
*/}
56+
{(!!autoGrow || !!autoGrowHeight) && !isAutoGrowHeightMarkdown && (
57+
// Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
58+
// Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628
59+
// For mobile Chrome, ensure proper display of the text selection handle (blue bubble down).
60+
// Reference: https://github.com/Expensify/App/issues/34921
61+
<Text
62+
style={[
63+
inputStyle,
64+
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
65+
styles.hiddenElementOutsideOfWindow,
66+
styles.visibilityHidden,
67+
]}
68+
onLayout={(e) => {
69+
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
70+
return;
71+
}
72+
let additionalWidth = 0;
73+
if (isMobileSafari() || isSafari() || isMobileChrome()) {
74+
additionalWidth = 2;
75+
}
76+
onSetTextInputWidth(e.nativeEvent.layout.width + additionalWidth);
77+
onSetTextInputHeight(e.nativeEvent.layout.height);
78+
}}
79+
>
80+
{/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */}
81+
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
82+
</Text>
83+
)}
84+
</>
85+
);
86+
}
87+
88+
TextInputMeasurement.displayName = 'TextInputMeasurement';
89+
90+
export default TextInputMeasurement;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
2+
3+
type TextInputMeasurementProps = {
4+
/** The value to measure */
5+
value?: string;
6+
7+
/** The placeholder to measure */
8+
placeholder?: string;
9+
10+
/** The width to measure */
11+
contentWidth?: number;
12+
13+
/** Whether to auto grow height */
14+
autoGrowHeight?: boolean;
15+
16+
/** The maximum height for auto grow */
17+
maxAutoGrowHeight?: number;
18+
19+
/** The width of the container */
20+
width: number | null;
21+
22+
/** The input style */
23+
inputStyle?: StyleProp<TextStyle>;
24+
25+
/** The input padding left */
26+
inputPaddingLeft?: StyleProp<ViewStyle>;
27+
28+
/** Whether to auto grow */
29+
autoGrow?: boolean;
30+
31+
/** Whether the input is markdown */
32+
isAutoGrowHeightMarkdown?: boolean;
33+
34+
/** Callback to set the text input width */
35+
onSetTextInputWidth: (width: number) => void;
36+
37+
/** Callback to set the text input height */
38+
onSetTextInputHeight: (height: number) => void;
39+
};
40+
41+
export default TextInputMeasurementProps;

0 commit comments

Comments
 (0)