293 lines
8.0 KiB
TypeScript
293 lines
8.0 KiB
TypeScript
|
|
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<ViewStyle | TextStyle | ImageStyle>
|
||
|
|
status: TextFieldProps["status"]
|
||
|
|
multiline: boolean
|
||
|
|
editable: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface TextFieldProps extends Omit<TextInputProps, "ref"> {
|
||
|
|
/**
|
||
|
|
* 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<TextStyle>
|
||
|
|
/**
|
||
|
|
* Style overrides for the container
|
||
|
|
*/
|
||
|
|
containerStyle?: StyleProp<ViewStyle>
|
||
|
|
/**
|
||
|
|
* Style overrides for the input wrapper
|
||
|
|
*/
|
||
|
|
inputWrapperStyle?: StyleProp<ViewStyle>
|
||
|
|
/**
|
||
|
|
* An optional component to render on the right side of the input.
|
||
|
|
* Example: `RightAccessory={(props) => <Icon icon="ladybug" containerStyle={props.style} color={props.editable ? colors.textDim : colors.text} />}`
|
||
|
|
* Note: It is a good idea to memoize this.
|
||
|
|
*/
|
||
|
|
RightAccessory?: ComponentType<TextFieldAccessoryProps>
|
||
|
|
/**
|
||
|
|
* An optional component to render on the left side of the input.
|
||
|
|
* Example: `LeftAccessory={(props) => <Icon icon="ladybug" containerStyle={props.style} color={props.editable ? colors.textDim : colors.text} />}`
|
||
|
|
* Note: It is a good idea to memoize this.
|
||
|
|
*/
|
||
|
|
LeftAccessory?: ComponentType<TextFieldAccessoryProps>
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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<TextInput>) {
|
||
|
|
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<TextInput>(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<TextStyle> = [
|
||
|
|
$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 (
|
||
|
|
<TouchableOpacity
|
||
|
|
activeOpacity={1}
|
||
|
|
style={$containerStyles}
|
||
|
|
onPress={focusInput}
|
||
|
|
accessibilityState={{ disabled }}
|
||
|
|
>
|
||
|
|
{!!(label || labelTx) && (
|
||
|
|
<Text
|
||
|
|
preset="formLabel"
|
||
|
|
text={label}
|
||
|
|
tx={labelTx}
|
||
|
|
txOptions={labelTxOptions}
|
||
|
|
{...LabelTextProps}
|
||
|
|
style={themed($labelStyles)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<View style={themed($inputWrapperStyles)}>
|
||
|
|
{!!LeftAccessory && (
|
||
|
|
<LeftAccessory
|
||
|
|
style={themed($leftAccessoryStyle)}
|
||
|
|
status={status}
|
||
|
|
editable={!disabled}
|
||
|
|
multiline={TextInputProps.multiline ?? false}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<TextInput
|
||
|
|
ref={input}
|
||
|
|
underlineColorAndroid={colors.transparent}
|
||
|
|
textAlignVertical="top"
|
||
|
|
placeholder={placeholderContent}
|
||
|
|
placeholderTextColor={colors.textDim}
|
||
|
|
{...TextInputProps}
|
||
|
|
editable={!disabled}
|
||
|
|
style={themed($inputStyles)}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{!!RightAccessory && (
|
||
|
|
<RightAccessory
|
||
|
|
style={themed($rightAccessoryStyle)}
|
||
|
|
status={status}
|
||
|
|
editable={!disabled}
|
||
|
|
multiline={TextInputProps.multiline ?? false}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{!!(helper || helperTx) && (
|
||
|
|
<Text
|
||
|
|
preset="formHelper"
|
||
|
|
text={helper}
|
||
|
|
tx={helperTx}
|
||
|
|
txOptions={helperTxOptions}
|
||
|
|
{...HelperTextProps}
|
||
|
|
style={themed($helperStyles)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</TouchableOpacity>
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
const $labelStyle: ThemedStyle<TextStyle> = ({ spacing }) => ({
|
||
|
|
marginBottom: spacing.xs,
|
||
|
|
})
|
||
|
|
|
||
|
|
const $inputWrapperStyle: ThemedStyle<ViewStyle> = ({ colors }) => ({
|
||
|
|
alignItems: "flex-start",
|
||
|
|
borderWidth: 1,
|
||
|
|
borderRadius: s(4),
|
||
|
|
backgroundColor: colors.palette.neutral200,
|
||
|
|
borderColor: colors.palette.neutral400,
|
||
|
|
overflow: "hidden",
|
||
|
|
})
|
||
|
|
|
||
|
|
const $inputStyle: ThemedStyle<TextStyle> = ({ 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<TextStyle> = ({ spacing }) => ({
|
||
|
|
marginTop: spacing.xs,
|
||
|
|
})
|
||
|
|
|
||
|
|
const $rightAccessoryStyle: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||
|
|
marginEnd: spacing.xs,
|
||
|
|
height: s(40),
|
||
|
|
justifyContent: "center",
|
||
|
|
alignItems: "center",
|
||
|
|
})
|
||
|
|
|
||
|
|
const $leftAccessoryStyle: ThemedStyle<ViewStyle> = ({ spacing }) => ({
|
||
|
|
marginStart: spacing.xs,
|
||
|
|
height: s(40),
|
||
|
|
justifyContent: "center",
|
||
|
|
alignItems: "center",
|
||
|
|
})
|