diff --git a/example/src/Examples/DialogExample.tsx b/example/src/Examples/DialogExample.tsx index 3c572d5239..7a929c0ade 100644 --- a/example/src/Examples/DialogExample.tsx +++ b/example/src/Examples/DialogExample.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet } from 'react-native'; +import { Platform, StyleSheet } from 'react-native'; import { Button } from 'react-native-paper'; @@ -7,6 +7,7 @@ import { useExampleTheme } from '..'; import ScreenWrapper from '../ScreenWrapper'; import { DialogWithCustomColors, + DialogWithDismissableBackButton, DialogWithIcon, DialogWithLoadingIndicator, DialogWithLongText, @@ -73,6 +74,15 @@ const DialogExample = () => { With icon )} + {Platform.OS === 'android' && ( + + )} { close={_toggleDialog('dialog6')} /> )} + ); }; diff --git a/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx b/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx new file mode 100644 index 0000000000..32512dc91d --- /dev/null +++ b/example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +import { Button, Portal, Dialog, MD2Colors } from 'react-native-paper'; + +import { TextComponent } from './DialogTextComponent'; + +const DialogWithDismissableBackButton = ({ + visible, + close, +}: { + visible: boolean; + close: () => void; +}) => ( + + + Alert + + + This is an undismissable dialog, however you can use hardware back + button to close it! + + + + + + + + +); + +export default DialogWithDismissableBackButton; diff --git a/example/src/Examples/Dialogs/index.tsx b/example/src/Examples/Dialogs/index.tsx index c7df1b308c..7af735d036 100644 --- a/example/src/Examples/Dialogs/index.tsx +++ b/example/src/Examples/Dialogs/index.tsx @@ -4,3 +4,4 @@ export { default as DialogWithLongText } from './DialogWithLongText'; export { default as DialogWithRadioBtns } from './DialogWithRadioBtns'; export { default as UndismissableDialog } from './UndismissableDialog'; export { default as DialogWithIcon } from './DialogWithIcon'; +export { default as DialogWithDismissableBackButton } from './DialogWithDismissableBackButton'; diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index 8c2680a4c0..fd5baada08 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -22,6 +22,10 @@ export type Props = { * Determines whether clicking outside the dialog dismiss it. */ dismissable?: boolean; + /** + * Determines whether clicking Android hardware back button dismiss dialog. + */ + dismissableBackButton?: boolean; /** * Callback that is called when the user dismisses the dialog. */ @@ -95,6 +99,7 @@ const DIALOG_ELEVATION: number = 24; const Dialog = ({ children, dismissable = true, + dismissableBackButton = dismissable, onDismiss, visible = false, style, @@ -116,6 +121,7 @@ const Dialog = ({ return ( {}, @@ -170,7 +175,7 @@ function Modal({ } const onHardwareBackPress = () => { - if (dismissable) { + if (dismissable || dismissableBackButton) { hideModal(); } @@ -183,7 +188,7 @@ function Modal({ onHardwareBackPress ); return () => subscription.remove(); - }, [dismissable, hideModal, visible]); + }, [dismissable, dismissableBackButton, hideModal, visible]); const prevVisible = React.useRef(null); diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index 7d049caa01..eef2dd9fea 100644 --- a/src/components/__tests__/Dialog.test.tsx +++ b/src/components/__tests__/Dialog.test.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { Text, StyleSheet } from 'react-native'; +import { + Text, + StyleSheet, + Platform, + BackHandler as RNBackHandler, + BackHandlerStatic as RNBackHandlerStatic, +} from 'react-native'; import { act, fireEvent, render } from '@testing-library/react-native'; @@ -10,6 +16,17 @@ jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ bottom: 44, left: 0, right: 0, top: 37 }), })); +jest.mock('react-native/Libraries/Utilities/BackHandler', () => + // eslint-disable-next-line jest/no-mocks-import + require('react-native/Libraries/Utilities/__mocks__/BackHandler') +); + +interface BackHandlerStatic extends RNBackHandlerStatic { + mockPressBack(): void; +} + +const BackHandler = RNBackHandler as BackHandlerStatic; + describe('Dialog', () => { it('should render passed children', () => { const { getByTestId } = render( @@ -37,6 +54,51 @@ describe('Dialog', () => { expect(onDismiss).toHaveBeenCalledTimes(1); }); + it('should not call onDismiss when dismissable is false', () => { + const onDismiss = jest.fn(); + const { getByTestId } = render( + + This is simple dialog + + ); + + fireEvent.press(getByTestId('dialog-backdrop')); + + act(() => { + jest.runAllTimers(); + }); + expect(onDismiss).toHaveBeenCalledTimes(0); + }); + + it('should call onDismiss on Android back button when dismissable is false but dismissableBackButton is true', () => { + Platform.OS = 'android'; + const onDismiss = jest.fn(); + const { getByTestId } = render( + + This is simple dialog + + ); + + fireEvent.press(getByTestId('dialog-backdrop')); + + act(() => { + jest.runAllTimers(); + }); + expect(onDismiss).toHaveBeenCalledTimes(0); + + act(() => { + BackHandler.mockPressBack(); + jest.runAllTimers(); + }); + expect(onDismiss).toHaveBeenCalledTimes(1); + }); + it('should apply top margin to the first child if the dialog is V3', () => { const { getByTestId } = render(