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