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