mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react' 2import { 3 AccessibilityProps, 4 StyleSheet, 5 TextInput, 6 TextInputProps, 7 TextStyle, 8 View, 9 ViewStyle, 10} from 'react-native' 11 12import {mergeRefs} from '#/lib/merge-refs' 13import {HITSLOP_20} from 'lib/constants' 14import {android, atoms as a, useTheme, web} from '#/alf' 15import {useInteractionState} from '#/components/hooks/useInteractionState' 16import {Props as SVGIconProps} from '#/components/icons/common' 17import {Text} from '#/components/Typography' 18 19const Context = React.createContext<{ 20 inputRef: React.RefObject<TextInput> | null 21 isInvalid: boolean 22 hovered: boolean 23 onHoverIn: () => void 24 onHoverOut: () => void 25 focused: boolean 26 onFocus: () => void 27 onBlur: () => void 28}>({ 29 inputRef: null, 30 isInvalid: false, 31 hovered: false, 32 onHoverIn: () => {}, 33 onHoverOut: () => {}, 34 focused: false, 35 onFocus: () => {}, 36 onBlur: () => {}, 37}) 38 39export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}> 40 41export function Root({children, isInvalid = false}: RootProps) { 42 const inputRef = React.useRef<TextInput>(null) 43 const { 44 state: hovered, 45 onIn: onHoverIn, 46 onOut: onHoverOut, 47 } = useInteractionState() 48 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() 49 50 const context = React.useMemo( 51 () => ({ 52 inputRef, 53 hovered, 54 onHoverIn, 55 onHoverOut, 56 focused, 57 onFocus, 58 onBlur, 59 isInvalid, 60 }), 61 [ 62 inputRef, 63 hovered, 64 onHoverIn, 65 onHoverOut, 66 focused, 67 onFocus, 68 onBlur, 69 isInvalid, 70 ], 71 ) 72 73 return ( 74 <Context.Provider value={context}> 75 <View 76 style={[a.flex_row, a.align_center, a.relative, a.flex_1, a.px_md]} 77 {...web({ 78 onClick: () => inputRef.current?.focus(), 79 onMouseOver: onHoverIn, 80 onMouseOut: onHoverOut, 81 })}> 82 {children} 83 </View> 84 </Context.Provider> 85 ) 86} 87 88export function useSharedInputStyles() { 89 const t = useTheme() 90 return React.useMemo(() => { 91 const hover: ViewStyle[] = [ 92 { 93 borderColor: t.palette.contrast_100, 94 }, 95 ] 96 const focus: ViewStyle[] = [ 97 { 98 backgroundColor: t.palette.contrast_50, 99 borderColor: t.palette.primary_500, 100 }, 101 ] 102 const error: ViewStyle[] = [ 103 { 104 backgroundColor: 105 t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 106 borderColor: 107 t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, 108 }, 109 ] 110 const errorHover: ViewStyle[] = [ 111 { 112 backgroundColor: 113 t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, 114 borderColor: t.palette.negative_500, 115 }, 116 ] 117 118 return { 119 chromeHover: StyleSheet.flatten(hover), 120 chromeFocus: StyleSheet.flatten(focus), 121 chromeError: StyleSheet.flatten(error), 122 chromeErrorHover: StyleSheet.flatten(errorHover), 123 } 124 }, [t]) 125} 126 127export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & { 128 label: string 129 value?: string 130 onChangeText?: (value: string) => void 131 isInvalid?: boolean 132 inputRef?: React.RefObject<TextInput> 133} 134 135export function createInput(Component: typeof TextInput) { 136 return function Input({ 137 label, 138 placeholder, 139 value, 140 onChangeText, 141 isInvalid, 142 inputRef, 143 ...rest 144 }: InputProps) { 145 const t = useTheme() 146 const ctx = React.useContext(Context) 147 const withinRoot = Boolean(ctx.inputRef) 148 149 const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = 150 useSharedInputStyles() 151 152 if (!withinRoot) { 153 return ( 154 <Root isInvalid={isInvalid}> 155 <Input 156 label={label} 157 placeholder={placeholder} 158 value={value} 159 onChangeText={onChangeText} 160 isInvalid={isInvalid} 161 {...rest} 162 /> 163 </Root> 164 ) 165 } 166 167 const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) 168 169 return ( 170 <> 171 <Component 172 accessibilityHint={undefined} 173 {...rest} 174 accessibilityLabel={label} 175 ref={refs} 176 value={value} 177 onChangeText={onChangeText} 178 onFocus={ctx.onFocus} 179 onBlur={ctx.onBlur} 180 placeholder={placeholder || label} 181 placeholderTextColor={t.palette.contrast_500} 182 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 183 hitSlop={HITSLOP_20} 184 style={[ 185 a.relative, 186 a.z_20, 187 a.flex_1, 188 a.text_md, 189 t.atoms.text, 190 a.px_xs, 191 { 192 // paddingVertical doesn't work w/multiline - esb 193 paddingTop: 14, 194 paddingBottom: 14, 195 lineHeight: a.text_md.fontSize * 1.1875, 196 textAlignVertical: rest.multiline ? 'top' : undefined, 197 minHeight: rest.multiline ? 80 : undefined, 198 }, 199 android({ 200 paddingBottom: 16, 201 }), 202 ]} 203 /> 204 205 <View 206 style={[ 207 a.z_10, 208 a.absolute, 209 a.inset_0, 210 a.rounded_sm, 211 t.atoms.bg_contrast_25, 212 {borderColor: 'transparent', borderWidth: 2}, 213 ctx.hovered ? chromeHover : {}, 214 ctx.focused ? chromeFocus : {}, 215 ctx.isInvalid || isInvalid ? chromeError : {}, 216 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 217 ? chromeErrorHover 218 : {}, 219 ]} 220 /> 221 </> 222 ) 223 } 224} 225 226export const Input = createInput(TextInput) 227 228export function LabelText({ 229 nativeID, 230 children, 231}: React.PropsWithChildren<{nativeID?: string}>) { 232 const t = useTheme() 233 return ( 234 <Text 235 nativeID={nativeID} 236 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> 237 {children} 238 </Text> 239 ) 240} 241 242export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 243 const t = useTheme() 244 const ctx = React.useContext(Context) 245 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 246 const hover: TextStyle[] = [ 247 { 248 color: t.palette.contrast_800, 249 }, 250 ] 251 const focus: TextStyle[] = [ 252 { 253 color: t.palette.primary_500, 254 }, 255 ] 256 const errorHover: TextStyle[] = [ 257 { 258 color: t.palette.negative_500, 259 }, 260 ] 261 const errorFocus: TextStyle[] = [ 262 { 263 color: t.palette.negative_500, 264 }, 265 ] 266 267 return { 268 hover, 269 focus, 270 errorHover, 271 errorFocus, 272 } 273 }, [t]) 274 275 return ( 276 <View style={[a.z_20, a.pr_xs]}> 277 <Comp 278 size="md" 279 style={[ 280 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, 281 ctx.hovered ? hover : {}, 282 ctx.focused ? focus : {}, 283 ctx.isInvalid && ctx.hovered ? errorHover : {}, 284 ctx.isInvalid && ctx.focused ? errorFocus : {}, 285 ]} 286 /> 287 </View> 288 ) 289} 290 291export function SuffixText({ 292 children, 293 label, 294 accessibilityHint, 295}: React.PropsWithChildren<{ 296 label: string 297 accessibilityHint?: AccessibilityProps['accessibilityHint'] 298}>) { 299 const t = useTheme() 300 const ctx = React.useContext(Context) 301 return ( 302 <Text 303 accessibilityLabel={label} 304 accessibilityHint={accessibilityHint} 305 style={[ 306 a.z_20, 307 a.pr_sm, 308 a.text_md, 309 t.atoms.text_contrast_medium, 310 { 311 pointerEvents: 'none', 312 }, 313 web({ 314 marginTop: -2, 315 }), 316 ctx.hovered || ctx.focused 317 ? { 318 color: t.palette.contrast_800, 319 } 320 : {}, 321 ]}> 322 {children} 323 </Text> 324 ) 325}