UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

209 lines (208 loc) • 10.4 kB
import _omit from "lodash/omit"; import _trim from "lodash/trim"; import _isEmpty from "lodash/isEmpty"; /** * Known issues with React Native TextInput component * 1. iOS - input inner padding is off in multiline mode * 2. Android - input has minHeight that can't be overridden with zero padding (unlike iOS) * 3. Passing typography preset that includes lineHeight usually cause alignment issues with * other elements (leading/trailing accessories). It usually best to set lineHeight with undefined */ import React, { useMemo } from 'react'; import { StyleSheet } from 'react-native'; import { asBaseComponent, Constants, forwardRef } from "../../commons/new"; import View from "../view"; import Text from "../text"; import { useMeasure } from "../../hooks"; import { TextFieldProps, ValidationMessagePosition, FieldContextType, TextFieldMethods, TextFieldRef, Validator, ValidationMessagePositionType, MandatoryIndication, Presets } from "./types"; import TextFieldValidators from "./validators"; import { shouldHidePlaceholder } from "./Presenter"; import Input from "./Input"; import ValidationMessage from "./ValidationMessage"; import Label from "./Label"; import FieldContext from "./FieldContext"; import useFieldState from "./useFieldState"; import usePreset from "./usePreset"; import FloatingPlaceholder from "./FloatingPlaceholder"; import CharCounter from "./CharCounter"; import ClearButton from "./ClearButton"; /** * @description: A controlled, customizable TextField with validation support * @extends: TextInput * @extendsLink: https://reactnative.dev/docs/textinput * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TextFieldScreen.tsx * @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/FloatingPlaceholder.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/Validation.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/ColorByState.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/CharCounter.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/Hint.gif?raw=true */ const TextField = props => { const { modifiers, // General containerProps, fieldStyle: fieldStyleProp, dynamicFieldStyle, containerStyle, floatingPlaceholder, floatingPlaceholderColor, floatingPlaceholderStyle, floatOnFocus, placeholderTextColor, hint, helperText, validationIcon, innerFlexBehavior, // Label label, labelColor, labelStyle, labelProps, // Accessory Buttons leadingAccessory: propsLeadingAccessory, trailingAccessory, topTrailingAccessory, bottomAccessory, showClearButton, onClear, // Validation enableErrors, // TODO: rename to enableValidation validationMessageStyle, validationMessagePosition = ValidationMessagePosition.BOTTOM, retainValidationSpace = !helperText && !bottomAccessory, // Char Counter showCharCounter, charCounterStyle, // Input placeholder, children, centered, readonly = false, showMandatoryIndication, clearButtonStyle, ...others } = usePreset(props); const { ref: leadingAccessoryRef, measurements: leadingAccessoryMeasurements } = useMeasure(); const { onFocus, onBlur, onChangeText, fieldState, validateField, checkValidity } = useFieldState(others); const context = useMemo(() => { return { ...fieldState, disabled: others.editable === false, readonly, validateField, checkValidity }; }, [fieldState, others.editable, readonly, validateField, checkValidity]); const leadingAccessoryClone = useMemo(() => { if (propsLeadingAccessory) { return React.cloneElement(propsLeadingAccessory, { ref: leadingAccessoryRef }); } }, [propsLeadingAccessory]); const leadingAccessory = useMemo(() => { return floatingPlaceholder ? leadingAccessoryClone : propsLeadingAccessory; }, [floatingPlaceholder, leadingAccessoryClone, propsLeadingAccessory]); const { margins, paddings, typography, positionStyle, color } = modifiers; const typographyStyle = useMemo(() => _omit(typography, 'lineHeight'), [typography]); const colorStyle = useMemo(() => color && { color }, [color]); const _floatingPlaceholderStyle = useMemo(() => [typographyStyle, floatingPlaceholderStyle], [typographyStyle, floatingPlaceholderStyle]); const fieldStyle = [fieldStyleProp, dynamicFieldStyle?.(context, { preset: props.preset })]; const hidePlaceholder = shouldHidePlaceholder(props, fieldState.isFocused); const retainTopMessageSpace = !floatingPlaceholder && _isEmpty(_trim(label)); const centeredContainerStyle = centered && styles.centeredContainer; const centeredTextStyle = centered && !showCharCounter && styles.centeredText; const _labelStyle = useMemo(() => [labelStyle, centeredTextStyle], [labelStyle, centeredTextStyle]); const _validationMessageStyle = useMemo(() => [validationMessageStyle, centeredTextStyle], [validationMessageStyle, centeredTextStyle]); const hasValue = fieldState.value !== undefined; // NOTE: not pressable if centered without a value (so can't center placeholder) const inputStyle = useMemo(() => [typographyStyle, colorStyle, others.style, hasValue && centeredTextStyle], [typographyStyle, colorStyle, others.style, centeredTextStyle, hasValue]); const dummyPlaceholderStyle = useMemo(() => [inputStyle, styles.dummyPlaceholder], [inputStyle]); return <FieldContext.Provider value={context}> <View {...containerProps} style={[margins, positionStyle, containerStyle, centeredContainerStyle]}> <View row spread style={centeredContainerStyle}> <Label label={label} labelColor={labelColor} labelStyle={_labelStyle} labelProps={labelProps} floatingPlaceholder={floatingPlaceholder} validationMessagePosition={validationMessagePosition} testID={`${props.testID}.label`} showMandatoryIndication={showMandatoryIndication} enableErrors={enableErrors} /> {validationMessagePosition === ValidationMessagePosition.TOP && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace && retainTopMessageSpace} testID={`${props.testID}.validationMessage`} />} {topTrailingAccessory && <View>{topTrailingAccessory}</View>} </View> <View style={[paddings, fieldStyle]} row centerV centerH={centered}> {/* <View row centerV> */} {leadingAccessory} {/* Note: We're passing flexG to the View to support properly inline behavior - so the input will be rendered correctly in a row container. Known Issue: This slightly push the trailing accessory and clear button when entering a long text */} {children || <View {...Constants.isWeb || innerFlexBehavior ? { flex: true } : { flexG: true }}> {/* Note: Render dummy placeholder for Android center issues */} {Constants.isAndroid && centered && <Text marginR-s1 style={dummyPlaceholderStyle}> {placeholder} </Text>} {floatingPlaceholder && <FloatingPlaceholder defaultValue={others.defaultValue} placeholder={placeholder} floatingPlaceholderStyle={_floatingPlaceholderStyle} floatingPlaceholderColor={floatingPlaceholderColor} floatOnFocus={floatOnFocus} validationMessagePosition={validationMessagePosition} extraOffset={leadingAccessoryMeasurements?.width} testID={`${props.testID}.floatingPlaceholder`} showMandatoryIndication={showMandatoryIndication} />} <Input hitSlop={{ top: 20, bottom: 20 }} placeholderTextColor={hidePlaceholder ? 'transparent' : placeholderTextColor} value={fieldState.value} {...others} readonly={readonly} style={inputStyle} onFocus={onFocus} onBlur={onBlur} onChangeText={onChangeText} placeholder={placeholder} hint={hint} showMandatoryIndication={showMandatoryIndication && !label} /> </View>} {showClearButton && <ClearButton onClear={onClear} testID={`${props.testID}.clearButton`} onChangeText={onChangeText} clearButtonStyle={clearButtonStyle} />} {trailingAccessory} {/* </View> */} </View> <View row spread center={centered}> <View flex={!centered} flexG={centered} marginR-s4={showCharCounter}> {validationMessagePosition === ValidationMessagePosition.BOTTOM && <ValidationMessage enableErrors={enableErrors} validate={others.validate} validationMessage={others.validationMessage} validationIcon={validationIcon} validationMessageStyle={_validationMessageStyle} retainValidationSpace={retainValidationSpace} testID={`${props.testID}.validationMessage`} />} {helperText && <Text $textNeutralHeavy subtext marginT-s1 testID={`${props.testID}.helperText`}> {helperText} </Text>} {bottomAccessory} </View> <View> {showCharCounter && <CharCounter maxLength={others.maxLength} charCounterStyle={charCounterStyle} testID={`${props.testID}.charCounter`} />} </View> </View> </View> </FieldContext.Provider>; }; TextField.displayName = 'TextField'; TextField.presets = Presets; TextField.validationMessagePositions = ValidationMessagePosition; export { TextFieldProps, FieldContextType, TextFieldMethods, TextFieldRef, ValidationMessagePosition as TextFieldValidationMessagePosition, Validator as TextFieldValidator, ValidationMessagePositionType as TextFieldValidationMessagePositionType, MandatoryIndication as TextFieldMandatoryIndication, TextFieldValidators }; export default asBaseComponent(forwardRef(TextField), { modifiersOptions: { margins: true, paddings: true, typography: true, position: true, color: true } }); const styles = StyleSheet.create({ centeredContainer: { alignSelf: 'center' }, centeredText: { textAlign: 'center' }, dummyPlaceholder: { height: 0 } });