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: t.palette.negative_25, 105 borderColor: t.palette.negative_300, 106 }, 107 ] 108 const errorHover: ViewStyle[] = [ 109 { 110 backgroundColor: t.palette.negative_25, 111 borderColor: t.palette.negative_500, 112 }, 113 ] 114 115 return { 116 chromeHover: StyleSheet.flatten(hover), 117 chromeFocus: StyleSheet.flatten(focus), 118 chromeError: StyleSheet.flatten(error), 119 chromeErrorHover: StyleSheet.flatten(errorHover), 120 } 121 }, [t]) 122} 123 124export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & { 125 label: string 126 value?: string 127 onChangeText?: (value: string) => void 128 isInvalid?: boolean 129 inputRef?: React.RefObject<TextInput> 130} 131 132export function createInput(Component: typeof TextInput) { 133 return function Input({ 134 label, 135 placeholder, 136 value, 137 onChangeText, 138 isInvalid, 139 inputRef, 140 style, 141 ...rest 142 }: InputProps) { 143 const t = useTheme() 144 const ctx = React.useContext(Context) 145 const withinRoot = Boolean(ctx.inputRef) 146 147 const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = 148 useSharedInputStyles() 149 150 if (!withinRoot) { 151 return ( 152 <Root isInvalid={isInvalid}> 153 <Input 154 label={label} 155 placeholder={placeholder} 156 value={value} 157 onChangeText={onChangeText} 158 isInvalid={isInvalid} 159 {...rest} 160 /> 161 </Root> 162 ) 163 } 164 165 const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) 166 167 return ( 168 <> 169 <Component 170 accessibilityHint={undefined} 171 {...rest} 172 accessibilityLabel={label} 173 ref={refs} 174 value={value} 175 onChangeText={onChangeText} 176 onFocus={ctx.onFocus} 177 onBlur={ctx.onBlur} 178 placeholder={placeholder || label} 179 placeholderTextColor={t.palette.contrast_500} 180 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 181 hitSlop={HITSLOP_20} 182 style={[ 183 a.relative, 184 a.z_20, 185 a.flex_1, 186 a.text_md, 187 t.atoms.text, 188 a.px_xs, 189 { 190 // paddingVertical doesn't work w/multiline - esb 191 paddingTop: 14, 192 paddingBottom: 14, 193 lineHeight: a.text_md.fontSize * 1.1875, 194 textAlignVertical: rest.multiline ? 'top' : undefined, 195 minHeight: rest.multiline ? 80 : undefined, 196 }, 197 // fix for autofill styles covering border 198 web({ 199 paddingTop: 12, 200 paddingBottom: 12, 201 marginTop: 2, 202 marginBottom: 2, 203 }), 204 android({ 205 paddingBottom: 16, 206 }), 207 style, 208 ]} 209 /> 210 211 <View 212 style={[ 213 a.z_10, 214 a.absolute, 215 a.inset_0, 216 a.rounded_sm, 217 t.atoms.bg_contrast_25, 218 {borderColor: 'transparent', borderWidth: 2}, 219 ctx.hovered ? chromeHover : {}, 220 ctx.focused ? chromeFocus : {}, 221 ctx.isInvalid || isInvalid ? chromeError : {}, 222 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 223 ? chromeErrorHover 224 : {}, 225 ]} 226 /> 227 </> 228 ) 229 } 230} 231 232export const Input = createInput(TextInput) 233 234export function LabelText({ 235 nativeID, 236 children, 237}: React.PropsWithChildren<{nativeID?: string}>) { 238 const t = useTheme() 239 return ( 240 <Text 241 nativeID={nativeID} 242 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> 243 {children} 244 </Text> 245 ) 246} 247 248export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 249 const t = useTheme() 250 const ctx = React.useContext(Context) 251 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 252 const hover: TextStyle[] = [ 253 { 254 color: t.palette.contrast_800, 255 }, 256 ] 257 const focus: TextStyle[] = [ 258 { 259 color: t.palette.primary_500, 260 }, 261 ] 262 const errorHover: TextStyle[] = [ 263 { 264 color: t.palette.negative_500, 265 }, 266 ] 267 const errorFocus: TextStyle[] = [ 268 { 269 color: t.palette.negative_500, 270 }, 271 ] 272 273 return { 274 hover, 275 focus, 276 errorHover, 277 errorFocus, 278 } 279 }, [t]) 280 281 return ( 282 <View style={[a.z_20, a.pr_xs]}> 283 <Comp 284 size="md" 285 style={[ 286 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, 287 ctx.hovered ? hover : {}, 288 ctx.focused ? focus : {}, 289 ctx.isInvalid && ctx.hovered ? errorHover : {}, 290 ctx.isInvalid && ctx.focused ? errorFocus : {}, 291 ]} 292 /> 293 </View> 294 ) 295} 296 297export function SuffixText({ 298 children, 299 label, 300 accessibilityHint, 301}: React.PropsWithChildren<{ 302 label: string 303 accessibilityHint?: AccessibilityProps['accessibilityHint'] 304}>) { 305 const t = useTheme() 306 const ctx = React.useContext(Context) 307 return ( 308 <Text 309 accessibilityLabel={label} 310 accessibilityHint={accessibilityHint} 311 style={[ 312 a.z_20, 313 a.pr_sm, 314 a.text_md, 315 t.atoms.text_contrast_medium, 316 { 317 pointerEvents: 'none', 318 }, 319 web({ 320 marginTop: -2, 321 }), 322 ctx.hovered || ctx.focused 323 ? { 324 color: t.palette.contrast_800, 325 } 326 : {}, 327 ]}> 328 {children} 329 </Text> 330 ) 331}