mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 View,
4 TextInput,
5 TextInputProps,
6 TextStyle,
7 ViewStyle,
8 StyleSheet,
9 AccessibilityProps,
10} from 'react-native'
11
12import {HITSLOP_20} from 'lib/constants'
13import {useTheme, atoms as a, web, android} from '#/alf'
14import {Text} from '#/components/Typography'
15import {useInteractionState} from '#/components/hooks/useInteractionState'
16import {Props as SVGIconProps} from '#/components/icons/common'
17
18const Context = React.createContext<{
19 inputRef: React.RefObject<TextInput> | null
20 isInvalid: boolean
21 hovered: boolean
22 onHoverIn: () => void
23 onHoverOut: () => void
24 focused: boolean
25 onFocus: () => void
26 onBlur: () => void
27}>({
28 inputRef: null,
29 isInvalid: false,
30 hovered: false,
31 onHoverIn: () => {},
32 onHoverOut: () => {},
33 focused: false,
34 onFocus: () => {},
35 onBlur: () => {},
36})
37
38export type RootProps = React.PropsWithChildren<{isInvalid?: boolean}>
39
40export function Root({children, isInvalid = false}: RootProps) {
41 const inputRef = React.useRef<TextInput>(null)
42 const {
43 state: hovered,
44 onIn: onHoverIn,
45 onOut: onHoverOut,
46 } = useInteractionState()
47 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
48
49 const context = React.useMemo(
50 () => ({
51 inputRef,
52 hovered,
53 onHoverIn,
54 onHoverOut,
55 focused,
56 onFocus,
57 onBlur,
58 isInvalid,
59 }),
60 [
61 inputRef,
62 hovered,
63 onHoverIn,
64 onHoverOut,
65 focused,
66 onFocus,
67 onBlur,
68 isInvalid,
69 ],
70 )
71
72 return (
73 <Context.Provider value={context}>
74 <View
75 style={[a.flex_row, a.align_center, a.relative, a.flex_1, a.px_md]}
76 {...web({
77 onClick: () => inputRef.current?.focus(),
78 onMouseOver: onHoverIn,
79 onMouseOut: onHoverOut,
80 })}>
81 {children}
82 </View>
83 </Context.Provider>
84 )
85}
86
87export function useSharedInputStyles() {
88 const t = useTheme()
89 return React.useMemo(() => {
90 const hover: ViewStyle[] = [
91 {
92 borderColor: t.palette.contrast_100,
93 },
94 ]
95 const focus: ViewStyle[] = [
96 {
97 backgroundColor: t.palette.contrast_50,
98 borderColor: t.palette.primary_500,
99 },
100 ]
101 const error: ViewStyle[] = [
102 {
103 backgroundColor:
104 t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
105 borderColor:
106 t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
107 },
108 ]
109 const errorHover: ViewStyle[] = [
110 {
111 backgroundColor:
112 t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
113 borderColor: t.palette.negative_500,
114 },
115 ]
116
117 return {
118 chromeHover: StyleSheet.flatten(hover),
119 chromeFocus: StyleSheet.flatten(focus),
120 chromeError: StyleSheet.flatten(error),
121 chromeErrorHover: StyleSheet.flatten(errorHover),
122 }
123 }, [t])
124}
125
126export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
127 label: string
128 value: string
129 onChangeText: (value: string) => void
130 isInvalid?: boolean
131}
132
133export function createInput(Component: typeof TextInput) {
134 return function Input({
135 label,
136 placeholder,
137 value,
138 onChangeText,
139 isInvalid,
140 ...rest
141 }: InputProps) {
142 const t = useTheme()
143 const ctx = React.useContext(Context)
144 const withinRoot = Boolean(ctx.inputRef)
145
146 const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
147 useSharedInputStyles()
148
149 if (!withinRoot) {
150 return (
151 <Root isInvalid={isInvalid}>
152 <Input
153 label={label}
154 placeholder={placeholder}
155 value={value}
156 onChangeText={onChangeText}
157 isInvalid={isInvalid}
158 {...rest}
159 />
160 </Root>
161 )
162 }
163
164 return (
165 <>
166 <Component
167 accessibilityHint={undefined}
168 {...rest}
169 accessibilityLabel={label}
170 ref={ctx.inputRef}
171 value={value}
172 onChangeText={onChangeText}
173 onFocus={ctx.onFocus}
174 onBlur={ctx.onBlur}
175 placeholder={placeholder || label}
176 placeholderTextColor={t.palette.contrast_500}
177 hitSlop={HITSLOP_20}
178 style={[
179 a.relative,
180 a.z_20,
181 a.flex_1,
182 a.text_md,
183 t.atoms.text,
184 a.px_xs,
185 {
186 // paddingVertical doesn't work w/multiline - esb
187 paddingTop: 14,
188 paddingBottom: 14,
189 lineHeight: a.text_md.fontSize * 1.1875,
190 textAlignVertical: rest.multiline ? 'top' : undefined,
191 minHeight: rest.multiline ? 80 : undefined,
192 },
193 android({
194 paddingBottom: 16,
195 }),
196 ]}
197 />
198
199 <View
200 style={[
201 a.z_10,
202 a.absolute,
203 a.inset_0,
204 a.rounded_sm,
205 t.atoms.bg_contrast_25,
206 {borderColor: 'transparent', borderWidth: 2},
207 ctx.hovered ? chromeHover : {},
208 ctx.focused ? chromeFocus : {},
209 ctx.isInvalid || isInvalid ? chromeError : {},
210 (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused)
211 ? chromeErrorHover
212 : {},
213 ]}
214 />
215 </>
216 )
217 }
218}
219
220export const Input = createInput(TextInput)
221
222export function Label({
223 nativeID,
224 children,
225}: React.PropsWithChildren<{nativeID?: string}>) {
226 const t = useTheme()
227 return (
228 <Text
229 nativeID={nativeID}
230 style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}>
231 {children}
232 </Text>
233 )
234}
235
236export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
237 const t = useTheme()
238 const ctx = React.useContext(Context)
239 const {hover, focus, errorHover, errorFocus} = React.useMemo(() => {
240 const hover: TextStyle[] = [
241 {
242 color: t.palette.contrast_800,
243 },
244 ]
245 const focus: TextStyle[] = [
246 {
247 color: t.palette.primary_500,
248 },
249 ]
250 const errorHover: TextStyle[] = [
251 {
252 color: t.palette.negative_500,
253 },
254 ]
255 const errorFocus: TextStyle[] = [
256 {
257 color: t.palette.negative_500,
258 },
259 ]
260
261 return {
262 hover,
263 focus,
264 errorHover,
265 errorFocus,
266 }
267 }, [t])
268
269 return (
270 <View style={[a.z_20, a.pr_xs]}>
271 <Comp
272 size="md"
273 style={[
274 {color: t.palette.contrast_500, pointerEvents: 'none'},
275 ctx.hovered ? hover : {},
276 ctx.focused ? focus : {},
277 ctx.isInvalid && ctx.hovered ? errorHover : {},
278 ctx.isInvalid && ctx.focused ? errorFocus : {},
279 ]}
280 />
281 </View>
282 )
283}
284
285export function Suffix({
286 children,
287 label,
288 accessibilityHint,
289}: React.PropsWithChildren<{
290 label: string
291 accessibilityHint?: AccessibilityProps['accessibilityHint']
292}>) {
293 const t = useTheme()
294 const ctx = React.useContext(Context)
295 return (
296 <Text
297 accessibilityLabel={label}
298 accessibilityHint={accessibilityHint}
299 style={[
300 a.z_20,
301 a.pr_sm,
302 a.text_md,
303 t.atoms.text_contrast_medium,
304 {
305 pointerEvents: 'none',
306 },
307 web({
308 marginTop: -2,
309 }),
310 ctx.hovered || ctx.focused
311 ? {
312 color: t.palette.contrast_800,
313 }
314 : {},
315 ]}>
316 {children}
317 </Text>
318 )
319}