Skip to content

Commit 46d1201

Browse files
authored
feat: add dismissableBackButton prop to Dialog (#3865)
1 parent f36cdab commit 46d1201

File tree

6 files changed

+130
-4
lines changed

6 files changed

+130
-4
lines changed

example/src/Examples/DialogExample.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as React from 'react';
2-
import { StyleSheet } from 'react-native';
2+
import { Platform, StyleSheet } from 'react-native';
33

44
import { Button } from 'react-native-paper';
55

66
import { useExampleTheme } from '..';
77
import ScreenWrapper from '../ScreenWrapper';
88
import {
99
DialogWithCustomColors,
10+
DialogWithDismissableBackButton,
1011
DialogWithIcon,
1112
DialogWithLoadingIndicator,
1213
DialogWithLongText,
@@ -73,6 +74,15 @@ const DialogExample = () => {
7374
With icon
7475
</Button>
7576
)}
77+
{Platform.OS === 'android' && (
78+
<Button
79+
mode="outlined"
80+
onPress={_toggleDialog('dialog7')}
81+
style={styles.button}
82+
>
83+
Dismissable back button
84+
</Button>
85+
)}
7686
<DialogWithLongText
7787
visible={_getVisible('dialog1')}
7888
close={_toggleDialog('dialog1')}
@@ -99,6 +109,10 @@ const DialogExample = () => {
99109
close={_toggleDialog('dialog6')}
100110
/>
101111
)}
112+
<DialogWithDismissableBackButton
113+
visible={_getVisible('dialog7')}
114+
close={_toggleDialog('dialog7')}
115+
/>
102116
</ScreenWrapper>
103117
);
104118
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as React from 'react';
2+
3+
import { Button, Portal, Dialog, MD2Colors } from 'react-native-paper';
4+
5+
import { TextComponent } from './DialogTextComponent';
6+
7+
const DialogWithDismissableBackButton = ({
8+
visible,
9+
close,
10+
}: {
11+
visible: boolean;
12+
close: () => void;
13+
}) => (
14+
<Portal>
15+
<Dialog
16+
onDismiss={close}
17+
visible={visible}
18+
dismissable={false}
19+
dismissableBackButton
20+
>
21+
<Dialog.Title>Alert</Dialog.Title>
22+
<Dialog.Content>
23+
<TextComponent>
24+
This is an undismissable dialog, however you can use hardware back
25+
button to close it!
26+
</TextComponent>
27+
</Dialog.Content>
28+
<Dialog.Actions>
29+
<Button color={MD2Colors.teal500} disabled>
30+
Disagree
31+
</Button>
32+
<Button onPress={close}>Agree</Button>
33+
</Dialog.Actions>
34+
</Dialog>
35+
</Portal>
36+
);
37+
38+
export default DialogWithDismissableBackButton;

example/src/Examples/Dialogs/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { default as DialogWithLongText } from './DialogWithLongText';
44
export { default as DialogWithRadioBtns } from './DialogWithRadioBtns';
55
export { default as UndismissableDialog } from './UndismissableDialog';
66
export { default as DialogWithIcon } from './DialogWithIcon';
7+
export { default as DialogWithDismissableBackButton } from './DialogWithDismissableBackButton';

