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