2023-07-06 20:03:57 +02:00
|
|
|
import * as React from 'react';
|
2024-06-10 23:31:06 +02:00
|
|
|
import { RefObject, useCallback, useMemo, useRef } from 'react';
|
2024-11-16 23:09:50 +02:00
|
|
|
import { GestureResponderEvent, Modal, ModalProps, ScrollView, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
|
2023-07-06 20:03:57 +02:00
|
|
|
import { hasNotch } from 'react-native-device-info';
|
|
|
|
|
|
|
|
interface ModalElementProps extends ModalProps {
|
|
|
|
children: React.ReactNode;
|
|
|
|
containerStyle?: ViewStyle;
|
2024-05-27 10:05:15 +02:00
|
|
|
backgroundColor?: string;
|
2024-11-16 23:09:50 +02:00
|
|
|
|
|
|
|
// If scrollOverflow is provided, the modal is wrapped in a vertical
|
|
|
|
// ScrollView. This allows the user to scroll parts of dialogs into
|
|
|
|
// view that would otherwise be clipped by the screen edge.
|
|
|
|
scrollOverflow?: boolean;
|
2023-07-06 20:03:57 +02:00
|
|
|
}
|
|
|
|
|
2024-11-16 23:09:50 +02:00
|
|
|
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
2024-05-27 10:05:15 +02:00
|
|
|
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
|
|
|
const isLandscape = windowWidth > windowHeight;
|
|
|
|
return useMemo(() => {
|
2024-06-10 23:31:06 +02:00
|
|
|
const backgroundPadding: ViewStyle = isLandscape ? {
|
|
|
|
paddingRight: hasNotch() ? 60 : 0,
|
|
|
|
paddingLeft: hasNotch() ? 60 : 0,
|
|
|
|
paddingTop: 15,
|
|
|
|
paddingBottom: 15,
|
|
|
|
} : {
|
|
|
|
paddingTop: hasNotch() ? 65 : 15,
|
|
|
|
paddingBottom: hasNotch() ? 35 : 15,
|
|
|
|
};
|
2024-05-27 10:05:15 +02:00
|
|
|
return StyleSheet.create({
|
2024-06-10 23:31:06 +02:00
|
|
|
modalBackground: {
|
|
|
|
...backgroundPadding,
|
2024-11-16 23:09:50 +02:00
|
|
|
flexGrow: 1,
|
|
|
|
flexShrink: 1,
|
|
|
|
|
|
|
|
// When hasScrollView, the modal background is wrapped in a ScrollView. In this case, it's
|
|
|
|
// possible to scroll content outside the background into view. To prevent the edge of the
|
|
|
|
// background from being visible, the background color is applied to the ScrollView container
|
|
|
|
// instead:
|
|
|
|
backgroundColor: hasScrollView ? null : backgroundColor,
|
|
|
|
},
|
|
|
|
modalScrollView: {
|
2024-06-10 23:31:06 +02:00
|
|
|
backgroundColor,
|
|
|
|
flexGrow: 1,
|
2024-08-02 15:51:49 +02:00
|
|
|
flexShrink: 1,
|
2024-05-27 10:05:15 +02:00
|
|
|
},
|
2024-11-16 23:09:50 +02:00
|
|
|
modalScrollViewContent: {
|
|
|
|
// Make the scroll view's scrolling region at least as tall as its container.
|
|
|
|
// This makes it possible to vertically center the content of scrollable modals.
|
|
|
|
flexGrow: 1,
|
|
|
|
},
|
2024-05-27 10:05:15 +02:00
|
|
|
});
|
2024-11-16 23:09:50 +02:00
|
|
|
}, [hasScrollView, isLandscape, backgroundColor]);
|
2024-05-27 10:05:15 +02:00
|
|
|
};
|
|
|
|
|
2024-06-10 23:31:06 +02:00
|
|
|
const useBackgroundTouchListeners = (onRequestClose: (event: GestureResponderEvent)=> void, backdropRef: RefObject<View>) => {
|
|
|
|
const onShouldBackgroundCaptureTouch = useCallback((event: GestureResponderEvent) => {
|
|
|
|
return event.target === backdropRef.current && event.nativeEvent.touches.length === 1;
|
|
|
|
}, [backdropRef]);
|
|
|
|
|
|
|
|
const onBackgroundTouchFinished = useCallback((event: GestureResponderEvent) => {
|
|
|
|
if (event.target === backdropRef.current) {
|
|
|
|
onRequestClose?.(event);
|
|
|
|
}
|
|
|
|
}, [onRequestClose, backdropRef]);
|
|
|
|
|
|
|
|
return { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished };
|
|
|
|
};
|
|
|
|
|
2023-07-06 20:03:57 +02:00
|
|
|
const ModalElement: React.FC<ModalElementProps> = ({
|
|
|
|
children,
|
|
|
|
containerStyle,
|
2024-05-27 10:05:15 +02:00
|
|
|
backgroundColor,
|
2024-11-16 23:09:50 +02:00
|
|
|
scrollOverflow,
|
2023-07-06 20:03:57 +02:00
|
|
|
...modalProps
|
|
|
|
}) => {
|
2024-11-16 23:09:50 +02:00
|
|
|
const styles = useStyles(scrollOverflow, backgroundColor);
|
2024-05-27 10:05:15 +02:00
|
|
|
|
|
|
|
// contentWrapper adds padding. To allow styling the region outside of the modal
|
|
|
|
// (e.g. to add a background), the content is wrapped twice.
|
|
|
|
const content = (
|
2024-06-10 23:31:06 +02:00
|
|
|
<View style={containerStyle}>
|
2024-05-27 10:05:15 +02:00
|
|
|
{children}
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
|
2024-06-10 23:31:06 +02:00
|
|
|
const backgroundRef = useRef<View>();
|
|
|
|
const { onShouldBackgroundCaptureTouch, onBackgroundTouchFinished } = useBackgroundTouchListeners(modalProps.onRequestClose, backgroundRef);
|
|
|
|
|
2024-11-16 23:09:50 +02:00
|
|
|
const contentAndBackdrop = <View
|
|
|
|
ref={backgroundRef}
|
|
|
|
style={styles.modalBackground}
|
|
|
|
onStartShouldSetResponder={onShouldBackgroundCaptureTouch}
|
|
|
|
onResponderRelease={onBackgroundTouchFinished}
|
|
|
|
>{content}</View>;
|
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
// supportedOrientations: On iOS, this allows the dialog to be shown in non-portrait orientations.
|
2023-07-06 20:03:57 +02:00
|
|
|
return (
|
2024-03-25 13:39:48 +02:00
|
|
|
<Modal
|
|
|
|
supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
|
|
|
|
{...modalProps}
|
|
|
|
>
|
2024-11-16 23:09:50 +02:00
|
|
|
{scrollOverflow ? (
|
|
|
|
<ScrollView
|
|
|
|
style={styles.modalScrollView}
|
|
|
|
contentContainerStyle={styles.modalScrollViewContent}
|
|
|
|
>{contentAndBackdrop}</ScrollView>
|
|
|
|
) : contentAndBackdrop}
|
2023-07-06 20:03:57 +02:00
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default ModalElement;
|