Skip to content

Commit

Permalink
feat: add dismissableBackButton prop to Dialog (#3865)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukewalczak authored May 15, 2023
1 parent f36cdab commit 46d1201
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 4 deletions.
16 changes: 15 additions & 1 deletion example/src/Examples/DialogExample.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { StyleSheet } from 'react-native';
import { Platform, StyleSheet } from 'react-native';

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

import { useExampleTheme } from '..';
import ScreenWrapper from '../ScreenWrapper';
import {
DialogWithCustomColors,
DialogWithDismissableBackButton,
DialogWithIcon,
DialogWithLoadingIndicator,
DialogWithLongText,
Expand Down Expand Up @@ -73,6 +74,15 @@ const DialogExample = () => {
With icon
</Button>
)}
{Platform.OS === 'android' && (
<Button
mode="outlined"
onPress={_toggleDialog('dialog7')}
style={styles.button}
>
Dismissable back button
</Button>
)}
<DialogWithLongText
visible={_getVisible('dialog1')}
close={_toggleDialog('dialog1')}
Expand All @@ -99,6 +109,10 @@ const DialogExample = () => {
close={_toggleDialog('dialog6')}
/>
)}
<DialogWithDismissableBackButton
visible={_getVisible('dialog7')}
close={_toggleDialog('dialog7')}
/>
</ScreenWrapper>
);
};
Expand Down
38 changes: 38 additions & 0 deletions example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx
Original file line number Diff line number Diff line change
@@ -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;
}) => (
<Portal>
<Dialog
onDismiss={close}
visible={visible}
dismissable={false}
dismissableBackButton
>
<Dialog.Title>Alert</Dialog.Title>
<Dialog.Content>
<TextComponent>
This is an undismissable dialog, however you can use hardware back
button to close it!
</TextComponent>
</Dialog.Content>
<Dialog.Actions>
<Button color={MD2Colors.teal500} disabled>
Disagree
</Button>
<Button onPress={close}>Agree</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);

export default DialogWithDismissableBackButton;
1 change: 1 addition & 0 deletions example/src/Examples/Dialogs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
6 changes: 6 additions & 0 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -95,6 +99,7 @@ const DIALOG_ELEVATION: number = 24;
const Dialog = ({
children,
dismissable = true,
dismissableBackButton = dismissable,
onDismiss,
visible = false,
style,
Expand All @@ -116,6 +121,7 @@ const Dialog = ({
return (
<Modal
dismissable={dismissable}
dismissableBackButton={dismissableBackButton}
onDismiss={onDismiss}
visible={visible}
contentContainerStyle={[
Expand Down
9 changes: 7 additions & 2 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export type Props = {
* Determines whether clicking outside the modal dismiss it.
*/
dismissable?: boolean;
/**
* Determines whether clicking Android hardware back button dismiss dialog.
*/
dismissableBackButton?: boolean;
/**
* Callback that is called when the user dismisses the modal.
*/
Expand Down Expand Up @@ -102,6 +106,7 @@ const DEFAULT_DURATION = 220;
*/
function Modal({
dismissable = true,
dismissableBackButton = dismissable,
visible = false,
overlayAccessibilityLabel = 'Close modal',
onDismiss = () => {},
Expand Down Expand Up @@ -170,7 +175,7 @@ function Modal({
}

const onHardwareBackPress = () => {
if (dismissable) {
if (dismissable || dismissableBackButton) {
hideModal();
}

Expand All @@ -183,7 +188,7 @@ function Modal({
onHardwareBackPress
);
return () => subscription.remove();
}, [dismissable, hideModal, visible]);
}, [dismissable, dismissableBackButton, hideModal, visible]);

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

Expand Down
64 changes: 63 additions & 1 deletion src/components/__tests__/Dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(
Expand Down Expand Up @@ -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(
<Dialog visible onDismiss={onDismiss} dismissable={false} testID="dialog">
<Text>This is simple dialog</Text>
</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(
<Dialog
visible
onDismiss={onDismiss}
dismissable={false}
dismissableBackButton
testID="dialog"
>
<Text>This is simple dialog</Text>
</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(
<Dialog visible={true}>
Expand Down

0 comments on commit 46d1201

Please sign in to comment.