mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 7.9 kB view raw
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 minWidth: 0, 197 }, 198 // fix for autofill styles covering border 199 web({ 200 paddingTop: 12, 201 paddingBottom: 12, 202 marginTop: 2, 203 marginBottom: 2, 204 }), 205 android({ 206 paddingBottom: 16, 207 }), 208 style, 209 ]} 210 /> 211 212 <View 213 style={[ 214 a.z_10, 215 a.absolute, 216 a.inset_0, 217 a.rounded_sm, 218 t.atoms.bg_contrast_25, 219 {borderColor: 'transparent', borderWidth: 2}, 220 ctx.hovered ? chromeHover : {}, 221 ctx.focused ? chromeFocus : {}, 222 ctx.isInvalid || isInvalid ? chromeError : {}, 223 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 224 ? chromeErrorHover 225 : {}, 226 ]} 227 /> 228 </> 229 ) 230 } 231} 232 233export const Input = createInput(TextInput) 234 235export function LabelText({ 236 nativeID, 237 children, 238}: React.PropsWithChildren<{nativeID?: string}>) { 239 const t = useTheme() 240 return ( 241 <Text 242 nativeID={nativeID} 243 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> 244 {children} 245 </Text> 246 ) 247} 248 249export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 250 const t = useTheme() 251 const ctx = React.useContext(Context) 252 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 253 const hover: TextStyle[] = [ 254 { 255 color: t.palette.contrast_800, 256 }, 257 ] 258 const focus: TextStyle[] = [ 259 { 260 color: t.palette.primary_500, 261 }, 262 ] 263 const errorHover: TextStyle[] = [ 264 { 265 color: t.palette.negative_500, 266 }, 267 ] 268 const errorFocus: TextStyle[] = [ 269 { 270 color: t.palette.negative_500, 271 }, 272 ] 273 274 return { 275 hover, 276 focus, 277 errorHover, 278 errorFocus, 279 } 280 }, [t]) 281 282 return ( 283 <View style={[a.z_20, a.pr_xs]}> 284 <Comp 285 size="md" 286 style={[ 287 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, 288 ctx.hovered ? hover : {}, 289 ctx.focused ? focus : {}, 290 ctx.isInvalid && ctx.hovered ? errorHover : {}, 291 ctx.isInvalid && ctx.focused ? errorFocus : {}, 292 ]} 293 /> 294 </View> 295 ) 296} 297 298export function SuffixText({ 299 children, 300 label, 301 accessibilityHint, 302}: React.PropsWithChildren<{ 303 label: string 304 accessibilityHint?: AccessibilityProps['accessibilityHint'] 305}>) { 306 const t = useTheme() 307 const ctx = React.useContext(Context) 308 return ( 309 <Text 310 accessibilityLabel={label} 311 accessibilityHint={accessibilityHint} 312 style={[ 313 a.z_20, 314 a.pr_sm, 315 a.text_md, 316 t.atoms.text_contrast_medium, 317 { 318 pointerEvents: 'none', 319 }, 320 web({ 321 marginTop: -2, 322 }), 323 ctx.hovered || ctx.focused 324 ? { 325 color: t.palette.contrast_800, 326 } 327 : {}, 328 ]}> 329 {children} 330 </Text> 331 ) 332}