mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}