Skip to content

Commit de57d65

Browse files
authored
Merge pull request #59078 from margelo/fix/19961-input-numbers-flickers
fix: autoGrow prop on mobile is handled by native platform
2 parents 7e5a333 + 2effe74 commit de57d65

File tree

5 files changed

+188
-111
lines changed

5 files changed

+188
-111
lines changed

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

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ 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';
2425
import useStyleUtils from '@hooks/useStyleUtils';
2526
import useTheme from '@hooks/useTheme';
2627
import useThemeStyles from '@hooks/useThemeStyles';
28+
import getPlatform from '@libs/getPlatform';
2729
import isInputAutoFilled from '@libs/isInputAutoFilled';
2830
import variables from '@styles/variables';
2931
import CONST from '@src/CONST';
@@ -78,6 +80,10 @@ function BaseTextInput(
7880
}: BaseTextInputProps,
7981
ref: ForwardedRef<BaseTextInputRef>,
8082
) {
83+
// For iOS, we don't need to measure the text input because it already has auto grow behavior
84+
// See TextInputMeasurement.ios.tsx for more details
85+
const isExternalAutoGrowMeasurement = getPlatform() !== CONST.PLATFORM.IOS && autoGrow;
86+
8187
const InputComponent = InputComponentMap.get(type) ?? RNTextInput;
8288
const isMarkdownEnabled = type === 'markdown';
8389
const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight;
@@ -247,7 +253,7 @@ function BaseTextInput(
247253
styles.textInputContainer,
248254
textInputContainerStyles,
249255
!!contentWidth && StyleUtils.getWidthStyle(textInputWidth),
250-
autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace),
256+
isExternalAutoGrowMeasurement && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace),
251257
!hideFocusedState && isFocused && styles.borderColorFocus,
252258
(!!hasError || !!errorText) && styles.borderColorDanger,
253259
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
@@ -259,6 +265,10 @@ function BaseTextInput(
259265

260266
// Height fix is needed only for Text single line inputs
261267
const shouldApplyHeight = !isMultiline && !isMarkdownEnabled;
268+
269+
// Fix iOS cursor jumping when entering first character using HW keyboard https://github.com/Expensify/App/pull/59078#issuecomment-2802834037
270+
const selection = inputProps.selection?.end === 0 && inputProps.selection?.start === 0 ? undefined : inputProps.selection;
271+
262272
return (
263273
<>
264274
<View style={[containerStyles]}>
@@ -345,8 +355,8 @@ function BaseTextInput(
345355
placeholderTextColor={placeholderTextColor ?? theme.placeholderText}
346356
underlineColorAndroid="transparent"
347357
style={[
348-
styles.flex1,
349-
styles.w100,
358+
!autoGrow && styles.flex1,
359+
!autoGrow && styles.w100,
350360
inputStyle,
351361
(!hasLabel || isMultiline) && styles.pv0,
352362
inputPaddingLeft,
@@ -377,7 +387,7 @@ function BaseTextInput(
377387
keyboardType={inputProps.keyboardType}
378388
inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE}
379389
value={uncontrolled ? undefined : value}
380-
selection={inputProps.selection}
390+
selection={selection}
381391
readOnly={isReadOnly}
382392
defaultValue={defaultValue}
383393
markdownStyle={markdownStyle}
@@ -441,55 +451,21 @@ function BaseTextInput(
441451
/>
442452
)}
443453
</View>
444-
{!!contentWidth && isPrefixCharacterPaddingCalculated && (
445-
<View
446-
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
447-
onLayout={(e) => {
448-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
449-
return;
450-
}
451-
setTextInputWidth(e.nativeEvent.layout.width);
452-
setTextInputHeight(e.nativeEvent.layout.height);
453-
}}
454-
>
455-
<Text
456-
style={[
457-
inputStyle,
458-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
459-
{width: contentWidth},
460-
]}
461-
>
462-
{/* \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) */}
463-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
464-
</Text>
465-
</View>
466-
)}
467-
{/*
468-
Text input component doesn't support auto grow by default.
469-
This text view is used to calculate width or height of the input value given textStyle in this component.
470-
This Text component is intentionally positioned out of the screen.
471-
*/}
472-
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
473-
<Text
474-
style={[
475-
inputStyle,
476-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
477-
styles.hiddenElementOutsideOfWindow,
478-
styles.visibilityHidden,
479-
]}
480-
onLayout={(e) => {
481-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
482-
return;
483-
}
484-
// Add +2 to width so that cursor is not cut off / covered at the end of text content
485-
setTextInputWidth(e.nativeEvent.layout.width + 2);
486-
setTextInputHeight(e.nativeEvent.layout.height);
487-
}}
488-
>
489-
{/* \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) */}
490-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
491-
</Text>
492-
)}
454+
<TextInputMeasurement
455+
value={value}
456+
placeholder={placeholder}
457+
contentWidth={contentWidth}
458+
autoGrowHeight={autoGrowHeight}
459+
maxAutoGrowHeight={maxAutoGrowHeight}
460+
width={width}
461+
inputStyle={inputStyle}
462+
inputPaddingLeft={inputPaddingLeft}
463+
autoGrow={autoGrow}
464+
isAutoGrowHeightMarkdown={isAutoGrowHeightMarkdown}
465+
onSetTextInputWidth={setTextInputWidth}
466+
onSetTextInputHeight={setTextInputHeight}
467+
isPrefixCharacterPaddingCalculated={isPrefixCharacterPaddingCalculated}
468+
/>
493469
</>
494470
);
495471
}

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

