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 ...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={ctx.onFocus}
179 onBlur={ctx.onBlur}
180 placeholder={placeholder || label}
181 placeholderTextColor={t.palette.contrast_500}
182 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
183 hitSlop={HITSLOP_20}
184 style={[
185 a.relative,
186 a.z_20,
187 a.flex_1,
188 a.text_md,
189 t.atoms.text,
190 a.px_xs,
191 {
192 // paddingVertical doesn't work w/multiline - esb
193 paddingTop: 14,
194 paddingBottom: 14,
195 lineHeight: a.text_md.fontSize * 1.1875,
196 textAlignVertical: rest.multiline ? 'top' : undefined,
197 minHeight: rest.multiline ? 80 : undefined,
198 },
199 android({
200 paddingBottom: 16,
201 }),
202 ]}
203 />
204
205 <View
206 style={[
207 a.z_10,
208 a.absolute,
209 a.inset_0,
210 a.rounded_sm,
211 t.atoms.bg_contrast_25,
212 {borderColor: 'transparent', borderWidth: 2},
213 ctx.hovered ? chromeHover : {},
214 ctx.focused ? chromeFocus : {},
215 ctx.isInvalid || isInvalid ? chromeError : {},
216 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused)
217 ? chromeErrorHover
218 : {},
219 ]}
220 />
221 </>
222 )
223 }
224}
225
226export const Input = createInput(TextInput)
227
228export function LabelText({
229 nativeID,
230 children,
231}: React.PropsWithChildren<{nativeID?: string}>) {
232 const t = useTheme()
233 return (
234 <Text
235 nativeID={nativeID}
236 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}>
237 {children}
238 </Text>
239 )
240}
241
242export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
243 const t = useTheme()
244 const ctx = React.useContext(Context)
245 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
246 const hover: TextStyle[] = [
247 {
248 color: t.palette.contrast_800,
249 },
250 ]
251 const focus: TextStyle[] = [
252 {
253 color: t.palette.primary_500,
254 },
255 ]
256 const errorHover: TextStyle[] = [
257 {
258 color: t.palette.negative_500,
259 },
260 ]
261 const errorFocus: TextStyle[] = [
262 {
263 color: t.palette.negative_500,
264 },
265 ]
266
267 return {
268 hover,
269 focus,
270 errorHover,
271 errorFocus,
272 }
273 }, [t])
274
275 return (
276 <View style={[a.z_20, a.pr_xs]}>
277 <Comp
278 size="md"
279 style={[
280 {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0},
281 ctx.hovered ? hover : {},
282 ctx.focused ? focus : {},
283 ctx.isInvalid && ctx.hovered ? errorHover : {},
284 ctx.isInvalid && ctx.focused ? errorFocus : {},
285 ]}
286 />
287 </View>
288 )
289}
290
291export function SuffixText({
292 children,
293 label,
294 accessibilityHint,
295}: React.PropsWithChildren<{
296 label: string
297 accessibilityHint?: AccessibilityProps['accessibilityHint']
298}>) {
299 const t = useTheme()
300 const ctx = React.useContext(Context)
301 return (
302 <Text
303 accessibilityLabel={label}
304 accessibilityHint={accessibilityHint}
305 style={[
306 a.z_20,
307 a.pr_sm,
308 a.text_md,
309 t.atoms.text_contrast_medium,
310 {
311 pointerEvents: 'none',
312 },
313 web({
314 marginTop: -2,
315 }),
316 ctx.hovered || ctx.focused
317 ? {
318 color: t.palette.contrast_800,
319 }
320 : {},
321 ]}>
322 {children}
323 </Text>
324 )
325}