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