diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 016f085caa..f31af14764 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -29,6 +29,7 @@ import ListAccordionExampleGroup from './Examples/ListAccordionGroupExample'; import ListItemExample from './Examples/ListItemExample'; import ListSectionExample from './Examples/ListSectionExample'; import MenuExample from './Examples/MenuExample'; +import ModalExample from './Examples/ModalExample'; import ProgressBarExample from './Examples/ProgressBarExample'; import RadioButtonExample from './Examples/RadioButtonExample'; import RadioButtonGroupExample from './Examples/RadioButtonGroupExample'; @@ -79,6 +80,7 @@ export const mainExamples: Record< listSection: ListSectionExample, listItem: ListItemExample, menu: MenuExample, + modalExample: ModalExample, progressbar: ProgressBarExample, radio: RadioButtonExample, radioGroup: RadioButtonGroupExample, diff --git a/example/src/Examples/ModalExample.tsx b/example/src/Examples/ModalExample.tsx new file mode 100644 index 0000000000..b7e3e86582 --- /dev/null +++ b/example/src/Examples/ModalExample.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { Button, List, Modal, Portal, Text } from 'react-native-paper'; + +import { useExampleTheme } from '../hooks/useExampleTheme'; +import ScreenWrapper from '../ScreenWrapper'; + +const ModalExample = () => { + const theme = useExampleTheme(); + + const [visibleModal1, setVisibleModal1] = React.useState(false); + const [visibleModal2, setVisibleModal2] = React.useState(false); + const [visibleModal3, setVisibleModal3] = React.useState(false); + + return ( + + + + + setVisibleModal1(false)} + contentContainerStyle={[ + styles.modal, + { backgroundColor: theme.colors.background }, + ]} + > + Example Modal. Click outside this area to dismiss. + + + + + + + + + setVisibleModal2(false)} + contentContainerStyle={[ + styles.modal, + { backgroundColor: theme.colors.background }, + ]} + > + + Example Modal with animations disabled. Click outside this area + to dismiss. + + + + + + + + + + setVisibleModal3(false)} + contentContainerStyle={[ + styles.modal, + { backgroundColor: theme.colors.background }, + ]} + > + Example Modal with escape handling. + + + + + + + ); +}; + +ModalExample.title = 'Modal'; + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + margin: 8, + }, + modal: { + padding: 20, + }, +}); + +export default ModalExample; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index be16a5abde..fd97236d92 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -4,6 +4,7 @@ import { Easing, StyleProp, StyleSheet, + Platform, Pressable, View, ViewStyle, @@ -40,6 +41,14 @@ export type Props = { * Determines Whether the modal is visible. */ visible: boolean; + /** + * Determines whether the modal uses animations. + */ + disableAnimations?: boolean; + /** + * Determines whether the modal closes on Escape on web. + */ + handleEscape?: boolean; /** * Content of the `Modal`. */ @@ -104,6 +113,8 @@ function Modal({ dismissable = true, dismissableBackButton = dismissable, visible = false, + disableAnimations = false, + handleEscape = false, overlayAccessibilityLabel = 'Close modal', onDismiss = () => {}, children, @@ -120,28 +131,34 @@ function Modal({ const [visibleInternal, setVisibleInternal] = React.useState(visible); const showModalAnimation = React.useCallback(() => { - Animated.timing(opacity, { - toValue: 1, - duration: scale * DEFAULT_DURATION, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); - }, [opacity, scale]); + if (!disableAnimations) { + Animated.timing(opacity, { + toValue: 1, + duration: scale * DEFAULT_DURATION, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(); + } + }, [opacity, scale, disableAnimations]); const hideModalAnimation = React.useCallback(() => { - Animated.timing(opacity, { - toValue: 0, - duration: scale * DEFAULT_DURATION, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(({ finished }) => { - if (!finished) { - return; - } + if (!disableAnimations) { + Animated.timing(opacity, { + toValue: 0, + duration: scale * DEFAULT_DURATION, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }).start(({ finished }) => { + if (!finished) { + return; + } + setVisibleInternal(false); + }); + } else { setVisibleInternal(false); - }); - }, [opacity, scale]); + } + }, [opacity, scale, disableAnimations]); React.useEffect(() => { if (visibleInternal === visible) { @@ -179,6 +196,23 @@ function Modal({ return () => subscription.remove(); }, [dismissable, dismissableBackButton, onDismissCallback, visible]); + React.useEffect(() => { + if (!visible || !handleEscape || Platform.OS !== 'web') { + return undefined; + } + + const closeOnEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation(); + if (dismissable) { + onDismissCallback(); + } + } + }; + document.addEventListener('keyup', closeOnEscape, false); + return () => document.removeEventListener('keyup', closeOnEscape, false); + }, [dismissable, onDismissCallback, visible, handleEscape]); + if (!visibleInternal) { return null; } @@ -202,8 +236,8 @@ function Modal({ styles.backdrop, { backgroundColor: theme.colors?.backdrop, - opacity, }, + ...(!disableAnimations ? [{ opacity }] : []), ]} testID={`${testID}-backdrop`} /> @@ -219,7 +253,11 @@ function Modal({ {children}