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