Skip to content

feat: modal disableAnimations and handleEscape props #4776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/src/ExampleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +80,7 @@ export const mainExamples: Record<
listSection: ListSectionExample,
listItem: ListItemExample,
menu: MenuExample,
modalExample: ModalExample,
progressbar: ProgressBarExample,
radio: RadioButtonExample,
radioGroup: RadioButtonGroupExample,
Expand Down
98 changes: 98 additions & 0 deletions example/src/Examples/ModalExample.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ScreenWrapper>
<List.Section title="Simple">
<View style={styles.row}>
<Portal>
<Modal
visible={visibleModal1}
onDismiss={() => setVisibleModal1(false)}
contentContainerStyle={[
styles.modal,
{ backgroundColor: theme.colors.background },
]}
>
<Text>Example Modal. Click outside this area to dismiss.</Text>
</Modal>
</Portal>
<Button mode="outlined" onPress={() => setVisibleModal1(true)}>
Open Modal
</Button>
</View>
</List.Section>
<List.Section title="Disable Animations">
<View style={styles.row}>
<Portal>
<Modal
disableAnimations={true}
visible={visibleModal2}
onDismiss={() => setVisibleModal2(false)}
contentContainerStyle={[
styles.modal,
{ backgroundColor: theme.colors.background },
]}
>
<Text>
Example Modal with animations disabled. Click outside this area
to dismiss.
</Text>
</Modal>
</Portal>
<Button mode="outlined" onPress={() => setVisibleModal2(true)}>
Open Modal
</Button>
</View>
</List.Section>
<List.Section title="Handle escape (web)">
<View style={styles.row}>
<Portal>
<Modal
handleEscape={true}
visible={visibleModal3}
onDismiss={() => setVisibleModal3(false)}
contentContainerStyle={[
styles.modal,
{ backgroundColor: theme.colors.background },
]}
>
<Text>Example Modal with escape handling.</Text>
</Modal>
</Portal>
<Button mode="outlined" onPress={() => setVisibleModal3(true)}>
Open Modal
</Button>
</View>
</List.Section>
</ScreenWrapper>
);
};

ModalExample.title = 'Modal';

const styles = StyleSheet.create({
row: {
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
margin: 8,
},
modal: {
padding: 20,
},
});

export default ModalExample;
78 changes: 58 additions & 20 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Easing,
StyleProp,
StyleSheet,
Platform,
Pressable,
View,
ViewStyle,
Expand Down Expand Up @@ -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`.
*/
Expand Down Expand Up @@ -104,6 +113,8 @@ function Modal({
dismissable = true,
dismissableBackButton = dismissable,
visible = false,
disableAnimations = false,
handleEscape = false,
overlayAccessibilityLabel = 'Close modal',
onDismiss = () => {},
children,
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -202,8 +236,8 @@ function Modal({
styles.backdrop,
{
backgroundColor: theme.colors?.backdrop,
opacity,
},
...(!disableAnimations ? [{ opacity }] : []),
]}
testID={`${testID}-backdrop`}
/>
Expand All @@ -219,7 +253,11 @@ function Modal({
<Surface
testID={`${testID}-surface`}
theme={theme}
style={[{ opacity }, styles.content, contentContainerStyle]}
style={[
...(!disableAnimations ? [{ opacity }] : []),
styles.content,
contentContainerStyle,
]}
container
>
{children}
Expand Down