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}