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