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