diff --git a/example/src/Examples/DialogExample.tsx b/example/src/Examples/DialogExample.tsx
index d926ae6616..b12cbb5c84 100644
--- a/example/src/Examples/DialogExample.tsx
+++ b/example/src/Examples/DialogExample.tsx
@@ -10,6 +10,7 @@ import {
DialogWithLoadingIndicator,
DialogWithLongText,
DialogWithRadioBtns,
+ DialogWithUndismissableBackButton,
UndismissableDialog,
} from './Dialogs';
import ScreenWrapper from '../ScreenWrapper';
@@ -79,6 +80,15 @@ const DialogExample = () => {
Dismissable back button
)}
+ {Platform.OS === 'android' && (
+
+ )}
{
visible={_getVisible('dialog7')}
close={_toggleDialog('dialog7')}
/>
+
);
};
diff --git a/example/src/Examples/Dialogs/DialogWithUndismissableBackButton.tsx b/example/src/Examples/Dialogs/DialogWithUndismissableBackButton.tsx
new file mode 100644
index 0000000000..434b4ad154
--- /dev/null
+++ b/example/src/Examples/Dialogs/DialogWithUndismissableBackButton.tsx
@@ -0,0 +1,31 @@
+import { Button, Portal, Dialog, Palette } from 'react-native-paper';
+
+import { TextComponent } from './DialogTextComponent';
+
+const DialogWithUndismissableBackButton = ({
+ visible,
+ close,
+}: {
+ visible: boolean;
+ close: () => void;
+}) => (
+
+
+
+);
+
+export default DialogWithUndismissableBackButton;
diff --git a/example/src/Examples/Dialogs/index.tsx b/example/src/Examples/Dialogs/index.tsx
index 7af735d036..5ae48db11f 100644
--- a/example/src/Examples/Dialogs/index.tsx
+++ b/example/src/Examples/Dialogs/index.tsx
@@ -5,3 +5,4 @@ export { default as DialogWithRadioBtns } from './DialogWithRadioBtns';
export { default as UndismissableDialog } from './UndismissableDialog';
export { default as DialogWithIcon } from './DialogWithIcon';
export { default as DialogWithDismissableBackButton } from './DialogWithDismissableBackButton';
+export { default as DialogWithUndismissableBackButton } from './DialogWithUndismissableBackButton';
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index fbc1197630..f24f0e986b 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -36,6 +36,10 @@ export type Props = {
* Accessibility label for the overlay. This is read by the screen reader when the user taps outside the modal.
*/
overlayAccessibilityLabel?: string;
+ /**
+ * Accessible name for the modal.
+ */
+ 'aria-label'?: string;
/**
* testID for the overlay that is displayed behind the modal content.
*/
@@ -89,7 +93,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
/**
* The Modal component is a simple way to present content above an enclosing view.
* To render the `Modal` above other components, you'll need to wrap it with the [`Portal`](./Portal) component.
- * Note that this modal is NOT accessible by default; if you need an accessible modal, please use the React Native Modal.
+ * Give the modal an accessible name with `aria-label`.
*
* ## Usage
* ```js
@@ -110,6 +114,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
*
@@ -131,6 +136,7 @@ function Modal({
dismissableBackButton = dismissable,
visible = false,
overlayAccessibilityLabel = 'Close modal',
+ 'aria-label': ariaLabel,
overlayTestID,
onDismiss = () => {},
children,
@@ -182,7 +188,7 @@ function Modal({
}
const onHardwareBackPress = () => {
- if (dismissable || dismissableBackButton) {
+ if (dismissableBackButton) {
onDismissCallback();
}
@@ -196,7 +202,7 @@ function Modal({
);
return () => subscription.remove();
- }, [dismissable, dismissableBackButton, onDismissCallback, visible]);
+ }, [dismissableBackButton, onDismissCallback, visible]);
const transitionTimingFunction = cubicBezier(1 / 3, 1, 2 / 3, 1);
@@ -227,10 +233,9 @@ function Modal({
return (
{
expect(toJSON()).toBeNull();
});
+ describe('if closed via the accessibility escape gesture', () => {
+ it('should invoke the onDismiss function', async () => {
+ const onDismiss = jest.fn();
+
+ await render(
+
+ {null}
+
+ );
+
+ await fireEvent(screen.getByTestId('modal'), 'accessibilityEscape');
+
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('if closed via Android back button', () => {
it('invokes onDismiss', async () => {
const onDismiss = jest.fn();
@@ -219,6 +235,37 @@ describe('Modal', () => {
expect(toJSON()).toMatchSnapshot();
});
+ it('should not invoke onDismiss on back press when only dismissableBackButton is false', async () => {
+ const onDismiss = jest.fn();
+
+ await render(
+
+ {null}
+
+ );
+
+ await act(() => {
+ BackHandler.mockPressBack();
+ jest.runAllTimers();
+ });
+
+ expect(onDismiss).not.toHaveBeenCalled();
+
+ // Pressing outside still dismisses it -- only the back button is opted out.
+ await userEvent.press(screen.getByLabelText('Close modal'));
+
+ await act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
it('should not invoke onDismiss', async () => {
const onDismiss = jest.fn();
await render(
@@ -246,6 +293,28 @@ describe('Modal', () => {
});
});
+ describe('if closed via the accessibility escape gesture', () => {
+ it('should keep the modal on screen', async () => {
+ const onDismiss = jest.fn();
+
+ await render(
+
+ {null}
+
+ );
+
+ await fireEvent(screen.getByTestId('modal'), 'accessibilityEscape');
+
+ expect(onDismiss).not.toHaveBeenCalled();
+ expect(screen.getByLabelText('Close modal')).toBeOnTheScreen();
+ });
+ });
+
describe('if closed via Android back button', () => {
it('will run the animation but not fade out', async () => {
const { toJSON } = await render(
@@ -523,3 +592,13 @@ describe('Modal', () => {
});
});
});
+
+it('exposes the modal as a dialog with an accessible name', async () => {
+ await render(
+ {}} aria-label="Example modal">
+ Modal content
+
+ );
+
+ expect(screen.getByLabelText('Example modal')).toHaveProp('role', 'dialog');
+});
diff --git a/src/components/__tests__/__snapshots__/Modal.test.tsx.snap b/src/components/__tests__/__snapshots__/Modal.test.tsx.snap
index 5b36c6a27a..5ba0c2a9dc 100644
--- a/src/components/__tests__/__snapshots__/Modal.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Modal.test.tsx.snap
@@ -3,7 +3,6 @@
exports[`Modal by default should receive appropriate top and bottom insets 1`] = `