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 minWidth: 0,
197 },
198 // fix for autofill styles covering border
199 web({
200 paddingTop: 12,
201 paddingBottom: 12,
202 marginTop: 2,
203 marginBottom: 2,
204 }),
205 android({
206 paddingBottom: 16,
207 }),
208 style,
209 ]}
210 />
211
212 <View
213 style={[
214 a.z_10,
215 a.absolute,
216 a.inset_0,
217 a.rounded_sm,
218 t.atoms.bg_contrast_25,
219 {borderColor: 'transparent', borderWidth: 2},
220 ctx.hovered ? chromeHover : {},
221 ctx.focused ? chromeFocus : {},
222 ctx.isInvalid || isInvalid ? chromeError : {},
223 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused)
224 ? chromeErrorHover
225 : {},
226 ]}
227 />
228 </>
229 )
230 }
231}
232
233export const Input = createInput(TextInput)
234
235export function LabelText({
236 nativeID,
237 children,
238}: React.PropsWithChildren<{nativeID?: string}>) {
239 const t = useTheme()
240 return (
241 <Text
242 nativeID={nativeID}
243 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}>
244 {children}
245 </Text>
246 )
247}
248
249export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
250 const t = useTheme()
251 const ctx = React.useContext(Context)
252 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
253 const hover: TextStyle[] = [
254 {
255 color: t.palette.contrast_800,
256 },
257 ]
258 const focus: TextStyle[] = [
259 {
260 color: t.palette.primary_500,
261 },
262 ]
263 const errorHover: TextStyle[] = [
264 {
265 color: t.palette.negative_500,
266 },
267 ]
268 const errorFocus: TextStyle[] = [
269 {
270 color: t.palette.negative_500,
271 },
272 ]
273
274 return {
275 hover,
276 focus,
277 errorHover,
278 errorFocus,
279 }
280 }, [t])
281
282 return (
283 <View style={[a.z_20, a.pr_xs]}>
284 <Comp
285 size="md"
286 style={[
287 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0},
288 ctx.hovered ? hover : {},
289 ctx.focused ? focus : {},
290 ctx.isInvalid && ctx.hovered ? errorHover : {},
291 ctx.isInvalid && ctx.focused ? errorFocus : {},
292 ]}
293 />
294 </View>
295 )
296}
297
298export function SuffixText({
299 children,
300 label,
301 accessibilityHint,
302}: React.PropsWithChildren<{
303 label: string
304 accessibilityHint?: AccessibilityProps['accessibilityHint']
305}>) {
306 const t = useTheme()
307 const ctx = React.useContext(Context)
308 return (
309 <Text
310 accessibilityLabel={label}
311 accessibilityHint={accessibilityHint}
312 style={[
313 a.z_20,
314 a.pr_sm,
315 a.text_md,
316 t.atoms.text_contrast_medium,
317 {
318 pointerEvents: 'none',
319 },
320 web({
321 marginTop: -2,
322 }),
323 ctx.hovered || ctx.focused
324 ? {
325 color: t.palette.contrast_800,
326 }
327 : {},
328 ]}>
329 {children}
330 </Text>
331 )
332}