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