From 761e3cbe78d641c45d621c89e40478f1148b6376 Mon Sep 17 00:00:00 2001
From: Luke Walczak <lukasz.walczak.pwr@gmail.com>
Date: Mon, 18 Sep 2023 22:09:51 +0200
Subject: [PATCH] fix: set input min width (#3941)

---
 example/src/Examples/TextInputExample.tsx     | 64 +++++++++++++++++++
 src/components/TextInput/Label/InputLabel.tsx |  8 ++-
 src/components/TextInput/TextInput.tsx        | 22 +++++++
 src/components/TextInput/TextInputFlat.tsx    |  4 ++
 .../TextInput/TextInputOutlined.tsx           |  6 +-
 src/components/TextInput/types.tsx            |  5 ++
 .../__snapshots__/TextInput.test.tsx.snap     | 25 +++++++-
 7 files changed, 130 insertions(+), 4 deletions(-)

diff --git a/example/src/Examples/TextInputExample.tsx b/example/src/Examples/TextInputExample.tsx
index a8cab508da..d6bdd29196 100644
--- a/example/src/Examples/TextInputExample.tsx
+++ b/example/src/Examples/TextInputExample.tsx
@@ -697,6 +697,57 @@ const TextInputExample = () => {
                 />
               </View>
             ) : null}
+            <View style={styles.row}>
+              <TextInput
+                mode="outlined"
+                label="CVV"
+                placeholder="CVV"
+                keyboardType="phone-pad"
+                maxLength={3}
+              />
+            </View>
+            <View style={styles.row}>
+              <TextInput
+                mode="flat"
+                label="CVV"
+                placeholder="CVV"
+                keyboardType="phone-pad"
+                maxLength={3}
+              />
+            </View>
+            <View style={styles.row}>
+              <TextInput
+                mode="outlined"
+                label="Code"
+                placeholder="Code"
+                keyboardType="phone-pad"
+                maxLength={4}
+              />
+            </View>
+            <View style={styles.row}>
+              <TextInput
+                mode="flat"
+                label="Code"
+                placeholder="Code"
+                keyboardType="phone-pad"
+                maxLength={4}
+              />
+            </View>
+            <View style={styles.row}>
+              <TextInput
+                mode="flat"
+                label="Month"
+                placeholder="Month"
+                style={styles.month}
+              />
+              <TextInput
+                mode="flat"
+                label="Year"
+                placeholder="Year"
+                keyboardType="phone-pad"
+                style={styles.year}
+              />
+            </View>
           </List.Accordion>
         </List.AccordionGroup>
       </ScreenWrapper>
@@ -745,6 +796,19 @@ const styles = StyleSheet.create({
   fixedHeight: {
     height: 100,
   },
+  row: {
+    margin: 8,
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+  },
+  month: {
+    flex: 1,
+    marginRight: 4,
+  },
+  year: {
+    flex: 1,
+    marginLeft: 4,
+  },
 });
 
 export default TextInputExample;
diff --git a/src/components/TextInput/Label/InputLabel.tsx b/src/components/TextInput/Label/InputLabel.tsx
index a2d8d7845a..df5ade3812 100644
--- a/src/components/TextInput/Label/InputLabel.tsx
+++ b/src/components/TextInput/Label/InputLabel.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { Animated, StyleSheet } from 'react-native';
+import { Animated, StyleSheet, Dimensions } from 'react-native';
 
 import AnimatedText from '../../Typography/AnimatedText';
 import type { InputLabelProps } from '../types';
