mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at ruby-v 8.4 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, TextStyleProp, 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 /** 127 * @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible. 128 * 129 * See https://github.com/facebook/react-native-website/pull/4247 130 */ 131 value?: string 132 onChangeText?: (value: string) => void 133 isInvalid?: boolean 134 inputRef?: React.RefObject<TextInput> | React.ForwardedRef<TextInput> 135} 136 137export function createInput(Component: typeof TextInput) { 138 return function Input({ 139 label, 140 placeholder, 141 value, 142 onChangeText, 143 onFocus, 144 onBlur, 145 isInvalid, 146 inputRef, 147 style, 148 ...rest 149 }: InputProps) { 150 const t = useTheme() 151 const ctx = React.useContext(Context) 152 const withinRoot = Boolean(ctx.inputRef) 153 154 const {chromeHover, chromeFocus, chromeError, chromeErrorHover} = 155 useSharedInputStyles() 156 157 if (!withinRoot) { 158 return ( 159 <Root isInvalid={isInvalid}> 160 <Input 161 label={label} 162 placeholder={placeholder} 163 value={value} 164 onChangeText={onChangeText} 165 isInvalid={isInvalid} 166 {...rest} 167 /> 168 </Root> 169 ) 170 } 171 172 const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) 173 174 return ( 175 <> 176 <Component 177 accessibilityHint={undefined} 178 {...rest} 179 accessibilityLabel={label} 180 ref={refs} 181 value={value} 182 onChangeText={onChangeText} 183 onFocus={e => { 184 ctx.onFocus() 185 onFocus?.(e) 186 }} 187 onBlur={e => { 188 ctx.onBlur() 189 onBlur?.(e) 190 }} 191 placeholder={placeholder || label} 192 placeholderTextColor={t.palette.contrast_500} 193 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 194 hitSlop={HITSLOP_20} 195 style={[ 196 a.relative, 197 a.z_20, 198 a.flex_1, 199 a.text_md, 200 t.atoms.text, 201 a.px_xs, 202 { 203 // paddingVertical doesn't work w/multiline - esb 204 paddingTop: 12, 205 paddingBottom: 13, 206 lineHeight: a.text_md.fontSize * 1.1875, 207 textAlignVertical: rest.multiline ? 'top' : undefined, 208 minHeight: rest.multiline ? 80 : undefined, 209 minWidth: 0, 210 }, 211 // fix for autofill styles covering border 212 web({ 213 paddingTop: 10, 214 paddingBottom: 11, 215 marginTop: 2, 216 marginBottom: 2, 217 }), 218 android({ 219 paddingTop: 8, 220 paddingBottom: 8, 221 }), 222 style, 223 ]} 224 /> 225 226 <View 227 style={[ 228 a.z_10, 229 a.absolute, 230 a.inset_0, 231 a.rounded_sm, 232 t.atoms.bg_contrast_25, 233 {borderColor: 'transparent', borderWidth: 2}, 234 ctx.hovered ? chromeHover : {}, 235 ctx.focused ? chromeFocus : {}, 236 ctx.isInvalid || isInvalid ? chromeError : {}, 237 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused) 238 ? chromeErrorHover 239 : {}, 240 ]} 241 /> 242 </> 243 ) 244 } 245} 246 247export const Input = createInput(TextInput) 248 249export function LabelText({ 250 nativeID, 251 children, 252}: React.PropsWithChildren<{nativeID?: string}>) { 253 const t = useTheme() 254 return ( 255 <Text 256 nativeID={nativeID} 257 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> 258 {children} 259 </Text> 260 ) 261} 262 263export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) { 264 const t = useTheme() 265 const ctx = React.useContext(Context) 266 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => { 267 const hover: TextStyle[] = [ 268 { 269 color: t.palette.contrast_800, 270 }, 271 ] 272 const focus: TextStyle[] = [ 273 { 274 color: t.palette.primary_500, 275 }, 276 ] 277 const errorHover: TextStyle[] = [ 278 { 279 color: t.palette.negative_500, 280 }, 281 ] 282 const errorFocus: TextStyle[] = [ 283 { 284 color: t.palette.negative_500, 285 }, 286 ] 287 288 return { 289 hover, 290 focus, 291 errorHover, 292 errorFocus, 293 } 294 }, [t]) 295 296 return ( 297 <View style={[a.z_20, a.pr_xs]}> 298 <Comp 299 size="md" 300 style={[ 301 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0}, 302 ctx.hovered ? hover : {}, 303 ctx.focused ? focus : {}, 304 ctx.isInvalid && ctx.hovered ? errorHover : {}, 305 ctx.isInvalid && ctx.focused ? errorFocus : {}, 306 ]} 307 /> 308 </View> 309 ) 310} 311 312export function SuffixText({ 313 children, 314 label, 315 accessibilityHint, 316 style, 317}: React.PropsWithChildren< 318 TextStyleProp & { 319 label: string 320 accessibilityHint?: AccessibilityProps['accessibilityHint'] 321 } 322>) { 323 const t = useTheme() 324 const ctx = React.useContext(Context) 325 return ( 326 <Text 327 accessibilityLabel={label} 328 accessibilityHint={accessibilityHint} 329 numberOfLines={1} 330 style={[ 331 a.z_20, 332 a.pr_sm, 333 a.text_md, 334 t.atoms.text_contrast_medium, 335 { 336 pointerEvents: 'none', 337 }, 338 web({ 339 marginTop: -2, 340 }), 341 ctx.hovered || ctx.focused 342 ? { 343 color: t.palette.contrast_800, 344 } 345 : {}, 346 style, 347 ]}> 348 {children} 349 </Text> 350 ) 351}