Lines changed: 17 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';
@@ -466,63 +467,21 @@ function BaseTextInput(
466467
/>
467468
)}
468469
</View>
469-
{!!contentWidth && isPrefixCharacterPaddingCalculated && (
470-
<View
471-
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
472-
onLayout={(e) => {
473-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
474-
return;
475-
}
476-
setTextInputWidth(e.nativeEvent.layout.width);
477-
setTextInputHeight(e.nativeEvent.layout.height);
478-
}}
479-
>
480-
<Text
481-
style={[
482-
inputStyle,
483-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
484-
{width: contentWidth},
485-
]}
486-
>
487-
{/* \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) */}
488-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
489-
</Text>
490-
</View>
491-
)}
492-
{/*
493-
Text input component doesn't support auto grow by default.
494-
We're using a hidden text input to achieve that.
495-
This text view is used to calculate width or height of the input value given textStyle in this component.
496-
This Text component is intentionally positioned out of the screen.
497-
*/}
498-
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
499-
// Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
500-
// Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628
501-
// For mobile Chrome, ensure proper display of the text selection handle (blue bubble down).
502-
// Reference: https://github.com/Expensify/App/issues/34921
503-
<Text
504-
style={[
505-
inputStyle,
506-
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
507-
styles.hiddenElementOutsideOfWindow,
508-
styles.visibilityHidden,
509-
]}
510-
onLayout={(e) => {
511-
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
512-
return;
513-
}
514-
let additionalWidth = 0;
515-
if (isMobileSafari() || isSafari() || isMobileChrome()) {
516-
additionalWidth = 2;
517-
}
518-
setTextInputWidth(e.nativeEvent.layout.width + additionalWidth);
519-
setTextInputHeight(e.nativeEvent.layout.height);
520-
}}
521-
>
522-
{/* \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) */}
523-
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
524-
</Text>
525-
)}
470+
<TextInputMeasurement
471+
value={value}
472+
placeholder={placeholder}
473+
contentWidth={contentWidth}
474+
autoGrowHeight={autoGrowHeight}
475+
maxAutoGrowHeight={maxAutoGrowHeight}
476+
width={width}
477+
inputStyle={inputStyle}
478+
inputPaddingLeft={inputPaddingLeft}
479+
autoGrow={autoGrow}
480+
isAutoGrowHeightMarkdown={isAutoGrowHeightMarkdown}
481+
onSetTextInputWidth={setTextInputWidth}
482+
onSetTextInputHeight={setTextInputHeight}
483+
isPrefixCharacterPaddingCalculated={isPrefixCharacterPaddingCalculated}
484+
/>
526485
</>
527486
);
528487
}
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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
isPrefixCharacterPaddingCalculated,
23+
}: TextInputMeasurementProps) {
24+
const styles = useThemeStyles();
25+
26+
return (
27+
<>
28+
{!!contentWidth && isPrefixCharacterPaddingCalculated && (
29+
<View
30+
style={[inputStyle as ViewStyle, styles.hiddenElementOutsideOfWindow, styles.visibilityHidden, styles.wAuto, inputPaddingLeft]}
31+
onLayout={(e) => {
32+
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
33+
return;
34+
}
35+
onSetTextInputWidth(e.nativeEvent.layout.width);
36+
onSetTextInputHeight(e.nativeEvent.layout.height);
37+
}}
38+
>
39+
<Text
40+
style={[
41+
inputStyle,
42+
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
43+
{width: contentWidth},
44+
]}
45+
>
46+
{/* \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) */}
47+
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
48+
</Text>
49+
</View>
50+
)}
51+
{/*
52+
Text input component doesn't support auto grow by default.
53+
We're using a hidden text input to achieve that.
54+
This text view is used to calculate width or height of the input value given textStyle in this component.
55+
This Text component is intentionally positioned out of the screen.
56+
*/}
57+
{(!!autoGrow || !!autoGrowHeight) && !isAutoGrowHeightMarkdown && (
58+
// Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
59+
// Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628
60+
// For mobile Chrome, ensure proper display of the text selection handle (blue bubble down).
61+
// Reference: https://github.com/Expensify/App/issues/34921
62+
<Text
63+
style={[
64+
inputStyle,
65+
autoGrowHeight && styles.autoGrowHeightHiddenInput(width ?? 0, typeof maxAutoGrowHeight === 'number' ? maxAutoGrowHeight : undefined),
66+
styles.hiddenElementOutsideOfWindow,
67+
styles.visibilityHidden,
68+
]}
69+
onLayout={(e) => {
70+
if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) {
71+
return;
72+
}
73+
let additionalWidth = 0;
74+
if (isMobileSafari() || isSafari() || isMobileChrome()) {
75+
additionalWidth = 2;
76+
}
77+
onSetTextInputWidth(e.nativeEvent.layout.width + additionalWidth);
78+
onSetTextInputHeight(e.nativeEvent.layout.height);
79+
}}
80+
>
81+
{/* \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) */}
82+
{value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder}
83+
</Text>
84+
)}
85+
</>
86+
);
87+
}
88+
89+
TextInputMeasurement.displayName = 'TextInputMeasurement';
90+
91+
export default TextInputMeasurement;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
/** Whether the prefix character padding is calculated */
41+
isPrefixCharacterPaddingCalculated: boolean;
42+
};
43+
44+
export default TextInputMeasurementProps;

0 commit comments

Comments
 (0)