@@ -16,6 +16,7 @@ const InputLabel = (props: InputLabelProps) => {
     label,
     labelError,
     onLayoutAnimatedText,
+    onLabelTextLayout,
     hasActiveOutline,
     activeColor,
     placeholderStyle,
@@ -40,6 +41,8 @@ const InputLabel = (props: InputLabelProps) => {
     testID,
   } = props;
 
+  const { width } = Dimensions.get('window');
+
   const paddingOffset =
     paddingLeft && paddingRight ? { paddingLeft, paddingRight } : {};
 
@@ -107,7 +110,7 @@ const InputLabel = (props: InputLabelProps) => {
       style={[
         StyleSheet.absoluteFill,
         styles.labelContainer,
-        { opacity },
+        { opacity, width },
         labelTranslationX,
       ]}
     >
@@ -127,6 +130,7 @@ const InputLabel = (props: InputLabelProps) => {
       <AnimatedText
         variant="bodySmall"
         onLayout={onLayoutAnimatedText}
+        onTextLayout={onLabelTextLayout}
         style={[
           placeholderStyle,
           {
diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx
index 7ecf3d1a4b..9b84397341 100644
--- a/src/components/TextInput/TextInput.tsx
+++ b/src/components/TextInput/TextInput.tsx
@@ -6,6 +6,8 @@ import {
   TextInput as NativeTextInput,
   TextStyle,
   ViewStyle,
+  NativeSyntheticEvent,
+  TextLayoutEventData,
 } from 'react-native';
 
 import TextInputAffix, {
@@ -248,6 +250,10 @@ const TextInput = forwardRef<TextInputHandles, Props>(
     // Use value from props instead of local state when input is controlled
     const value = isControlled ? rest.value : uncontrolledValue;
 
+    const [labelTextLayout, setLabelTextLayout] = React.useState({
+      width: 33,
+    });
+
     const [labelLayout, setLabelLayout] = React.useState<{
       measured: boolean;
       width: number;
@@ -447,6 +453,18 @@ const TextInput = forwardRef<TextInputHandles, Props>(
       [labelLayout.height, labelLayout.width]
     );
 
+    const handleLabelTextLayout = React.useCallback(
+      ({ nativeEvent }: NativeSyntheticEvent<TextLayoutEventData>) => {
+        setLabelTextLayout({
+          width: nativeEvent.lines.reduce(
+            (acc, line) => acc + Math.ceil(line.width),
+            0
+          ),
+        });
+      },
+      []
+    );
+
     const forceFocus = React.useCallback(() => root.current?.focus(), []);
 
     const { maxFontSizeMultiplier = 1.5 } = rest;
@@ -469,6 +487,7 @@ const TextInput = forwardRef<TextInputHandles, Props>(
             focused,
             placeholder,
             value,
+            labelTextLayout,
             labelLayout,
             leftLayout,
             rightLayout,
@@ -481,6 +500,7 @@ const TextInput = forwardRef<TextInputHandles, Props>(
           onBlur={handleBlur}
           onChangeText={handleChangeText}
           onLayoutAnimatedText={handleLayoutAnimatedText}
+          onLabelTextLayout={handleLabelTextLayout}
           onLeftAffixLayoutChange={onLeftAffixLayoutChange}
           onRightAffixLayoutChange={onRightAffixLayoutChange}
           maxFontSizeMultiplier={maxFontSizeMultiplier}
@@ -506,6 +526,7 @@ const TextInput = forwardRef<TextInputHandles, Props>(
           focused,
           placeholder,
           value,
+          labelTextLayout,
           labelLayout,
           leftLayout,
           rightLayout,
@@ -518,6 +539,7 @@ const TextInput = forwardRef<TextInputHandles, Props>(
         onBlur={handleBlur}
         onChangeText={handleChangeText}
         onLayoutAnimatedText={handleLayoutAnimatedText}
+        onLabelTextLayout={handleLabelTextLayout}
         onLeftAffixLayoutChange={onLeftAffixLayoutChange}
         onRightAffixLayoutChange={onRightAffixLayoutChange}
         maxFontSizeMultiplier={maxFontSizeMultiplier}
diff --git a/src/components/TextInput/TextInputFlat.tsx b/src/components/TextInput/TextInputFlat.tsx
index f90b4bedcd..44e163838a 100644
--- a/src/components/TextInput/TextInputFlat.tsx
+++ b/src/components/TextInput/TextInputFlat.tsx
@@ -65,6 +65,7 @@ const TextInputFlat = ({
   onBlur,
   onChangeText,
   onLayoutAnimatedText,
+  onLabelTextLayout,
   onLeftAffixLayoutChange,
   onRightAffixLayoutChange,
   left,
@@ -254,6 +255,7 @@ const TextInputFlat = ({
   const labelProps = {
     label,
     onLayoutAnimatedText,
+    onLabelTextLayout,
     placeholderOpacity,
     labelError: error,
     placeholderStyle: styles.placeholder,
@@ -405,6 +407,8 @@ const TextInputFlat = ({
                 : I18nManager.getConstants().isRTL
                 ? 'right'
                 : 'left',
+              minWidth:
+                parentState.labelTextLayout.width + 2 * FLAT_INPUT_OFFSET,
             },
             Platform.OS === 'web' && { outline: 'none' },
             adornmentStyleAdjustmentForNativeInput,
diff --git a/src/components/TextInput/TextInputOutlined.tsx b/src/components/TextInput/TextInputOutlined.tsx
index 025316c395..b8430c0ce9 100644
--- a/src/components/TextInput/TextInputOutlined.tsx
+++ b/src/components/TextInput/TextInputOutlined.tsx
@@ -65,6 +65,7 @@ const TextInputOutlined = ({
   onBlur,
   onChangeText,
   onLayoutAnimatedText,
+  onLabelTextLayout,
   onLeftAffixLayoutChange,
   onRightAffixLayoutChange,
   left,
@@ -200,6 +201,7 @@ const TextInputOutlined = ({
   const labelProps = {
     label,
     onLayoutAnimatedText,
+    onLabelTextLayout,
     placeholderOpacity,
     labelError: error,
     placeholderStyle,
@@ -376,6 +378,9 @@ const TextInputOutlined = ({
                   ? 'right'
                   : 'left',
                 paddingHorizontal: INPUT_PADDING_HORIZONTAL,
+                minWidth:
+                  parentState.labelTextLayout.width +
+                  2 * INPUT_PADDING_HORIZONTAL,
               },
               Platform.OS === 'web' && { outline: 'none' },
               adornmentStyleAdjustmentForNativeInput,
@@ -398,7 +403,6 @@ const styles = StyleSheet.create({
   },
   input: {
     margin: 0,
-    zIndex: 1,
   },
   inputOutlined: {
     paddingTop: 8,
diff --git a/src/components/TextInput/types.tsx b/src/components/TextInput/types.tsx
index 8e1b30b86b..659c960e8b 100644
--- a/src/components/TextInput/types.tsx
+++ b/src/components/TextInput/types.tsx
@@ -8,6 +8,8 @@ import type {
   StyleProp,
   ViewProps,
   ViewStyle,
+  NativeSyntheticEvent,
+  TextLayoutEventData,
 } from 'react-native';
 
 import type { $Omit, InternalTheme, ThemeProp } from './../../types';
@@ -70,6 +72,7 @@ export type State = {
   focused: boolean;
   placeholder?: string;
   value?: string;
+  labelTextLayout: { width: number };
   labelLayout: { measured: boolean; width: number; height: number };
   leftLayout: { height: number | null; width: number | null };
   rightLayout: { height: number | null; width: number | null };
@@ -83,6 +86,7 @@ export type ChildTextInputProps = {
   forceFocus: () => void;
   onChangeText?: (value: string) => void;
   onLayoutAnimatedText: (args: any) => void;
+  onLabelTextLayout: (event: NativeSyntheticEvent<TextLayoutEventData>) => void;
   onLeftAffixLayoutChange: (event: LayoutChangeEvent) => void;
   onRightAffixLayoutChange: (event: LayoutChangeEvent) => void;
 } & $Omit<TextInputTypesWithoutMode, 'theme'> & { theme: InternalTheme };
@@ -114,6 +118,7 @@ export type LabelProps = {
   errorColor?: string;
   labelError?: boolean | null;
   onLayoutAnimatedText: (args: any) => void;
+  onLabelTextLayout: (event: NativeSyntheticEvent<TextLayoutEventData>) => void;
   roundness: number;
   maxFontSizeMultiplier?: number | undefined | null;
   testID?: string;
diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap
index d977929c39..1938f874e0 100644
--- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap
@@ -62,6 +62,7 @@ exports[`correctly applies a component as the text label 1`] = `
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -71,6 +72,7 @@ exports[`correctly applies a component as the text label 1`] = `
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -188,6 +190,7 @@ exports[`correctly applies a component as the text label 1`] = `
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 16,
             "textAlign": "left",
@@ -270,6 +273,7 @@ exports[`correctly applies cursorColor prop 1`] = `
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -279,6 +283,7 @@ exports[`correctly applies cursorColor prop 1`] = `
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -380,6 +385,7 @@ exports[`correctly applies cursorColor prop 1`] = `
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 16,
             "textAlign": "left",
@@ -462,6 +468,7 @@ exports[`correctly applies default textAlign based on default RTL 1`] = `
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -471,6 +478,7 @@ exports[`correctly applies default textAlign based on default RTL 1`] = `
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -572,6 +580,7 @@ exports[`correctly applies default textAlign based on default RTL 1`] = `
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 16,
             "textAlign": "left",
@@ -648,6 +657,7 @@ exports[`correctly applies height to multiline Outline TextInput 1`] = `
                 "translateX": 3,
               },
             ],
+            "width": 750,
             "zIndex": 3,
           }
         }
@@ -698,6 +708,7 @@ exports[`correctly applies height to multiline Outline TextInput 1`] = `
           maxFontSizeMultiplier={1.5}
           numberOfLines={1}
           onLayout={[Function]}
+          onTextLayout={[Function]}
           style={
             {
               "color": "rgba(103, 80, 164, 1)",
@@ -782,7 +793,6 @@ exports[`correctly applies height to multiline Outline TextInput 1`] = `
           [
             {
               "margin": 0,
-              "zIndex": 1,
             },
             {
               "height": 100,
@@ -798,6 +808,7 @@ exports[`correctly applies height to multiline Outline TextInput 1`] = `
               "fontWeight": undefined,
               "letterSpacing": 0.15,
               "lineHeight": 19.2,
+              "minWidth": 65,
               "paddingHorizontal": 16,
               "textAlign": "left",
               "textAlignVertical": "top",
@@ -880,6 +891,7 @@ exports[`correctly applies paddingLeft from contentStyleProp 1`] = `
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -889,6 +901,7 @@ exports[`correctly applies paddingLeft from contentStyleProp 1`] = `
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -990,6 +1003,7 @@ exports[`correctly applies paddingLeft from contentStyleProp 1`] = `
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 16,
             "textAlign": "left",
@@ -1074,6 +1088,7 @@ exports[`correctly applies textAlign center 1`] = `
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -1083,6 +1098,7 @@ exports[`correctly applies textAlign center 1`] = `
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -1184,6 +1200,7 @@ exports[`correctly applies textAlign center 1`] = `
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 16,
             "textAlign": "center",
@@ -1266,6 +1283,7 @@ exports[`correctly renders left-side affix adornment, and right-side icon adornm
               "translateX": 4,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -1275,6 +1293,7 @@ exports[`correctly renders left-side affix adornment, and right-side icon adornm
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -1376,6 +1395,7 @@ exports[`correctly renders left-side affix adornment, and right-side icon adornm
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 16,
             "paddingRight": 56,
             "textAlign": "left",
@@ -1660,6 +1680,7 @@ exports[`correctly renders left-side icon adornment, and right-side affix adornm
               "translateX": 14,
             },
           ],
+          "width": 750,
           "zIndex": 3,
         }
       }
@@ -1669,6 +1690,7 @@ exports[`correctly renders left-side icon adornment, and right-side affix adornm
         maxFontSizeMultiplier={1.5}
         numberOfLines={1}
         onLayout={[Function]}
+        onTextLayout={[Function]}
         style={
           {
             "color": "rgba(103, 80, 164, 1)",
@@ -1770,6 +1792,7 @@ exports[`correctly renders left-side icon adornment, and right-side affix adornm
             "fontWeight": undefined,
             "letterSpacing": 0.15,
             "lineHeight": 19.2,
+            "minWidth": 65,
             "paddingLeft": 56,
             "paddingRight": 56,
             "textAlign": "left",