src/components/Dialog/Dialog.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export type Props = {
2222
* Determines whether clicking outside the dialog dismiss it.
2323
*/
2424
dismissable?: boolean;
25+
/**
26+
* Determines whether clicking Android hardware back button dismiss dialog.
27+
*/
28+
dismissableBackButton?: boolean;
2529
/**
2630
* Callback that is called when the user dismisses the dialog.
2731
*/
@@ -95,6 +99,7 @@ const DIALOG_ELEVATION: number = 24;
9599
const Dialog = ({
96100
children,
97101
dismissable = true,
102+
dismissableBackButton = dismissable,
98103
onDismiss,
99104
visible = false,
100105
style,
@@ -116,6 +121,7 @@ const Dialog = ({
116121
return (
117122
<Modal
118123
dismissable={dismissable}
124+
dismissableBackButton={dismissableBackButton}
119125
onDismiss={onDismiss}
120126
visible={visible}
121127
contentContainerStyle={[

src/components/Modal.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export type Props = {
2424
* Determines whether clicking outside the modal dismiss it.
2525
*/
2626
dismissable?: boolean;
27+
/**
28+
* Determines whether clicking Android hardware back button dismiss dialog.
29+
*/
30+
dismissableBackButton?: boolean;
2731
/**
2832
* Callback that is called when the user dismisses the modal.
2933
*/
@@ -102,6 +106,7 @@ const DEFAULT_DURATION = 220;
102106
*/
103107
function Modal({
104108
dismissable = true,
109+
dismissableBackButton = dismissable,
105110
visible = false,
106111
overlayAccessibilityLabel = 'Close modal',
107112
onDismiss = () => {},
@@ -170,7 +175,7 @@ function Modal({
170175
}
171176

172177
const onHardwareBackPress = () => {
173-
if (dismissable) {
178+
if (dismissable || dismissableBackButton) {
174179
hideModal();
175180
}
176181

@@ -183,7 +188,7 @@ function Modal({
183188
onHardwareBackPress
184189
);
185190
return () => subscription.remove();
186-
}, [dismissable, hideModal, visible]);
191+
}, [dismissable, dismissableBackButton, hideModal, visible]);
187192

188193
const prevVisible = React.useRef<boolean | null>(null);
189194

src/components/__tests__/Dialog.test.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React from 'react';
2-
import { Text, StyleSheet } from 'react-native';
2+
import {
3+
Text,
4+
StyleSheet,
5+
Platform,
6+
BackHandler as RNBackHandler,
7+
BackHandlerStatic as RNBackHandlerStatic,
8+
} from 'react-native';
39

410
import { act, fireEvent, render } from '@testing-library/react-native';
511

@@ -10,6 +16,17 @@ jest.mock('react-native-safe-area-context', () => ({
1016
useSafeAreaInsets: () => ({ bottom: 44, left: 0, right: 0, top: 37 }),
1117
}));
1218

19+
jest.mock('react-native/Libraries/Utilities/BackHandler', () =>
20+
// eslint-disable-next-line jest/no-mocks-import
21+
require('react-native/Libraries/Utilities/__mocks__/BackHandler')
22+
);
23+
24+
interface BackHandlerStatic extends RNBackHandlerStatic {
25+
mockPressBack(): void;
26+
}
27+
28+
const BackHandler = RNBackHandler as BackHandlerStatic;
29+
1330
describe('Dialog', () => {
1431
it('should render passed children', () => {
1532
const { getByTestId } = render(
@@ -37,6 +54,51 @@ describe('Dialog', () => {
3754
expect(onDismiss).toHaveBeenCalledTimes(1);
3855
});
3956

57+
it('should not call onDismiss when dismissable is false', () => {
58+
const onDismiss = jest.fn();
59+
const { getByTestId } = render(
60+
<Dialog visible onDismiss={onDismiss} dismissable={false} testID="dialog">
61+
<Text>This is simple dialog</Text>
62+
</Dialog>
63+
);
64+
65+
fireEvent.press(getByTestId('dialog-backdrop'));
66+
67+
act(() => {
68+
jest.runAllTimers();
69+
});
70+
expect(onDismiss).toHaveBeenCalledTimes(0);
71+
});
72+
73+
it('should call onDismiss on Android back button when dismissable is false but dismissableBackButton is true', () => {
74+
Platform.OS = 'android';
75+
const onDismiss = jest.fn();
76+
const { getByTestId } = render(
77+
<Dialog
78+
visible
79+
onDismiss={onDismiss}
80+
dismissable={false}
81+
dismissableBackButton
82+
testID="dialog"
83+
>
84+
<Text>This is simple dialog</Text>
85+
</Dialog>
86+
);
87+
88+
fireEvent.press(getByTestId('dialog-backdrop'));
89+
90+
act(() => {
91+
jest.runAllTimers();
92+
});
93+
expect(onDismiss).toHaveBeenCalledTimes(0);
94+
95+
act(() => {
96+
BackHandler.mockPressBack();
97+
jest.runAllTimers();
98+
});
99+
expect(onDismiss).toHaveBeenCalledTimes(1);
100+
});
101+
40102
it('should apply top margin to the first child if the dialog is V3', () => {
41103
const { getByTestId } = render(
42104
<Dialog visible={true}>

0 commit comments

Comments
 (0)