Repository: BetterAngelsLA/monorepo
Pull request #1263: Use Input with Picker and DateField (DEV-1774)
Author: login: tglaz
Created: 2025-04-27T22:21:32Z | Merged: 2025-04-29T01:22:24Z

### Blur input field after Picker is selected
https://betterangels.atlassian.net/browse/DEV-1774


- `Input_V2` updates
  - add `asSelect` option for input that is meant to be "inactive" (a focusable input used with inputs like Picker etc...)
  -  add left and right `slots` to render content on left/right of the input, with option to focus input on Click
- update `Picker_V2 iOS` to use `Input_V2` to render value
- update `DatePicker_V2` to use `Input_V2` to render value
- update components to use `TMarginProps` with getMarginStyles helper

## Summary by Sourcery

Refactor `DatePicker_V2` and `Picker_V2` (iOS) to use the enhanced `Input_V2` component for a consistent input appearance and improved interaction behavior.

New Features:
- Add `slotLeft` and `slotRight` props to `Input_V2` for rendering components within its borders.
- Add `asSelect` prop to `Input_V2` to configure it for triggering pickers/selects without showing the keyboard.

Bug Fixes:
- Prevent keyboard display when opening `DatePicker_V2` and `Picker_V2` (iOS).

Enhancements:
- Utilize `Input_V2` within `DatePicker_V2`.
- Utilize `Input_V2` within `Picker_V2` (iOS).
- Standardize margin prop handling across UI components using new helpers.
- Update form components to align with revised `DatePicker`, `Picker`, and `SingleSelect` props.

Chores:
- Introduce reusable `InputSlot` and `InputClearIcon` components.
- Add `omitMarginProps` helper function to static styles library.


Diff:
diff --git a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx
index 49a7327a3c..a441f9f5c3 100644
--- a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx
+++ b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx
@@ -2,7 +2,7 @@ import { Regex } from '@monorepo/expo/shared/static';
 import {
   ActionModal,
   ControlledInput,
-  DatePicker,
+  DatePicker_V2 as DatePicker,
   Form,
   SingleSelect,
 } from '@monorepo/expo/shared/ui-components';
