mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at static-click 341 lines 8.1 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 {HITSLOP_20} from '#/lib/constants' 13import {mergeRefs} from '#/lib/merge-refs' 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.w_full, 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> | React.ForwardedRef<TextInput> 130} 131 132export function createInput(Component: typeof TextInput) { 133 return function Input({ 134 label, 135 placeholder, 136 value, 137 onChangeText, 138 onFocus, 139 onBlur, 140 isInvalid, 141 inputRef, 142 style, 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={e => { 179 ctx.onFocus() 180 onFocus?.(e) 181 }} 182 onBlur={e => { 183 ctx.onBlur() 184 onBlur?.(e) 185 }} 186 placeholder={placeholder || label} 187 placeholderTextColor={t.palette.contrast_500} 188 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 189 hitSlop={HITSLOP_20} 190 style={[ 191 a.relative, 192 a.z_20, 193 a.flex_1, 194 a.text_md, 195 t.atoms.text, 196 a.px_xs, 197 { 198 // paddingVertical doesn't work w/multiline - esb 199 paddingTop: 12, 200 paddingBottom: 13, 201 lineHeight: a.text_md.fontSize * 1.1875, 202 textAlignVertical: rest.multiline ? 'top' : undefined, 203 minHeight: rest.multiline ? 80 : undefined, 204 minWidth: 0, 205 }, 206 // fix for autofill styles covering border 207 web({ 208 paddingTop: 10, 209 paddingBottom: 11, 210 marginTop: 2, 211 marginBottom: 2, 212 }), 213 android({ 214 paddingTop: 8, 215 paddingBottom: 8, 216 }), 217 style, 218 ]} 219 /> 220 221 <View 222 style={[ 223 a.z_10, 224 a.absolute, 225 a.inset_0, 226 a.rounded_sm, 227 t.atoms.bg_contrast_25, 228 {borderColor: 'transparent', borderWidth: 2}, 229 ctx.hovered ? chromeHover : {}, 230 ctx.focused ? chromeFocus : {}, 231 ctx.isInvalid || isInvalid ? chromeError : {}, 232 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 233 ? chromeErrorHover 234 : {}, 235 ]} 236 /> 237 </> 238 ) 239 } 240} 241 242export const Input = createInput(TextInput) 243 244export function LabelText({ 245 nativeID, 246 children, 247}: React.PropsWithChildren<{nativeID?: string}>) { 248 const t = useTheme() 249 return ( 250 <Text 251 nativeID={nativeID} 252 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> 253 {children} 254 </Text> 255 ) 256} 257 258export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 259 const t = useTheme() 260 const ctx = React.useContext(Context) 261 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 262 const hover: TextStyle[] = [ 263 { 264 color: t.palette.contrast_800, 265 }, 266 ] 267 const focus: TextStyle[] = [ 268 { 269 color: t.palette.primary_500, 270 }, 271 ] 272 const errorHover: TextStyle[] = [ 273 { 274 color: t.palette.negative_500, 275 }, 276 ] 277 const errorFocus: TextStyle[] = [ 278 { 279 color: t.palette.negative_500, 280 }, 281 ] 282 283 return { 284 hover, 285 focus, 286 errorHover, 287 errorFocus, 288 } 289 }, [t]) 290 291 return ( 292 <View style={[a.z_20, a.pr_xs]}> 293 <Comp 294 size="md" 295 style={[ 296 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, 297 ctx.hovered ? hover : {}, 298 ctx.focused ? focus : {}, 299 ctx.isInvalid && ctx.hovered ? errorHover : {}, 300 ctx.isInvalid && ctx.focused ? errorFocus : {}, 301 ]} 302 /> 303 </View> 304 ) 305} 306 307export function SuffixText({ 308 children, 309 label, 310 accessibilityHint, 311}: React.PropsWithChildren<{ 312 label: string 313 accessibilityHint?: AccessibilityProps['accessibilityHint'] 314}>) { 315 const t = useTheme() 316 const ctx = React.useContext(Context) 317 return ( 318 <Text 319 accessibilityLabel={label} 320 accessibilityHint={accessibilityHint} 321 style={[ 322 a.z_20, 323 a.pr_sm, 324 a.text_md, 325 t.atoms.text_contrast_medium, 326 { 327 pointerEvents: 'none', 328 }, 329 web({ 330 marginTop: -2, 331 }), 332 ctx.hovered || ctx.focused 333 ? { 334 color: t.palette.contrast_800, 335 } 336 : {}, 337 ]}> 338 {children} 339 </Text> 340 ) 341}