import * as React from 'react' ; import { Animated , TextInputProps as RNTextInputProps } from 'react-native' ; import { createComponent , createElement , createHook , hexToRgb , mergeRefs , omitCSSProps , pickCSSProps , useTheme , } from 'bumbag/utils' ; import { Palette , Size } from 'bumbag/types' ; import { Box , BoxProps } from '../Box' ; import { Icon , IconProps } from '../Icon' ; import { FieldWrapper , FieldWrapperProps } from '../FieldWrapper' ; import { Pressable } from '../Pressable' ; import { palette , useSpace , usePalette } from '../utils' ; import * as styles from './Input.styles' ; export type LocalInputProps = { accessibilityLabelledBy ? : string ; /** Icon to place before the input. */ iconAfter ? : string ; iconAfterProps ? : Partial < IconProps > ; /** Icon to place after the input. */ iconBefore ? : string ; iconBeforeProps ? : Partial < IconProps > ; inputProps ? : Partial < InputProps > ; inputRef ? : React . Ref < any > ; /** Label of the input. */ label ? : string ; /** Label color of the input. Can be any color in your palette. */ labelTextColor ? : Palette ; labelProps ? : any ; palette ? : Palette ; /** Placeholder color of the input. Can be any color in your palette. */ placeholderTextColor ? : Palette ; /** State of the input. Can be any color in your palette. */ state ? : Palette ; /** Alters the size of the input. Can be "small", "medium" or "large" */ size ? : Size ; } ; export type InputProps = BoxProps & RNTextInputProps & LocalInputProps ; const defaultProps = { placeholderTextColor : 'gray300' , size : 'default' , variant : 'bordered' } ; const useProps = createHook < InputProps > ( ( props ) => { const ref = React . useRef ( ) ; const boxProps = Box . useProps ( { ... props , elementRef : mergeRefs ( ref , props . elementRef ) , _focus : true , _hover : true , _active : true , _hoveractive : true , } ) ; const { theme } = useTheme ( ) ; return { ... boxProps , accessibilityLabelledBy : props . accessibilityLabelledBy , placeholderTextColor : props . placeholderTextColor ? palette ( props . placeholderTextColor ) ( { theme } ) : undefined , } ; } , { defaultProps , themeKey : 'native.Input' } ) ; export const Input = createComponent < InputProps > ( ( _props ) => { const props = { ... _props , ... _props . inputProps } ; const { colorMode , disabled , iconAfter , iconAfterProps , iconBefore , iconBeforeProps , label , labelProps , size , } = props ; const defaultFontSize = props . fontSize || styles . SIZES [ size ] ; const [ value , setValue ] = React . useState ( props . defaultValue ) ; const inputRef = React . useRef ( { value : null , _lastNativeText : null } ) ; const [ isFocused , setIsFocused ] = React . useState ( false ) ; const handleBlur = React . useCallback ( ( e ) => { setIsFocused ( false ) ; props . onBlur && props . onBlur ( e ) ; } , [ props ] ) ; const handleChange = React . useCallback ( ( e ) => { setValue ( e ) ; props . onChangeText && props . onChangeText ( e ) ; } , [ props ] ) ; const handleFocus = React . useCallback ( ( e ) => { setIsFocused ( true ) ; props . onFocus && props . onFocus ( e ) ; } , [ props ] ) ; const handlePress = React . useCallback ( ( ) => { setIsFocused ( true ) ; inputRef . current && inputRef . current . focus ( ) ; } , [ ] ) ; const htmlProps = useProps ( { ... omitCSSProps ( { ... props , ref : mergeRefs ( inputRef , props . inputRef ) , onBlur : handleBlur , onFocus : handleFocus , onChangeText : handleChange , placeholder : ! label || props . disabled ? props . placeholder || props . label : '' , } ) , borderLeftRadius : props . borderLeftRadius , borderRightRadius : props . borderRightRadius , } ) ; const element = createElement ( { children : props . children , component : styles . StyledInput , htmlProps : { ... htmlProps , iconAfter , iconBefore , styledFontSize : defaultFontSize } , } ) ; const labelStartColor = usePalette ( props . state || props . placeholderTextColor || props . palette || 'gray300' ) ; const labelEndColor = usePalette ( props . state || props . labelTextColor || props . palette || 'gray300' ) ; const topStart = useSpace ( props . defaultValue ? - 0.5 : 0.8 , defaultFontSize ) ; const topEnd = useSpace ( - 0.5 , defaultFontSize ) ; const fontSizeStart = useSpace ( props . defaultValue ? 0.75 : 1 , defaultFontSize ) ; const fontSizeEnd = useSpace ( 0.75 , defaultFontSize ) ; const colorAnimation = React . useRef ( new Animated . Value ( 0 ) ) . current ; const topAnimation = React . useRef ( new Animated . Value ( topStart ) ) . current ; const fontSizeAnimation = React . useRef ( new Animated . Value ( 0 ) ) . current ; React . useEffect ( ( ) => { if ( isFocused || value ) { Animated . timing ( colorAnimation , { toValue : 1 , duration : 100 , useNativeDriver : false , } ) . start ( ) ; Animated . timing ( topAnimation , { toValue : topEnd , duration : 100 , useNativeDriver : false , } ) . start ( ) ; Animated . timing ( fontSizeAnimation , { toValue : 1 , duration : 100 , useNativeDriver : false , } ) . start ( ) ; } else { Animated . timing ( colorAnimation , { toValue : 0 , duration : 100 , useNativeDriver : false , } ) . start ( ) ; Animated . timing ( topAnimation , { toValue : topStart , duration : 100 , useNativeDriver : false , } ) . start ( ) ; Animated . timing ( fontSizeAnimation , { toValue : 0 , duration : 100 , useNativeDriver : false , } ) . start ( ) ; } } , [ colorAnimation , fontSizeAnimation , fontSizeEnd , fontSizeStart , isFocused , topAnimation , topEnd , topStart , value , ] ) ; const color = colorAnimation . interpolate ( { inputRange : [ 0 , 1 ] , outputRange : [ hexToRgb ( labelStartColor ) , hexToRgb ( labelEndColor ) ] , } ) ; const fontSize = fontSizeAnimation . interpolate ( { inputRange : [ 0 , 1 ] , outputRange : [ fontSizeStart , fontSizeEnd ] , } ) ; return ( < Box colorMode = { colorMode } position = "relative" { ... pickCSSProps ( props ) } > { element } { iconBefore && ( < styles . StyledIconWrapper colorMode = { colorMode } isBefore defaultFontSize = { defaultFontSize } variant = { props . variant } > < Icon color = "gray200" icon = { iconBefore } size = { defaultFontSize } { ... iconBeforeProps } /> < /styles.StyledIconWrapper>)} { iconAfter && ( < styles . StyledIconWrapper colorMode = { colorMode } isAfter defaultFontSize = { defaultFontSize } variant = { props . variant } > < Icon color = "gray200" icon = { iconAfter } size = { defaultFontSize } { ... iconAfterProps } /> < /styles.StyledIconWrapper>)} { label && ! props . disabled && ( < styles . StyledAnimatedLabelWrapper colorMode = { colorMode } iconAfter = { iconAfter } iconBefore = { iconBefore } defaultFontSize = { defaultFontSize } variant = { props . variant } style = { { position : 'absolute' , top : topAnimation } } > < Pressable onPress = { handlePress } > < styles . StyledAnimatedLabel colorMode = { colorMode } color = { color } fontSize = { fontSize } variant = { props . variant } _web = { { cursor : 'text' } } { ... labelProps } > { label } < /styles.StyledAnimatedLabel> < /Pressable> < /styles.StyledAnimatedLabelWrapper>)} < /Box>); } , { attach : { useProps , displayName : 'native.Input' , } , defaultProps , themeKey : 'native.Input' , } ) ; export type LocalInputFieldProps = { /** Additional props for the Input component */ inputProps ? : InputProps ; /** If addonBefore or addonAfter exists, then the addons will render vertically. */ orientation ? : 'vertical' | 'horizontal' ; useInlineLabel ? : boolean ; } ; export type InputFieldProps = BoxProps & FieldWrapperProps & InputProps & LocalInputFieldProps ; const useInputFieldProps = createHook < InputFieldProps > ( ( props ) => { const { accessibilityLabelledBy , children , autoFocus , defaultValue , description , disabled , hint , iconAfter , iconAfterProps , iconBefore , iconBeforeProps , inputRef , inputProps , isOptional , isRequired , labelProps , labelTextColor , orientation , label , size , palette , placeholder , placeholderTextColor , state , value , onBlur , onChangeText , onFocus , overrides , validationText , variant , useInlineLabel , ... restProps } = props ; const boxProps = Box . useProps ( restProps ) ; return { ... boxProps , children : ( < FieldWrapper description = { description } hint = { hint } isOptional = { isOptional } isRequired = { isRequired } label = { ! useInlineLabel ? label : undefined } overrides = { overrides } state = { state } variant = { variant } validationText = { validationText } > < Input accessibilityLabelledBy = { accessibilityLabelledBy } iconAfter = { iconAfter } iconAfterProps = { iconAfterProps } iconBefore = { iconBefore } iconBeforeProps = { iconBeforeProps } inputProps = { inputProps } inputRef = { inputRef } label = { useInlineLabel ? label : undefined } labelTextColor = { labelTextColor } labelProps = { labelProps } palette = { palette } placeholder = { placeholder } placeholderTextColor = { placeholderTextColor } state = { state } size = { size } value = { value } variant = { variant } onBlur = { onBlur } onChangeText = { onChangeText } onFocus = { onFocus } overrides = { overrides } / > < /FieldWrapper>), } ; } , { defaultProps : { orientation : 'horizontal' } , themeKey : 'InputField' } ) ; export const InputField = createComponent < InputFieldProps > ( ( props ) => { const inputFieldProps = useInputFieldProps ( props ) ; return createElement ( { children : props . children , component : styles . InputField , htmlProps : inputFieldProps , } ) ; } , { attach : { useProps , displayName : 'InputField' , } , themeKey : 'InputField' , } ) ;