@@ -168,16 +168,13 @@ export function PersonalInfoForm() {
 
       <Form.Field title="Date of Birth">
         <DatePicker
-          disabled
           maxDate={new Date()}
-          pattern={Regex.date}
           mode="date"
           format="MM/dd/yyyy"
           placeholder="Enter Date of Birth"
           minDate={new Date('1900-01-01')}
-          mt="xs"
           value={dateOfBirth}
-          setValue={(date) => setValue('dateOfBirth', date)}
+          onChange={(date) => setValue('dateOfBirth', date)}
         />
       </Form.Field>
 
diff --git a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HmisProfileForm/HmisProfileForm.tsx b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HmisProfileForm/HmisProfileForm.tsx
index d0908c2d9a..f812b6afb0 100644
--- a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HmisProfileForm/HmisProfileForm.tsx
+++ b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HmisProfileForm/HmisProfileForm.tsx
@@ -171,6 +171,7 @@ export function HmisProfileForm(props: TProps) {
             rules={{ required: 'Type of HMIS ID is required' }}
             render={({ field }) => (
               <SingleSelect
+                required={true}
                 disabled={isLoading}
                 label="Type of HMIS ID"
                 placeholder="Select type of HMIS ID"
@@ -186,6 +187,7 @@ export function HmisProfileForm(props: TProps) {
 
           <ControlledInput
             control={control}
+            required={true}
             disabled={isLoading}
             label={'HMIS ID'}
             name={'hmisId'}
diff --git a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HouseholdMemberForm/HouseholdMemberForm.tsx b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HouseholdMemberForm/HouseholdMemberForm.tsx
index fbb8646dcd..69d0a27ffe 100644
--- a/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HouseholdMemberForm/HouseholdMemberForm.tsx
+++ b/libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HouseholdMemberForm/HouseholdMemberForm.tsx
@@ -1,4 +1,3 @@
-import { Regex } from '@monorepo/expo/shared/static';
 import {
   ControlledInput,
   DatePicker_V2 as DatePicker,
@@ -169,7 +168,7 @@ export function HouseholdMemberForm(props: TProps) {
           <Controller
             name="gender"
             control={control}
-            render={({ field }) => (
+            render={({ field: { value, onChange } }) => (
               <SingleSelect
                 disabled={isLoading}
                 label="Gender"
@@ -177,8 +176,8 @@ export function HouseholdMemberForm(props: TProps) {
                 items={Object.entries(enumDisplayGender).map(
                   ([value, displayValue]) => ({ value, displayValue })
                 )}
-                selectedValue={field.value}
-                onChange={(value) => field.onChange(value)}
+                selectedValue={value}
+                onChange={(value) => onChange(value)}
                 error={errors.gender?.message}
               />
             )}
@@ -192,14 +191,13 @@ export function HouseholdMemberForm(props: TProps) {
                 label="Date of Birth"
                 disabled={isLoading}
                 maxDate={new Date()}
-                pattern={Regex.date}
                 mode="date"
                 format="MM/dd/yyyy"
                 placeholder="Enter date of birth"
                 minDate={new Date('1900-01-01')}
-                mt="xs"
                 value={value}
                 onChange={onChange}
+                error={errors.dateOfBirth?.message}
               />
             )}
           />
diff --git a/libs/expo/shared/static/src/lib/index.ts b/libs/expo/shared/static/src/lib/index.ts
index eb2e7543aa..5aaf7a9219 100644
--- a/libs/expo/shared/static/src/lib/index.ts
+++ b/libs/expo/shared/static/src/lib/index.ts
@@ -1,6 +1,11 @@
 export { Colors } from './colors';
 export { FontSizes } from './fontSizes';
-export { TMarginProps, getMarginStyles } from './margins';
+export {
+  TMarginProps,
+  getMarginStyles,
+  marginPropKeys,
+  omitMarginProps,
+} from './margins';
 export { MimeTypes, TMimeType } from './mimeTypes';
 export { Radiuses, TRadius, TRadiusKey } from './radiuses';
 export { Regex } from './regex';
diff --git a/libs/expo/shared/static/src/lib/margins.ts b/libs/expo/shared/static/src/lib/margins.ts
index 979438e822..ffba7a180d 100644
--- a/libs/expo/shared/static/src/lib/margins.ts
+++ b/libs/expo/shared/static/src/lib/margins.ts
@@ -19,7 +19,17 @@ const marginMap: Record<keyof TMarginProps, string> = {
   mx: 'marginHorizontal',
 };
 
-const marginMapKeys = Object.keys(marginMap);
+export const marginPropKeys = Object.keys(marginMap) as (keyof TMarginProps)[];
+
+export function omitMarginProps<T extends Record<string, unknown>>(props: T) {
+  const clone = { ...props };
+
+  for (const key of marginPropKeys) {
+    delete clone[key];
+  }
+
+  return clone as Omit<T, keyof TMarginProps>;
+}
 
 export const getMarginStyles = (props: TMarginProps) => {
   if (!props) {
@@ -28,8 +38,8 @@ export const getMarginStyles = (props: TMarginProps) => {
 
   const styles: Record<string, number> = {};
 
-  for (const key of marginMapKeys) {
-    const marginProp = props[key as keyof TMarginProps];
+  for (const key of marginPropKeys) {
+    const marginProp = props[key];
 
     if (!marginProp) {
       continue;
diff --git a/libs/expo/shared/ui-components/src/lib/DatePicker_V2/DatePicker.tsx b/libs/expo/shared/ui-components/src/lib/DatePicker_V2/DatePicker.tsx
index 28a09562f1..adeeb7a851 100644
--- a/libs/expo/shared/ui-components/src/lib/DatePicker_V2/DatePicker.tsx
+++ b/libs/expo/shared/ui-components/src/lib/DatePicker_V2/DatePicker.tsx
@@ -1,52 +1,39 @@
 import { CalendarLineIcon, ClockIcon } from '@monorepo/expo/shared/icons';
 import {
   Colors,
-  FontSizes,
   Radiuses,
   Spacings,
   TMarginProps,
   getMarginStyles,
+  omitMarginProps,
 } from '@monorepo/expo/shared/static';
 import DateTimePicker from '@react-native-community/datetimepicker';
 import { format as dateFnsFormat } from 'date-fns';
 import { useState } from 'react';
-import { RegisterOptions } from 'react-hook-form';
 import {
   Keyboard,
   Platform,
-  Pressable,
   StyleProp,
   StyleSheet,
-  Text,
-  TextInput,
   View,
   ViewStyle,
 } from 'react-native';
 import Button from '../Button';
-
-type TRules = Omit<
-  RegisterOptions,
-  'disabled' | 'valueAsNumber' | 'valueAsDate' | 'setValueAs'
->;
+import Input from '../Input_V2';
 
 interface IDatePickerProps extends TMarginProps {
-  label?: string;
   mode: 'date' | 'time';
-  height?: 40 | 56 | 200;
+  onChange: (date: Date) => void;
+  style?: StyleProp<ViewStyle>;
+  label?: string;
   placeholder?: string;
   required?: boolean;
-  pattern?: RegExp;
   disabled?: boolean;
-  error?: boolean;
-  rules?: TRules;
+  error?: string;
   format: string;
-  componentStyle?: StyleProp<ViewStyle>;
   onBlur?: () => void;
   minDate?: Date;
   maxDate?: Date;
-  pickerMode?: 'countdown' | 'date' | 'time' | 'datetime';
-  onChange: (date: Date) => void;
-  initialDate?: Date;
   value?: Date | null;
 }
 
@@ -55,80 +42,46 @@ export function DatePicker(props: IDatePickerProps) {
     label,
     onChange,
     error,
-    required,
-    disabled,
-    componentStyle,
-    height = 56,
+    style,
     minDate,
     maxDate,
     mode,
-    placeholder,
     format = 'MM/dd/yyyy',
-    pattern,
-    initialDate,
     value,
     ...rest
   } = props;
-
   const [pickerVisible, setPickerVisible] = useState(false);
 
+  const nonMarginOtherProps = omitMarginProps(rest);
+
   return (
     <View
       style={[
-        styles.inputContainer,
-        componentStyle,
+        styles.container,
         {
           ...getMarginStyles(props),
         },
+        style,
       ]}
     >
-      {label && (
-        <View style={styles.label}>
-          <Text style={styles.labelText}>{label}</Text>
-          {required && <Text style={styles.required}>*</Text>}
-        </View>
-      )}
-
-      <View
-        style={[
-          styles.input,
-          {
-            borderColor: error ? 'red' : Colors.NEUTRAL_LIGHT,
-          },
-        ]}
-      >
-        <TextInput
-          placeholder={placeholder}
-          maxLength={18}
-          style={[
-            styles.textInput,
-            {
-              height,
-              ...Platform.select({
-                web: {
-                  outline: 'none',
-                },
-              }),
-            },
-          ]}
-          value={value ? dateFnsFormat(value, format) : undefined}
-          editable={!disabled}
-          {...rest}
-        />
-        <Pressable
-          accessible
-          accessibilityRole="button"
-          accessibilityLabel="open date picker"
-          accessibilityHint="Opens the date picker to select a date"
-          onPress={() => {
-            setPickerVisible(true);
-            Keyboard.dismiss();
-          }}
-          style={styles.icon}
-        >
-          <FieldIcon mode={mode} />
-        </Pressable>
-      </View>
+      <Input
+        asSelect
+        value={value ? dateFnsFormat(value, format) : undefined}
+        label={label}
+        error={!!error}
+        errorMessage={error}
+        onFocus={() => {
+          setPickerVisible(!pickerVisible);
+          Keyboard.dismiss();
+        }}
+        slotRight={{
+          focusableInput: true,
+          component: <FieldIcon mode={mode} />,
+          accessibilityLabel: 'date selector',
+          accessibilityHint: `opens date selector for ${label || 'date field'}`,
+        }}
+        {...nonMarginOtherProps}
+      />
       {pickerVisible && (
         <View style={{ marginTop: Spacings.xs }}>
           <DateTimePicker
@@ -181,52 +134,12 @@ function FieldIcon({ mode }: { mode: 'date' | 'time' }) {
 }
 
 const styles = StyleSheet.create({
-  inputContainer: {
-    position: 'relative',
+  container: {
     width: '100%',
   },
-  input: {
-    position: 'relative',
-    fontFamily: 'Poppins-Regular',
-    backgroundColor: Colors.WHITE,
-    borderWidth: 1,
-    borderRadius: Radiuses.xs,
-    justifyContent: 'center',
-  },
-  textInput: {
-    color: Colors.PRIMARY_EXTRA_DARK,
-    paddingLeft: Spacings.sm,
-    paddingRight: 38,
-    fontFamily: 'Poppins-Regular',
-    fontSize: FontSizes.md.fontSize,
-  },
   dateTimePicker: {
     backgroundColor: Colors.WHITE,
     borderRadius: Radiuses.xs,
     overflow: 'hidden',
   },
-  label: {
-    flexDirection: 'row',
-    marginBottom: Spacings.xs,
-  },
-  labelText: {
-    fontSize: FontSizes.sm.fontSize,
-    lineHeight: FontSizes.sm.lineHeight,
-    color: Colors.PRIMARY_EXTRA_DARK,
-    fontFamily: 'Poppins-Regular',
-  },
-  required: {
-    marginLeft: 2,
-    color: 'red',
-  },
-  icon: {
-    position: 'absolute',
-    left: 0,
-    top: 0,
-    width: '100%',
-    height: '100%',
-    alignItems: 'flex-end',
-    justifyContent: 'center',
-    paddingRight: Spacings.sm,
-  },
 });
diff --git a/libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx b/libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx
index c19e500726..34e175ab23 100644
--- a/libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx
+++ b/libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx
@@ -1,15 +1,15 @@
-import { PlusIcon } from '@monorepo/expo/shared/icons';
 import {
   Colors,
   FontSizes,
   Radiuses,
   Spacings,
-  TSpacing,
+  TMarginProps,
+  getMarginStyles,
+  omitMarginProps,
 } from '@monorepo/expo/shared/static';
-import { ReactNode } from 'react';
+import { useRef } from 'react';
 import {
   Platform,
-  Pressable,
   StyleProp,
   StyleSheet,
   TextInput,
@@ -19,61 +19,60 @@ import {
 } from 'react-native';
 import FormFieldError from '../FormFieldError';
 import FormFieldLabel from '../FormFieldLabel';
+import { InputClearIcon } from './InputClearIcon';
+import { InputSlot, TInputSlot } from './InputSlot';
 
-export interface IInputProps extends TextInputProps {
+export interface IInputProps extends TMarginProps, TextInputProps {
   label?: string;
   required?: boolean;
   disabled?: boolean;
   error?: boolean;
   errorMessage?: string;
-  componentStyle?: StyleProp<ViewStyle>;
-  mb?: TSpacing;
-  mt?: TSpacing;
-  my?: TSpacing;
-  mx?: TSpacing;
-  ml?: TSpacing;
-  mr?: TSpacing;
-  icon?: ReactNode;
-  onDelete?: () => void;
+  style?: StyleProp<ViewStyle>;
+  inputStyle?: StyleProp<ViewStyle>;
   borderRadius?: number;
+  onDelete?: () => void;
+  slotLeft?: TInputSlot;
+  slotRight?: TInputSlot;
+  asSelect?: boolean;
 }
 
+const defaultAsSelectProps: TextInputProps = {
+  showSoftInputOnFocus: false,
+  caretHidden: true,
+};
+
 export function Input(props: IInputProps) {
   const {
     label,
     error,
     required,
     disabled,
-    componentStyle,
-    mb,
-    mt,
-    my,
-    mx,
-    ml,
-    mr,
-    icon,
+    style,
+    inputStyle,
     value,
     onDelete,
+    slotLeft,
+    slotRight,
     autoCorrect = false,
     autoCapitalize = 'none',
     borderRadius = Radiuses.xs,
     errorMessage,
-    style,
+    asSelect,
     ...rest
   } = props;
 
+  const inputRef = useRef<TextInput>(null);
+  const nonMarginOtherProps = omitMarginProps(rest);
+  const asSelectProps = asSelect ? defaultAsSelectProps : {};
+
   return (
     <View
       style={[
-        styles.inputContainer,
-        componentStyle,
+        styles.container,
+        style,
         {
-          marginBottom: mb && Spacings[mb],
-          marginTop: mt && Spacings[mt],
-          marginLeft: ml && Spacings[ml],
-          marginRight: mr && Spacings[mr],
-          marginHorizontal: mx && Spacings[mx],
-          marginVertical: my && Spacings[my],
+          ...getMarginStyles(props),
         },
       ]}
     >
@@ -82,21 +81,28 @@ export function Input(props: IInputProps) {
         style={[
           styles.input,
           {
-            paddingLeft: icon ? Spacings.sm : 0,
             borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT,
             borderRadius,
           },
         ]}
       >
-        {icon}
+        {slotLeft && (
+          <InputSlot
+            placement="left"
+            disabled={disabled}
+            inputRef={inputRef}
+            {...slotLeft}
+          />
+        )}
+
         <TextInput
+          ref={inputRef}
           style={[
             {
               color: disabled
                 ? Colors.NEUTRAL_LIGHT
                 : Colors.PRIMARY_EXTRA_DARK,
-              paddingLeft: icon ? Spacings.xs : Spacings.sm,
-              paddingRight: onDelete ? 38 : Spacings.sm,
+              paddingHorizontal: Spacings.sm,
               flex: 1,
               fontFamily: 'Poppins-Regular',
               fontSize: FontSizes.md.fontSize,
@@ -108,39 +114,47 @@ export function Input(props: IInputProps) {
                 },
               }),
             },
-            style,
+            inputStyle,
           ]}
           editable={!disabled}
           autoCorrect={autoCorrect}
           autoCapitalize={autoCapitalize}
-          {...rest}
+          {...asSelectProps}
+          {...nonMarginOtherProps}
           value={value}
         />
+
         {value && onDelete && (
-          <Pressable
+          <InputSlot
+            placement="right"
             disabled={disabled}
-            accessible
-            accessibilityRole="button"
-            accessibilityLabel="delete icon"
-            accessibilityHint="deletes input's value"
             onPress={onDelete}
-            style={[{ opacity: disabled ? 0.5 : 1 }, styles.pressable]}
-          >
-            <View style={styles.icon}>
-              <PlusIcon size="xs" rotate="45deg" />
-            </View>
-          </Pressable>
+            accessibilityLabel="clear input"
+            accessibilityHint={`clear value for ${label || 'input'}`}
+            component={<InputClearIcon />}
+          />
+        )}
+
+        {slotRight && (
+          <InputSlot
+            placement="right"
+            disabled={disabled}
+            inputRef={inputRef}
+            {...slotRight}
+          />
         )}
       </View>
+
       {errorMessage && <FormFieldError message={errorMessage} />}
     </View>
   );
 }
 
 const styles = StyleSheet.create({
-  inputContainer: {
+  container: {
     position: 'relative',
     width: '100%',
+    display: 'flex',
   },
   input: {
     position: 'relative',
@@ -162,23 +176,6 @@ const styles = StyleSheet.create({
   },
   required: {
     marginLeft: 2,
-    color: 'red',
-  },
-  pressable: {
-    position: 'absolute',
-    top: 12,
-    right: Spacings.xs,
-    height: Spacings.lg,
-    width: Spacings.lg,
-    alignItems: 'center',
-    justifyContent: 'center',
-  },
-  icon: {
-    height: Spacings.sm,
-    width: Spacings.sm,
-    backgroundColor: Colors.NEUTRAL_LIGHT,
-    borderRadius: Radiuses.xxxl,
-    alignItems: 'center',
-    justifyContent: 'center',
+    color: Colors.ERROR_DARK,
   },
 });
diff --git a/libs/expo/shared/ui-components/src/lib/Input_V2/InputClearIcon.tsx b/libs/expo/shared/ui-components/src/lib/Input_V2/InputClearIcon.tsx
new file mode 100644
index 0000000000..21766599b1
--- /dev/null
+++ b/libs/expo/shared/ui-components/src/lib/Input_V2/InputClearIcon.tsx
@@ -0,0 +1,22 @@
+import { PlusIcon } from '@monorepo/expo/shared/icons';
+import { Colors, Radiuses, Spacings } from '@monorepo/expo/shared/static';
+import { StyleSheet, View } from 'react-native';
+
+export function InputClearIcon() {
+  return (
+    <View style={styles.container}>
+      <PlusIcon size="xs" rotate="45deg" />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: Spacings.sm,
+    width: Spacings.sm,
+    backgroundColor: Colors.NEUTRAL_LIGHT,
+    borderRadius: Radiuses.xxxl,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+});
diff --git a/libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx b/libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx
new file mode 100644
index 0000000000..4fbda77c26
--- /dev/null
+++ b/libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx
@@ -0,0 +1,95 @@
+import { Spacings } from '@monorepo/expo/shared/static';
+import { ReactNode } from 'react';
+import {
+  Pressable,
+  StyleSheet,
+  TextInput,
+  View,
+  ViewStyle,
+} from 'react-native';
+
+export type TInputSlot = {
+  component: ReactNode;
+  style?: ViewStyle;
+  disabledStyle?: ViewStyle;
+  onPress?: () => void;
+  focusableInput?: boolean; // if true, will trigger focus on inputRef
+  accessibilityLabel?: string;
+  accessibilityHint?: string;
+};
+
+interface TProps extends TInputSlot {
+  placement: 'left' | 'right';
+  disabled?: boolean;
+  inputRef?: React.RefObject<TextInput>;
+}
+
+export function InputSlot(props: TProps) {
+  const {
+    style,
+    placement,
+    disabledStyle,
+    onPress,
+    focusableInput,
+    disabled,
+    accessibilityLabel,
+    accessibilityHint,
+    component,
+    inputRef,
+  } = props;
+
+  const disabledStyles = disabledStyle || { opacity: disabled ? 0.5 : 1 };
+  const placementStyles =
+    placement === 'left' ? styles.slotLeft : styles.slotRight;
+
+  const slotStyle = [
+    styles.container,
+    { ...placementStyles },
+    { ...disabledStyles },
+    style,
+  ];
+
+  const handlePress = () => {
+    if (disabled) {
+      return;
+    }
+
+    if (focusableInput) {
+      inputRef?.current?.focus();
+    }
+
+    if (onPress) {
+      onPress();
+    }
+  };
+
+  if (onPress || focusableInput) {
+    return (
+      <Pressable
+        style={slotStyle}
+        onPress={handlePress}
+        disabled={disabled}
+        accessibilityRole="button"
+        accessibilityLabel={accessibilityLabel}
+        accessibilityHint={accessibilityHint}
+      >
+        <View>{component}</View>
+      </Pressable>
+    );
+  }
+
+  return <View style={slotStyle}>{component}</View>;
+}
+
+const styles = StyleSheet.create({
+  container: {
+    display: 'flex',
+    justifyContent: 'center',
+  },
+  slotLeft: {
+    paddingLeft: Spacings.sm,
+  },
+  slotRight: {
+    paddingRight: Spacings.sm,
+  },
+});
diff --git a/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.ios.tsx b/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.ios.tsx
index fab411907b..6c8b8a8d31 100644
--- a/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.ios.tsx
+++ b/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.ios.tsx
@@ -1,20 +1,29 @@
 import { ChevronLeftIcon } from '@monorepo/expo/shared/icons';
-import { Colors, Radiuses, Spacings } from '@monorepo/expo/shared/static';
+import {
+  Colors,
+  Radiuses,
+  Spacings,
+  getMarginStyles,
+} from '@monorepo/expo/shared/static';
 import { Picker as RNPicker } from '@react-native-picker/picker';
-import { useEffect, useState } from 'react';
-import { Pressable, SafeAreaView, StyleSheet, View } from 'react-native';
+import { useCallback, useEffect, useState } from 'react';
+import {
+  Keyboard,
+  Pressable,
+  SafeAreaView,
+  StyleSheet,
+  View,
+} from 'react-native';
 import Modal from 'react-native-modal';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import FormFieldLabel from '../FormFieldLabel';
+import Input from '../Input_V2';
 import TextBold from '../TextBold';
-import TextRegular from '../TextRegular';
 import { IPickerProps } from './Picker';
 
 export default function Picker(props: IPickerProps) {
   const {
     onChange,
     error,
-    selectedDisplayValue,
     selectedValue,
     placeholder,
     allowSelectNone,
@@ -23,16 +32,8 @@ export default function Picker(props: IPickerProps) {
     label,
     required,
     disabled,
-    mb,
-    mt,
-    my,
-    mx,
-    mr,
-    ml,
   } = props;
-
   const [localValue, setLocalValue] = useState<string | null>(null);
-
   const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
 
   useEffect(() => {
@@ -40,6 +41,7 @@ export default function Picker(props: IPickerProps) {
   }, [selectedValue, setLocalValue]);
 
   function onPressDone() {
+    Keyboard.dismiss();
     setIsModalVisible(false);
 
     if (localValue) {
@@ -57,53 +59,48 @@ export default function Picker(props: IPickerProps) {
     onChange(items[0].value);
   }
 
+  const getDisplayValue = useCallback(
+    (value?: string | null) => {
+      const item = items.find((item) => item.value === value);
+
+      return item?.displayValue ?? item?.value;
+    },
+    [items]
+  );
+
   const insets = useSafeAreaInsets();
   const bottomOffset = insets.bottom;
 
   return (
     <>
-      <View>
-        {label && <FormFieldLabel label={label} required={required} />}
-
-        <Pressable
+      <View
+        style={[
+          {
+            borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT,
+            ...getMarginStyles(props),
+          },
+        ]}
+      >
+        <Input
+          asSelect
           disabled={disabled}
-          onPress={() => setIsModalVisible(true)}
-          style={[
-            styles.selectButton,
-            {
-              borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT,
-              marginBottom: mb && Spacings[mb],
-              marginTop: mt && Spacings[mt],
-              marginLeft: ml && Spacings[ml],
-              marginRight: mr && Spacings[mr],
-              marginHorizontal: mx && Spacings[mx],
-              marginVertical: my && Spacings[my],
-            },
-          ]}
-          accessibilityRole="button"
-        >
-          <TextRegular
-            color={
-              disabled
-                ? Colors.NEUTRAL_LIGHT
-                : selectedDisplayValue || selectedValue
-                ? Colors.PRIMARY_EXTRA_DARK
-                : Colors.NEUTRAL
-            }
-          >
-            {selectedDisplayValue || selectedValue || placeholder}
-          </TextRegular>
-          <ChevronLeftIcon
-            size="sm"
-            rotate={'-90deg'}
-            color={disabled ? Colors.NEUTRAL_LIGHT : Colors.PRIMARY_EXTRA_DARK}
-          />
-        </Pressable>
-        {error && (
-          <TextRegular size="sm" mt="xxs" color={Colors.ERROR}>
-            {error}
-          </TextRegular>
-        )}
+          required={required}
+          placeholder={placeholder}
+          value={getDisplayValue(localValue)}
+          label={label}
+          error={!!error}
+          errorMessage={error}
+          onFocus={() => {
+            Keyboard.dismiss();
+            setIsModalVisible(true);
+          }}
+          slotRight={{
+            focusableInput: true,
+            component: <ChevronLeftIcon size="sm" rotate={'-90deg'} />,
+            accessibilityLabel: `selector for ${label || 'field'}`,
+            accessibilityHint: `opens selector for ${label || 'field'}`,
+          }}
+        />
       </View>
       <Modal
         style={styles.modal}
@@ -159,16 +156,6 @@ export default function Picker(props: IPickerProps) {
 }
 
 const styles = StyleSheet.create({
-  selectButton: {
-    backgroundColor: Colors.WHITE,
-    height: 56,
-    paddingHorizontal: Spacings.sm,
-    borderWidth: 1,
-    borderRadius: Radiuses.xs,
-    flexDirection: 'row',
-    justifyContent: 'space-between',
-    alignItems: 'center',
-  },
   modal: {
     margin: 0,
     flex: 1,
diff --git a/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.tsx b/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.tsx
index 95f4a05fff..e04d5de0e3 100644
--- a/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.tsx
+++ b/libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.tsx
@@ -2,7 +2,8 @@ import {
   Colors,
   Radiuses,
   Spacings,
-  TSpacing,
+  TMarginProps,
+  getMarginStyles,
 } from '@monorepo/expo/shared/static';
 import { Picker as RNPicker } from '@react-native-picker/picker';
 import { StyleSheet, View } from 'react-native';
@@ -11,24 +12,22 @@ import TextRegular from '../TextRegular';
 
 const NONE_VALUE = '__none__';
 
-export interface IPickerProps {
+export type TPickerItem = {
+  displayValue?: string;
+  value: string;
+};
+
+export interface IPickerProps extends TMarginProps {
   onChange: (value: string | null) => void;
   error?: string;
   selectedValue?: string | null;
-  selectedDisplayValue?: string | null;
   placeholder: string;
-  items: { displayValue?: string; value: string }[];
+  items: TPickerItem[];
   label?: string;
   required?: boolean;
   disabled?: boolean;
   selectNoneLabel?: string;
   allowSelectNone?: boolean;
-  mb?: TSpacing;
-  mt?: TSpacing;
-  my?: TSpacing;
-  mx?: TSpacing;
-  ml?: TSpacing;
-  mr?: TSpacing;
 }
 
 export default function Picker(props: IPickerProps) {
@@ -43,12 +42,6 @@ export default function Picker(props: IPickerProps) {
     required,
     selectNoneLabel,
     allowSelectNone,
-    mb,
-    mt,
-    my,
-    mx,
-    ml,
-    mr,
   } = props;
 
   const noneLabel = selectNoneLabel || placeholder || 'Selece one';
@@ -66,12 +59,7 @@ export default function Picker(props: IPickerProps) {
           styles.pickerContainer,
           {
             borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT,
-            marginBottom: mb && Spacings[mb],
-            marginTop: mt && Spacings[mt],
-            marginLeft: ml && Spacings[ml],
-            marginRight: mr && Spacings[mr],
-            marginHorizontal: mx && Spacings[mx],
-            marginVertical: my && Spacings[my],
+            ...getMarginStyles(props),
           },
         ]}
       >
@@ -100,12 +88,12 @@ export default function Picker(props: IPickerProps) {
             />
           ))}
         </RNPicker>
-        {error && (
-          <TextRegular size="sm" color={Colors.ERROR}>
-            {error}
-          </TextRegular>
-        )}
       </View>
+      {error && (
+        <TextRegular mt="xxs" size="sm" color={Colors.ERROR}>
+          {error}
+        </TextRegular>
+      )}
     </View>
   );
 }
diff --git a/libs/expo/shared/ui-components/src/lib/SingleSelect/SingleSelect.tsx b/libs/expo/shared/ui-components/src/lib/SingleSelect/SingleSelect.tsx
index 7737848ff4..63b4c4c366 100644
--- a/libs/expo/shared/ui-components/src/lib/SingleSelect/SingleSelect.tsx
+++ b/libs/expo/shared/ui-components/src/lib/SingleSelect/SingleSelect.tsx
@@ -1,20 +1,21 @@
-import { Colors, Spacings, TSpacing } from '@monorepo/expo/shared/static';
+import {
+  Colors,
+  Spacings,
+  TMarginProps,
+  getMarginStyles,
+} from '@monorepo/expo/shared/static';
 import { View } from 'react-native';
+import FormFieldLabel from '../FormFieldLabel';
 import Picker from '../Picker_V2';
 import Radio from '../Radio_V2';
 import TextRegular from '../TextRegular';
 
-interface ISingleSelectProps {
-  mb?: TSpacing;
-  mt?: TSpacing;
-  my?: TSpacing;
-  mx?: TSpacing;
-  ml?: TSpacing;
-  mr?: TSpacing;
+interface ISingleSelectProps extends TMarginProps {
   label?: string;
   placeholder?: string;
   onChange: (value: string | null) => void;
   items: { displayValue?: string; value: string }[];
+  required?: boolean;
   disabled?: boolean;
   selectedValue?: string;
   selectNoneLabel?: string;
@@ -26,21 +27,15 @@ interface ISingleSelectProps {
 export function SingleSelect(props: ISingleSelectProps) {
   const {
     items,
-    mb,
-    mt,
-    mr,
-    ml,
-    my,
-    mx,
     label,
     onChange,
+    required,
     disabled,
     placeholder = '',
     selectedValue,
-    selectNoneLabel,
-    allowSelectNone,
     error,
     maxRadioItems = 3,
+    ...rest
   } = props;
 
   const asSelect = items.length > maxRadioItems;
@@ -52,20 +47,13 @@ export function SingleSelect(props: ISingleSelectProps) {
       <Picker
         error={error}
         label={label}
+        required={required}
         selectedValue={selectedItem?.value}
-        selectedDisplayValue={selectedItem?.displayValue}
         onChange={onChange}
         disabled={disabled}
         placeholder={placeholder}
-        selectNoneLabel={selectNoneLabel}
-        allowSelectNone={allowSelectNone}
         items={items}
-        mb={mb}
-        mt={mt}
-        mr={mr}
-        ml={ml}
-        my={my}
-        mx={mx}
+        {...rest}
       />
     );
   }
@@ -73,15 +61,12 @@ export function SingleSelect(props: ISingleSelectProps) {
   return (
     <View
       style={{
-        marginBottom: mb && Spacings[mb],
-        marginTop: mt && Spacings[mt],
-        marginLeft: ml && Spacings[ml],
-        marginRight: mr && Spacings[mr],
-        marginHorizontal: mx && Spacings[mx],
-        marginVertical: my && Spacings[my],
+        ...getMarginStyles(props),
         gap: Spacings.xs,
       }}
     >
+      {label && <FormFieldLabel label={label} required={required} />}
+
       {items.map(({ displayValue, value }) => (
         <Radio
           key={value || displayValue}


Reviews:
user: login: shiozakiyuko
state: APPROVED
author_association: NONE
submitted_at: 2025-04-28T16:28:41Z
user: login: sourcery-ai[bot]
body: Hey @tglaz - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Margin props (`TMarginProps`) appear to be applied by both container components (e.g., `DatePicker_V2`) and the inner `Input_V2` they render via `{...rest}`, which could lead to doubled margins.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-28T17:48:28Z
user: login: sourcery-ai[bot]
body: Hey @tglaz - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider refactoring `Picker.tsx` (Android/Web) to use `Input_V2` like the iOS version for better platform consistency.
- Using a non-editable `TextInput` for the `asSelect` behavior works, but consider if a non-input element might be semantically more appropriate for triggering overlays.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T00:17:48Z
user: login: Davit-BetterAngels
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T01:12:00Z

Review comments:
diff_hunk: @@ -19,61 +18,58 @@ import {
 } from 'react-native';
 import FormFieldError from '../FormFieldError';
 import FormFieldLabel from '../FormFieldLabel';
+import { InputClearIcon } from './InputClearIcon';
+import { InputSlot, TInputSlot } from './InputSlot';
 
-export interface IInputProps extends TextInputProps {
+export interface IInputProps extends TMarginProps, TextInputProps {
   label?: string;
   required?: boolean;
   disabled?: boolean;
   error?: boolean;
   errorMessage?: string;
-  componentStyle?: StyleProp<ViewStyle>;
-  mb?: TSpacing;
-  mt?: TSpacing;
-  my?: TSpacing;
-  mx?: TSpacing;
-  ml?: TSpacing;
-  mr?: TSpacing;
-  icon?: ReactNode;
-  onDelete?: () => void;
+  style?: StyleProp<ViewStyle>;
+  inputStyle?: StyleProp<ViewStyle>;
   borderRadius?: number;
+  onDelete?: () => void;
+  slotLeft?: TInputSlot;
+  slotRight?: TInputSlot;
+  asSelect?: boolean;
 }
 
+const defaultAsSelectProps: TextInputProps = {
+  showSoftInputOnFocus: false,
+  caretHidden: true,
+};
+
 export function Input(props: IInputProps) {
path: libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the slot-rendering logic into helper functions to clarify intent and reduce complexity.

Consider extracting the slot-rendering logic into helper functions. This will move some of the inline conditional branches out of the render method and clarify intent. For example, you could extract left and right slot-rendering like this:

```tsx
function renderLeftSlot(slotLeft: TInputSlot, disabled: boolean, inputRef: React.RefObject<TextInput>) {
  if (!slotLeft) return null;
  return (
    <InputSlot
      placement="left"
      disabled={disabled}
      inputRef={inputRef}
      {...slotLeft}
    />
  );
}

function renderRightSlot(
  value: string | undefined,
  onDelete: (() => void) | undefined,
  disabled: boolean,
  label: string | undefined,
  slotRight: TInputSlot,
  inputRef: React.RefObject<TextInput>
) {
  if (value && onDelete) {
    return (
      <InputSlot
        placement="right"
        disabled={disabled}
        onPress={onDelete}
        accessibilityLabel="clear input"
        accessibilityHint={`clear value for ${label || 'input'}`}
        component={<InputClearIcon />}
      />
    );
  }
  if (slotRight) {
    return (
      <InputSlot placement="right" disabled={disabled} inputRef={inputRef} {...slotRight} />
    );
  }
  return null;
}
```

Then use these helpers in your component render:

```tsx
export function Input(props: IInputProps) {
  const {
    label,
    error,
    required,
    disabled,
    style,
    inputStyle,
    value,
    onDelete,
    slotLeft,
    slotRight,
    autoCorrect = false,
    autoCapitalize = 'none',
    borderRadius = Radiuses.xs,
    errorMessage,
    asSelect,
    ...rest
  } = props;
  const inputRef = useRef<TextInput>(null);
  const asSelectProps = asSelect ? defaultAsSelectProps : {};

  return (
    <View style={[styles.container, style, { ...getMarginStyles(props) }]}>
      {label && <FormFieldLabel label={label} required={required} />}
      <View style={[styles.input, { borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT, borderRadius }]}>
        {renderLeftSlot(slotLeft, disabled, inputRef)}
        <TextInput
          ref={inputRef}
          style={[{
            color: disabled ? Colors.NEUTRAL_LIGHT : Colors.PRIMARY_EXTRA_DARK,
            paddingHorizontal: Spacings.sm,
            flex: 1,
            fontFamily: 'Poppins-Regular',
            fontSize: FontSizes.md.fontSize,
            paddingVertical: Spacings.sm,
            textAlignVertical: 'top',
            ...Platform.select({ web: { outline: 'none' } }),
          }, inputStyle]}
          editable={!disabled}
          autoCorrect={autoCorrect}
          autoCapitalize={autoCapitalize}
          {...asSelectProps}
          {...rest}
          value={value}
        />
        {renderRightSlot(value, onDelete, disabled, label, slotRight, inputRef)}
      </View>
      {errorMessage && <FormFieldError message={errorMessage} />}
    </View>
  );
}
```

This refactoring isolates the slot logic from the main render block, reducing the cyclomatic complexity while keeping functionality intact.
created_at: 2025-04-28T17:48:28Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Spacings } from '@monorepo/expo/shared/static';
+import { ReactNode } from 'react';
+import {
+  Pressable,
+  StyleSheet,
+  TextInput,
+  View,
+  ViewStyle,
+} from 'react-native';
+
+export type TInputSlot = {
+  component: ReactNode;
+  style?: ViewStyle;
+  disabledStyle?: ViewStyle;
+  onPress?: () => void;
+  focusableInput?: boolean; // if true, will trigger focus on inputRef
+  accessibilityLabel?: string;
+  accessibilityHint?: string;
+};
+
+interface TProps extends TInputSlot {
+  placement: 'left' | 'right';
+  disabled?: boolean;
+  inputRef?: React.RefObject<TextInput>;
+}
+
+export function InputSlot(props: TProps) {
path: libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider reducing conditional rendering by dynamically selecting the container component and assembling its props.

```markdown
Consider reducing the conditional rendering branches by dynamically selecting the container component (Pressable or View) and assembling its props. For example, you can define a variable for the component and its props:

```tsx
export function InputSlot(props: TProps) {
  const {
    style,
    placement,
    disabledStyle,
    onPress,
    focusableInput,
    disabled,
    accessibilityLabel,
    accessibilityHint,
    component,
    inputRef,
  } = props;

  const disabledStyles = disabledStyle || { opacity: disabled ? 0.5 : 1 };
  const placementStyles = placement === 'left' ? styles.slotLeft : styles.slotRight;
  const slotStyle = [styles.container, placementStyles, disabledStyles, style];

  const handlePress = () => {
    if (disabled) return;
    if (focusableInput) inputRef?.current?.focus();
    if (onPress) onPress();
  };

  const Container = onPress || focusableInput ? Pressable : View;
  const containerProps = Container === Pressable
    ? {
        style: slotStyle,
        onPress: handlePress,
        disabled: disabled,
        accessibilityRole: 'button',
        accessibilityLabel,
        accessibilityHint,
      }
    : { style: slotStyle };

  return (
    <Container {...containerProps}>
      <View>{component}</View>
    </Container>
  );
}
```

This refactor consolidates the conditional logic and reduces duplication without changing functionality.
```
created_at: 2025-04-28T17:48:28Z
author_association: NONE
diff_hunk: @@ -168,16 +168,13 @@ export function PersonalInfoForm() {
 
       <Form.Field title="Date of Birth">
         <DatePicker
path: libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx
user: login: sourcery-ai[bot]
body: **question (bug_risk):** Removal of the pattern prop in DatePicker usage.

Please verify that all required input validations are still applied elsewhere now that `pattern` has been removed from `DatePicker`.
created_at: 2025-04-29T00:17:47Z
author_association: NONE
diff_hunk: @@ -19,61 +19,60 @@ import {
 } from 'react-native';
 import FormFieldError from '../FormFieldError';
 import FormFieldLabel from '../FormFieldLabel';
+import { InputClearIcon } from './InputClearIcon';
+import { InputSlot, TInputSlot } from './InputSlot';
 
-export interface IInputProps extends TextInputProps {
+export interface IInputProps extends TMarginProps, TextInputProps {
   label?: string;
   required?: boolean;
   disabled?: boolean;
   error?: boolean;
   errorMessage?: string;
-  componentStyle?: StyleProp<ViewStyle>;
-  mb?: TSpacing;
-  mt?: TSpacing;
-  my?: TSpacing;
-  mx?: TSpacing;
-  ml?: TSpacing;
-  mr?: TSpacing;
-  icon?: ReactNode;
-  onDelete?: () => void;
+  style?: StyleProp<ViewStyle>;
+  inputStyle?: StyleProp<ViewStyle>;
   borderRadius?: number;
+  onDelete?: () => void;
+  slotLeft?: TInputSlot;
+  slotRight?: TInputSlot;
+  asSelect?: boolean;
 }
 
+const defaultAsSelectProps: TextInputProps = {
+  showSoftInputOnFocus: false,
+  caretHidden: true,
+};
+
 export function Input(props: IInputProps) {
   const {
     label,
     error,
     required,
     disabled,
-    componentStyle,
-    mb,
-    mt,
-    my,
-    mx,
-    ml,
-    mr,
-    icon,
+    style,
+    inputStyle,
     value,
     onDelete,
+    slotLeft,
+    slotRight,
     autoCorrect = false,
     autoCapitalize = 'none',
     borderRadius = Radiuses.xs,
     errorMessage,
-    style,
+    asSelect,
     ...rest
path: libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting prop merging and slot rendering logic into helper functions and dedicated subcomponents to improve code clarity.

Consider isolating some of the compound logic to helper functions and dedicated subcomponents. For example:

1. **Extract merging of conditional props:**  
   Create a helper that returns the merged input props (handling `asSelectProps` and margin-related props).

   ```ts
   // helpers.ts
   export function getMergedInputProps(asSelect: boolean, props: TextInputProps) {
     const defaultAsSelectProps: TextInputProps = {
       showSoftInputOnFocus: false,
       caretHidden: true,
     };
     return asSelect ? { ...defaultAsSelectProps, ...props } : props;
   }
   ```

   Then in your component:

   ```tsx
   import { getMergedInputProps } from './helpers';
   // ...

   const mergedProps = getMergedInputProps(asSelect, nonMarginOtherProps);
   // ...
   <TextInput {...mergedProps} ... />
   ```

2. **Extract slot rendering into dedicated functions:**  
   Create small render functions for left and right slots to reduce inline conditionals.

   ```tsx
   function renderSlotLeft(slotLeft: TInputSlot, disabled: boolean, inputRef: React.RefObject<TextInput>) {
     return slotLeft ? (
       <InputSlot
         placement="left"
         disabled={disabled}
         inputRef={inputRef}
         {...slotLeft}
       />
     ) : null;
   }

   function renderSlotRight(
     slotRight: TInputSlot,
     value: string,
     onDelete?: () => void,
     disabled?: boolean,
     inputRef?: React.RefObject<TextInput>,
     label?: string
   ) {
     if (value && onDelete) {
       return (
         <InputSlot
           placement="right"
           disabled={disabled}
           onPress={onDelete}
           accessibilityLabel="clear input"
           accessibilityHint={`clear value for ${label || 'input'}`}
           component={<InputClearIcon />}
         />
       );
     } else if (slotRight) {
       return (
         <InputSlot
           placement="right"
           disabled={disabled}
           inputRef={inputRef}
           {...slotRight}
         />
       );
     }
     return null;
   }
   ```

   And use them in your render:

   ```tsx
   <View style={[styles.input, { borderColor: error ? Colors.ERROR : Colors.NEUTRAL_LIGHT, borderRadius }]}>
     {renderSlotLeft(slotLeft, disabled, inputRef)}
     <TextInput
       ref={inputRef}
       style={[yourStyle, inputStyle]}
       // props...
     />
     {renderSlotRight(slotRight, value, onDelete, disabled, inputRef, label)}
   </View>
   ```

These steps keep all functionality intact while making the code easier to follow and maintain.
created_at: 2025-04-29T00:17:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Spacings } from '@monorepo/expo/shared/static';
+import { ReactNode } from 'react';
+import {
+  Pressable,
+  StyleSheet,
+  TextInput,
+  View,
+  ViewStyle,
+} from 'react-native';
+
+export type TInputSlot = {
+  component: ReactNode;
+  style?: ViewStyle;
+  disabledStyle?: ViewStyle;
+  onPress?: () => void;
+  focusableInput?: boolean; // if true, will trigger focus on inputRef
+  accessibilityLabel?: string;
+  accessibilityHint?: string;
+};
+
+interface TProps extends TInputSlot {
+  placement: 'left' | 'right';
+  disabled?: boolean;
+  inputRef?: React.RefObject<TextInput>;
+}
+
+export function InputSlot(props: TProps) {
path: libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider using a single Pressable component with conditional props instead of switching between Pressable and View based on interactivity.

Consider unifying the two render paths to avoid the explicit conditional that switches between a Pressable and a plain View. For example, you can always wrap the slot in a Pressable but only attach the interaction props if needed. This reduces duplication and complexity while keeping the functionality.

```tsx
export function InputSlot(props: TProps) {
  const {
    style,
    placement,
    disabledStyle,
    onPress,
    focusableInput,
    disabled,
    accessibilityLabel,
    accessibilityHint,
    component,
    inputRef,
  } = props;

  const disabledStyles = disabledStyle || { opacity: disabled ? 0.5 : 1 };
  const placementStyles = placement === 'left' ? styles.slotLeft : styles.slotRight;
  const slotStyle = [styles.container, placementStyles, disabledStyles, style];

  const isInteractive = onPress || focusableInput;

  const handlePress = () => {
    if (disabled) return;
    if (focusableInput) inputRef?.current?.focus();
    if (onPress) onPress();
  };

  return (
    <Pressable
      style={slotStyle}
      onPress={isInteractive ? handlePress : undefined}
      disabled={disabled}
      accessibilityRole={isInteractive ? 'button' : undefined}
      accessibilityLabel={accessibilityLabel}
      accessibilityHint={accessibilityHint}
    >
      <View>{component}</View>
    </Pressable>
  );
}
```

**Actionable steps:**

1. Define an `isInteractive` flag based on `onPress` or `focusableInput`.
2. Always use a `Pressable` as the container.
3. Conditionally supply `onPress` and `accessibilityRole` only when interactive.

This refactoring removes the branching between two different components, making the code easier to follow while preserving behavior.
created_at: 2025-04-29T00:17:48Z
author_association: NONE

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-04-27T22:21:36Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request refactors the `DatePicker_V2` and `Picker_V2` (iOS) components to utilize the enhanced `Input_V2` component. This change standardizes the visual appearance and interaction pattern for these components, particularly how they handle user input and display values. The `Input_V2` component has been significantly updated to support a 'select' mode and configurable content slots (left and right) to accommodate icons and other elements.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Refactored DatePicker_V2 and Picker_V2 (iOS) to use Input_V2. | <ul><li>Replaced native TextInput/Pressable implementation with Input_V2.</li><li>Used the `asSelect` prop on Input_V2 to prevent keyboard appearance and hide caret.</li><li>Implemented icon rendering using the new `slotRight` prop of Input_V2.</li><li>Removed old styling and logic related to the previous input implementation.</li><li>Migrated margin prop handling to use `getMarginStyles` helper.</li></ul> | `libs/expo/shared/ui-components/src/lib/DatePicker_V2/DatePicker.tsx`<br/>`libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.ios.tsx` |
| Enhanced Input_V2 component with asSelect mode and slot props. | <ul><li>Added `asSelect` prop to configure input behavior for select-like components.</li><li>Added `slotLeft` and `slotRight` props to render custom components within the input field.</li><li>Integrated the new `InputSlot` component to manage slot rendering and interaction.</li><li>Refactored internal styling and structure to support the new slot system.</li><li>Updated margin prop handling to use `getMarginStyles` and `omitMarginProps`.</li></ul> | `libs/expo/shared/ui-components/src/lib/Input_V2/Input.tsx` |
| Introduced new helper components for Input_V2 slots. | <ul><li>Created `InputSlot` component to handle positioning and interaction (including focusing the input) for slot content.</li><li>Created `InputClearIcon` component for the clear button functionality in Input_V2.</li></ul> | `libs/expo/shared/ui-components/src/lib/Input_V2/InputSlot.tsx`<br/>`libs/expo/shared/ui-components/src/lib/Input_V2/InputClearIcon.tsx` |
| Standardized margin prop handling across components. | <ul><li>Added `omitMarginProps` helper function to separate margin props from others.</li><li>Exported new margin utilities.</li><li>Updated `SingleSelect` and `Picker_V2` (non-iOS) to consistently use `TMarginProps` and `getMarginStyles`.</li></ul> | `libs/expo/shared/static/src/lib/margins.ts`<br/>`libs/expo/shared/static/src/lib/index.ts`<br/>`libs/expo/shared/ui-components/src/lib/SingleSelect/SingleSelect.tsx`<br/>`libs/expo/shared/ui-components/src/lib/Picker_V2/Picker.tsx` |
| Updated usage of DatePicker and SingleSelect in forms. | <ul><li>Removed deprecated props like `pattern` from DatePicker usage.</li><li>Adjusted prop passing for `onChange` and `value` in form controllers.</li><li>Ensured required prop is correctly passed to SingleSelect.</li></ul> | `libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HouseholdMemberForm/HouseholdMemberForm.tsx`<br/>`libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/ClientProfileForm/PersonalInfoForm/PersonalInfoForm.tsx`<br/>`libs/expo/betterangels/src/lib/screens/ClientProfileForms-V2/relatedClientProfileModel/forms/HmisProfileForm/HmisProfileForm.tsx` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-27T22:26:45Z
body: <!-- continuous-deploy-fingerprint-projectId:53171ba4-60ca-40cb-b3e6-b0c2393677b8 -->
🚀 Expo continuous deployment is ready for **betterangels**!

- Project → **betterangels**
- Environment → **Preview**
- Platforms → **android**, **ios**
- Scheme → **betterangels**

&nbsp; | 🤖 Android | 🍎 iOS
--- | --- | ---
Runtime Version | `43eace2e04eb5d2ac31a0ae898fd1e132028a476` | `43eace2e04eb5d2ac31a0ae898fd1e132028a476`
Build Details | [Build Permalink](https://expo.dev/accounts/better-angels/projects/betterangels/builds/60436476-4c56-4158-ab27-de7317fc07a3)<br /><details><summary>Details</summary>Distribution: `INTERNAL`<br />Build profile: `preview`<br />Runtime version: `43eace2e04eb5d2ac31a0ae898fd1e132028a476`<br />App version: `1.0.41`<br />Git commit: `8bf3e56fcffc6c0d98dbd501068de9921c95d4b8`</details> | [Build Permalink](https://expo.dev/accounts/better-angels/projects/betterangels/builds/9aec12bb-fad2-47cc-8899-26415519ad86)<br /><details><summary>Details</summary>Distribution: `INTERNAL`<br />Build profile: `preview`<br />Runtime version: `43eace2e04eb5d2ac31a0ae898fd1e132028a476`<br />App version: `1.0.41`<br />Git commit: `8bf3e56fcffc6c0d98dbd501068de9921c95d4b8`</details>
Update Details | [Update Permalink](https://expo.dev/projects/53171ba4-60ca-40cb-b3e6-b0c2393677b8/updates/a4ee16dd-eef8-46ae-9894-2f94a907871c)<br /><details><summary>Details</summary>Branch: `DEV-1774/picker-field-focus`<br />Runtime version: `43eace2e04eb5d2ac31a0ae898fd1e132028a476`<br />Git commit: `c9ad4b0354ba3cf83f133bd893c513813fa073e8`</details> | [Update Permalink](https://expo.dev/projects/53171ba4-60ca-40cb-b3e6-b0c2393677b8/updates/22cc11ea-f8a4-473a-9b7e-e2d8638a69b9)<br /><details><summary>Details</summary>Branch: `DEV-1774/picker-field-focus`<br />Runtime version: `43eace2e04eb5d2ac31a0ae898fd1e132028a476`<br />Git commit: `c9ad4b0354ba3cf83f133bd893c513813fa073e8`</details>
Update QR   | <a href="https://qr.expo.dev/eas-update?appScheme=betterangels&projectId=53171ba4-60ca-40cb-b3e6-b0c2393677b8&groupId=ea9bafcc-d492-4cf1-933a-ff043eb6dcce"><img src="https://qr.expo.dev/eas-update?appScheme=betterangels&projectId=53171ba4-60ca-40cb-b3e6-b0c2393677b8&groupId=ea9bafcc-d492-4cf1-933a-ff043eb6dcce" width="250px" height="250px" /></a> | <a href="https://qr.expo.dev/eas-update?appScheme=betterangels&projectId=53171ba4-60ca-40cb-b3e6-b0c2393677b8&groupId=ea9bafcc-d492-4cf1-933a-ff043eb6dcce"><img src="https://qr.expo.dev/eas-update?appScheme=betterangels&projectId=53171ba4-60ca-40cb-b3e6-b0c2393677b8&groupId=ea9bafcc-d492-4cf1-933a-ff043eb6dcce" width="250px" height="250px" /></a>

**iOS Simulator Build:** [Simulator Build Link](https://expo.dev/accounts/better-angels/projects/betterangels/builds/cfeca9f1-c62b-43d8-ac0a-061a4f783913)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-28T22:04:26Z
body: 🔍 [shelter-web] Preview available at: [https://shelter.dev.betterangels.la/branches/DEV-1774-picker-field-focus](https://shelter.dev.betterangels.la/branches/DEV-1774-picker-field-focus)

_Last updated: 2025-04-29T00:20:38.884Z_
author_association: NONE