mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useImperativeHandle} from 'react'
2import {
3 FlatList,
4 type FlatListProps,
5 type GestureResponderEvent,
6 type StyleProp,
7 TouchableWithoutFeedback,
8 View,
9 type ViewStyle,
10} from 'react-native'
11import {msg} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal'
14import {RemoveScrollBar} from 'react-remove-scroll-bar'
15
16import {logger} from '#/logger'
17import {useA11y} from '#/state/a11y'
18import {useDialogStateControlContext} from '#/state/dialogs'
19import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
20import {Button, ButtonIcon} from '#/components/Button'
21import {Context} from '#/components/Dialog/context'
22import {
23 type DialogControlProps,
24 type DialogInnerProps,
25 type DialogOuterProps,
26} from '#/components/Dialog/types'
27import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
28import {Portal} from '#/components/Portal'
29
30export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
31export * from '#/components/Dialog/shared'
32export * from '#/components/Dialog/types'
33export * from '#/components/Dialog/utils'
34export {Input} from '#/components/forms/TextField'
35
36// 100 minus 10vh of paddingVertical
37export const WEB_DIALOG_HEIGHT = '80vh'
38
39const stopPropagation = (e: any) => e.stopPropagation()
40const preventDefault = (e: any) => e.preventDefault()
41
42export function Outer({
43 children,
44 control,
45 onClose,
46 webOptions,
47}: React.PropsWithChildren<DialogOuterProps>) {
48 const {_} = useLingui()
49 const {gtMobile} = useBreakpoints()
50 const [isOpen, setIsOpen] = React.useState(false)
51 const {setDialogIsOpen} = useDialogStateControlContext()
52
53 const open = React.useCallback(() => {
54 setDialogIsOpen(control.id, true)
55 setIsOpen(true)
56 }, [setIsOpen, setDialogIsOpen, control.id])
57
58 const close = React.useCallback<DialogControlProps['close']>(
59 cb => {
60 setDialogIsOpen(control.id, false)
61 setIsOpen(false)
62
63 try {
64 if (cb && typeof cb === 'function') {
65 // This timeout ensures that the callback runs at the same time as it would on native. I.e.
66 // console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
67 // This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
68 // 'Step 1', 'Step 3', 'Step 2'.
69 setTimeout(cb)
70 }
71 } catch (e: any) {
72 logger.error(`Dialog closeCallback failed`, {
73 message: e.message,
74 })
75 }
76
77 onClose?.()
78 },
79 [control.id, onClose, setDialogIsOpen],
80 )
81
82 const handleBackgroundPress = React.useCallback(
83 async (e: GestureResponderEvent) => {
84 webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close()
85 },
86 [webOptions, close],
87 )
88
89 useImperativeHandle(
90 control.ref,
91 () => ({
92 open,
93 close,
94 }),
95 [close, open],
96 )
97
98 const context = React.useMemo(
99 () => ({
100 close,
101 isNativeDialog: false,
102 nativeSnapPoint: 0,
103 disableDrag: false,
104 setDisableDrag: () => {},
105 isWithinDialog: true,
106 }),
107 [close],
108 )
109
110 return (
111 <>
112 {isOpen && (
113 <Portal>
114 <Context.Provider value={context}>
115 <RemoveScrollBar />
116 <TouchableWithoutFeedback
117 accessibilityHint={undefined}
118 accessibilityLabel={_(msg`Close active dialog`)}
119 onPress={handleBackgroundPress}>
120 <View
121 style={[
122 web(a.fixed),
123 a.inset_0,
124 a.z_10,
125 a.px_xl,
126 webOptions?.alignCenter ? a.justify_center : undefined,
127 a.align_center,
128 {
129 overflowY: 'auto',
130 paddingVertical: gtMobile ? '10vh' : a.pt_xl.paddingTop,
131 },
132 ]}>
133 <Backdrop />
134 {/**
135 * This is needed to prevent centered dialogs from overflowing
136 * above the screen, and provides a "natural" centering so that
137 * stacked dialogs appear relatively aligned.
138 */}
139 <View
140 style={[
141 a.w_full,
142 a.z_20,
143 a.align_center,
144 web({minHeight: '60vh', position: 'static'}),
145 ]}>
146 {children}
147 </View>
148 </View>
149 </TouchableWithoutFeedback>
150 </Context.Provider>
151 </Portal>
152 )}
153 </>
154 )
155}
156
157export function Inner({
158 children,
159 style,
160 label,
161 accessibilityLabelledBy,
162 accessibilityDescribedBy,
163 header,
164 contentContainerStyle,
165}: DialogInnerProps) {
166 const t = useTheme()
167 const {close} = React.useContext(Context)
168 const {gtMobile} = useBreakpoints()
169 const {reduceMotionEnabled} = useA11y()
170 FocusGuards.useFocusGuards()
171 return (
172 <FocusScope.FocusScope loop asChild trapped>
173 <View
174 role="dialog"
175 aria-role="dialog"
176 aria-label={label}
177 aria-labelledby={accessibilityLabelledBy}
178 aria-describedby={accessibilityDescribedBy}
179 // @ts-expect-error web only -prf
180 onClick={stopPropagation}
181 onStartShouldSetResponder={_ => true}
182 onTouchEnd={stopPropagation}
183 style={flatten([
184 a.relative,
185 a.rounded_md,
186 a.w_full,
187 a.border,
188 t.atoms.bg,
189 {
190 maxWidth: 600,
191 borderColor: t.palette.contrast_200,
192 shadowColor: t.palette.black,
193 shadowOpacity: t.name === 'light' ? 0.1 : 0.4,
194 shadowRadius: 30,
195 },
196 !reduceMotionEnabled && a.zoom_fade_in,
197 style,
198 ])}>
199 <DismissableLayer.DismissableLayer
200 onInteractOutside={preventDefault}
201 onFocusOutside={preventDefault}
202 onDismiss={close}
203 style={{height: '100%', display: 'flex', flexDirection: 'column'}}>
204 {header}
205 <View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
206 {children}
207 </View>
208 </DismissableLayer.DismissableLayer>
209 </View>
210 </FocusScope.FocusScope>
211 )
212}
213
214export const ScrollableInner = Inner
215
216export const InnerFlatList = React.forwardRef<
217 FlatList,
218 FlatListProps<any> & {label: string} & {
219 webInnerStyle?: StyleProp<ViewStyle>
220 webInnerContentContainerStyle?: StyleProp<ViewStyle>
221 footer?: React.ReactNode
222 }
223>(function InnerFlatList(
224 {
225 label,
226 style,
227 webInnerStyle,
228 webInnerContentContainerStyle,
229 footer,
230 ...props
231 },
232 ref,
233) {
234 const {gtMobile} = useBreakpoints()
235 return (
236 <Inner
237 label={label}
238 style={[
239 a.overflow_hidden,
240 a.px_0,
241 web({maxHeight: WEB_DIALOG_HEIGHT}),
242 webInnerStyle,
243 ]}
244 contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
245 <FlatList
246 ref={ref}
247 style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
248 {...props}
249 />
250 {footer}
251 </Inner>
252 )
253})
254
255export function FlatListFooter({children}: {children: React.ReactNode}) {
256 const t = useTheme()
257
258 return (
259 <View
260 style={[
261 a.absolute,
262 a.bottom_0,
263 a.w_full,
264 a.z_10,
265 t.atoms.bg,
266 a.border_t,
267 t.atoms.border_contrast_low,
268 a.px_lg,
269 a.py_md,
270 ]}>
271 {children}
272 </View>
273 )
274}
275
276export function Close() {
277 const {_} = useLingui()
278 const {close} = React.useContext(Context)
279 return (
280 <View
281 style={[
282 a.absolute,
283 a.z_10,
284 {
285 top: a.pt_md.paddingTop,
286 right: a.pr_md.paddingRight,
287 },
288 ]}>
289 <Button
290 size="small"
291 variant="ghost"
292 color="secondary"
293 shape="round"
294 onPress={() => close()}
295 label={_(msg`Close active dialog`)}>
296 <ButtonIcon icon={X} size="md" />
297 </Button>
298 </View>
299 )
300}
301
302export function Handle() {
303 return null
304}
305
306function Backdrop() {
307 const t = useTheme()
308 const {reduceMotionEnabled} = useA11y()
309 return (
310 <View style={{opacity: 0.8}}>
311 <View
312 style={[
313 a.fixed,
314 a.inset_0,
315 {backgroundColor: t.palette.black},
316 !reduceMotionEnabled && a.fade_in,
317 ]}
318 />
319 </View>
320 )
321}