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