import { ComponentType, forwardRef, Ref, useImperativeHandle, useRef } from "react" import { ImageStyle, StyleProp, // eslint-disable-next-line no-restricted-imports TextInput, TextInputProps, TextStyle, TouchableOpacity, View, ViewStyle, } from "react-native" import { useTranslation } from "react-i18next" import { isRTL } from "@/i18n" import { useAppTheme } from "@/theme/context" import { $styles } from "@/theme/styles" import type { ThemedStyle, ThemedStyleArray } from "@/theme/types" import { s, fs } from "@/utils/responsive" import { Text, TextProps } from "./Text" export interface TextFieldAccessoryProps { style: StyleProp status: TextFieldProps["status"] multiline: boolean editable: boolean } export interface TextFieldProps extends Omit { /** * A style modifier for different input states. */ status?: "error" | "disabled" /** * The label text to display if not using `labelTx`. */ label?: TextProps["text"] /** * Label text which is looked up via i18n. */ labelTx?: TextProps["tx"] /** * Optional label options to pass to i18n. Useful for interpolation * as well as explicitly setting locale or translation fallbacks. */ labelTxOptions?: TextProps["txOptions"] /** * Pass any additional props directly to the label Text component. */ LabelTextProps?: TextProps /** * The helper text to display if not using `helperTx`. */ helper?: TextProps["text"] /** * Helper text which is looked up via i18n. */ helperTx?: TextProps["tx"] /** * Optional helper options to pass to i18n. Useful for interpolation * as well as explicitly setting locale or translation fallbacks. */ helperTxOptions?: TextProps["txOptions"] /** * Pass any additional props directly to the helper Text component. */ HelperTextProps?: TextProps /** * The placeholder text to display if not using `placeholderTx`. */ placeholder?: TextProps["text"] /** * Placeholder text which is looked up via i18n. */ placeholderTx?: TextProps["tx"] /** * Optional placeholder options to pass to i18n. Useful for interpolation * as well as explicitly setting locale or translation fallbacks. */ placeholderTxOptions?: TextProps["txOptions"] /** * Optional input style override. */ style?: StyleProp /** * Style overrides for the container */ containerStyle?: StyleProp /** * Style overrides for the input wrapper */ inputWrapperStyle?: StyleProp /** * An optional component to render on the right side of the input. * Example: `RightAccessory={(props) => }` * Note: It is a good idea to memoize this. */ RightAccessory?: ComponentType /** * An optional component to render on the left side of the input. * Example: `LeftAccessory={(props) => }` * Note: It is a good idea to memoize this. */ LeftAccessory?: ComponentType } /** * A component that allows for the entering and editing of text. * @see [Documentation and Examples]{@link https://docs.infinite.red/ignite-cli/boilerplate/app/components/TextField/} * @param {TextFieldProps} props - The props for the `TextField` component. * @returns {JSX.Element} The rendered `TextField` component. */ export const TextField = forwardRef(function TextField(props: TextFieldProps, ref: Ref) { const { labelTx, label, labelTxOptions, placeholderTx, placeholder, placeholderTxOptions, helper, helperTx, helperTxOptions, status, RightAccessory, LeftAccessory, HelperTextProps, LabelTextProps, style: $inputStyleOverride, containerStyle: $containerStyleOverride, inputWrapperStyle: $inputWrapperStyleOverride, ...TextInputProps } = props const input = useRef(null) const { t } = useTranslation() const { themed, theme: { colors }, } = useAppTheme() const disabled = TextInputProps.editable === false || status === "disabled" const placeholderContent = placeholderTx ? t(placeholderTx, placeholderTxOptions) : placeholder const $containerStyles = [$containerStyleOverride] const $labelStyles = [$labelStyle, LabelTextProps?.style] const $inputWrapperStyles = [ $styles.row, $inputWrapperStyle, status === "error" && { borderColor: colors.error }, TextInputProps.multiline && { minHeight: s(112) }, LeftAccessory && { paddingStart: 0 }, RightAccessory && { paddingEnd: 0 }, $inputWrapperStyleOverride, ] const $inputStyles: ThemedStyleArray = [ $inputStyle, disabled && { color: colors.textDim }, isRTL && { textAlign: "right" as TextStyle["textAlign"] }, TextInputProps.multiline && { height: "auto" }, $inputStyleOverride, ] const $helperStyles = [ $helperStyle, status === "error" && { color: colors.error }, HelperTextProps?.style, ] /** * */ function focusInput() { if (disabled) return input.current?.focus() } useImperativeHandle(ref, () => input.current as TextInput) return ( {!!(label || labelTx) && ( )} {!!LeftAccessory && ( )} {!!RightAccessory && ( )} {!!(helper || helperTx) && ( )} ) }) const $labelStyle: ThemedStyle = ({ spacing }) => ({ marginBottom: spacing.xs, }) const $inputWrapperStyle: ThemedStyle = ({ colors }) => ({ alignItems: "flex-start", borderWidth: 1, borderRadius: s(4), backgroundColor: colors.palette.neutral200, borderColor: colors.palette.neutral400, overflow: "hidden", }) const $inputStyle: ThemedStyle = ({ colors, typography, spacing }) => ({ flex: 1, alignSelf: "stretch", fontFamily: typography.primary.normal, color: colors.text, fontSize: fs(16), height: s(24), // https://github.com/facebook/react-native/issues/21720#issuecomment-532642093 paddingVertical: 0, paddingHorizontal: 0, marginVertical: spacing.xs, marginHorizontal: spacing.sm, }) const $helperStyle: ThemedStyle = ({ spacing }) => ({ marginTop: spacing.xs, }) const $rightAccessoryStyle: ThemedStyle = ({ spacing }) => ({ marginEnd: spacing.xs, height: s(40), justifyContent: "center", alignItems: "center", }) const $leftAccessoryStyle: ThemedStyle = ({ spacing }) => ({ marginStart: spacing.xs, height: s(40), justifyContent: "center", alignItems: "center", })