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