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