Bluesky app fork with some witchin' additions 馃挮
at main 334 lines 8.8 kB view raw
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}