this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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