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