Skip to content
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
20 changes: 18 additions & 2 deletions docs/6.x/docs/guides/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,23 @@ The `overlayAccessibilityLabel` prop was renamed to `dismissAccessibilityLabel`,
</Modal>
```

Previously, the Android back button dismissed the modal when `dismissable` was `true`, even if `dismissableBackButton` was `false`. The `dismissableBackButton` prop can now prevent the modal from being dismissed via the back button independently of the `dismissable` prop.
`dismissable` controls the Android back button, the Escape key, the screen reader's escape gesture, and the dismiss button described above. An outside tap is controlled by the new `dismissableOverlay` prop, which applies only when the modal is `dismissable`. The `dismissableBackButton` prop was removed. Setting `dismissable={false}` blocks every way of dismissing the modal.

To ignore an outside tap and still allow the back button, the Escape key, and the screen reader's escape gesture to close the modal, use `dismissableOverlay={false}`:

```diff
<Modal
visible={visible}
onDismiss={hideModal}
- dismissable={false}
- dismissableBackButton
+ dismissableOverlay={false}
>
<Text>Content</Text>
</Modal>
```

`Dialog` uses the same `dismissable` and `dismissableOverlay` props.

### Dialog

Expand All @@ -246,7 +262,7 @@ The dialog now has the `dialog` role. On web, the dialog's accessible name is se
</Dialog>
```

When the dialog is `dismissable`, screen reader users can dismiss it with a visually hidden button inside the dialog. You can change the button's accessibility label with the new `dismissAccessibilityLabel` prop.
When the dialog is `dismissable`, screen reader users can dismiss it with a visually hidden button inside the dialog. You can change the button's accessibility label with the new `dismissAccessibilityLabel` prop. `dismissable` and `dismissableOverlay` behave the same way as on `Modal`, and `dismissableBackButton` was removed.

- The default elevation changed from level `1` to level `3`.
- The `style` prop no longer configures the background color or border radius. You can override `theme.colors.surfaceContainerHigh` and `theme.shapes.corner.extraLarge` using the `theme` prop instead.
Expand Down
36 changes: 10 additions & 26 deletions example/src/Examples/DialogExample.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import { StyleSheet } from 'react-native';

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

import {
DialogWithCustomColors,
DialogWithDismissableBackButton,
DialogWithIcon,
DialogWithLoadingIndicator,
DialogWithLongText,
DialogWithRadioBtns,
DialogWithUndismissableBackButton,
DialogWithUndismissableOverlay,
UndismissableDialog,
} from './Dialogs';
import ScreenWrapper from '../ScreenWrapper';
Expand Down Expand Up @@ -71,24 +70,13 @@ const DialogExample = () => {
>
With icon
</Button>
{Platform.OS === 'android' && (
<Button
mode="outlined"
onPress={_toggleDialog('dialog7')}
style={styles.button}
>
Dismissable back button
</Button>
)}
{Platform.OS === 'android' && (
<Button
mode="outlined"
onPress={_toggleDialog('dialog8')}
style={styles.button}
>
Undismissable back button
</Button>
)}
<Button
mode="outlined"
onPress={_toggleDialog('dialog7')}
style={styles.button}
>
Undismissable overlay
</Button>
<DialogWithLongText
visible={_getVisible('dialog1')}
close={_toggleDialog('dialog1')}
Expand All @@ -113,14 +101,10 @@ const DialogExample = () => {
visible={_getVisible('dialog6')}
close={_toggleDialog('dialog6')}
/>
<DialogWithDismissableBackButton
<DialogWithUndismissableOverlay
visible={_getVisible('dialog7')}
close={_toggleDialog('dialog7')}
/>
<DialogWithUndismissableBackButton
visible={_getVisible('dialog8')}
close={_toggleDialog('dialog8')}
/>
</ScreenWrapper>
);
};
Expand Down
34 changes: 0 additions & 34 deletions example/src/Examples/Dialogs/DialogWithDismissableBackButton.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { Button, Dialog, Palette } from 'react-native-paper';

import { TextComponent } from './DialogTextComponent';

const DialogWithUndismissableBackButton = ({
const DialogWithUndismissableOverlay = ({
visible,
close,
}: {
visible: boolean;
close: () => void;
}) => (
<Dialog onDismiss={close} visible={visible} dismissableBackButton={false}>
<Dialog onDismiss={close} visible={visible} dismissableOverlay={false}>
<Dialog.Title>Alert</Dialog.Title>
<Dialog.Content>
<TextComponent>
This dialog can be dismissed by tapping outside, however the hardware
back button will not close it!
Tapping outside will not close this dialog, however you can still close
it with the back button or the Escape key!
</TextComponent>
</Dialog.Content>
<Dialog.Actions>
Expand All @@ -26,4 +26,4 @@ const DialogWithUndismissableBackButton = ({
</Dialog>
);

export default DialogWithUndismissableBackButton;
export default DialogWithUndismissableOverlay;
3 changes: 1 addition & 2 deletions example/src/Examples/Dialogs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +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';
export { default as DialogWithUndismissableBackButton } from './DialogWithUndismissableBackButton';
export { default as DialogWithUndismissableOverlay } from './DialogWithUndismissableOverlay';
12 changes: 7 additions & 5 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import type { DialogChildProps } from './utils';

export type Props = {
/**
* Determines whether clicking outside the dialog dismiss it.
* Determines whether the user can dismiss the dialog with the Android back button,
* the Escape key, or the screen reader's escape gesture.
*/
dismissable?: boolean;
/**
* Determines whether clicking Android hardware back button dismiss dialog.
* Determines whether clicking outside the dialog dismisses it.
* Applies only when the dialog is `dismissable`.
*/
dismissableBackButton?: boolean;
dismissableOverlay?: boolean;
/**
* Callback that is called when the user dismisses the dialog.
*/
Expand Down Expand Up @@ -102,7 +104,7 @@ const DIALOG_ELEVATION: Elevation = 3;
const Dialog = ({
children,
dismissable = true,
dismissableBackButton = dismissable,
dismissableOverlay = true,
onDismiss,
dismissAccessibilityLabel,
'aria-label': ariaLabel,
Expand All @@ -126,7 +128,7 @@ const Dialog = ({
aria-label={ariaLabel}
aria-labelledby={ariaLabel == null ? titleId : undefined}
dismissable={dismissable}
dismissableBackButton={dismissableBackButton}
dismissableOverlay={dismissableOverlay}
onDismiss={onDismiss}
dismissAccessibilityLabel={dismissAccessibilityLabel}
visible={visible}
Expand Down
48 changes: 16 additions & 32 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ import type { Props as SurfaceProps, SurfaceStyle } from './Surface';
import { useInternalTheme } from '../core/theming';
import { tokens } from '../theme/tokens';
import type { Elevation, ThemeProp } from '../theme/types';
import { addEventListener } from '../utils/addEventListener';
import { BackHandler } from '../utils/BackHandler/BackHandler';

const scrimAlpha = tokens.md.sys.scrim.alpha;

export type Props = {
/**
* Determines whether clicking outside the modal dismisses it.
* Determines whether the user can dismiss the modal with the Android back button,
* the Escape key, or the screen reader's escape gesture.
*/
dismissable?: boolean;
/**
* Determines whether clicking Android hardware back button dismisses the dialog.
* Determines whether clicking outside the modal dismisses it.
* Applies only when the modal is `dismissable`.
*/
dismissableBackButton?: boolean;
dismissableOverlay?: boolean;
/**
* Callback that is called when the user dismisses the modal.
*/
Expand Down Expand Up @@ -136,7 +136,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
*/
function Modal({
dismissable = true,
dismissableBackButton = dismissable,
dismissableOverlay = true,
visible = false,
dismissAccessibilityLabel = 'Close modal',
'aria-label': ariaLabel,
Expand Down Expand Up @@ -186,28 +186,6 @@ function Modal({
return () => clearTimeout(timeout);
}, [scale, visible, visibleInternal]);

React.useEffect(() => {
if (!visible) {
return undefined;
}

const onHardwareBackPress = () => {
if (dismissableBackButton) {
onDismissCallback();
}

return true;
};

const subscription = addEventListener(
BackHandler,
'hardwareBackPress',
onHardwareBackPress
);

return () => subscription.remove();
}, [dismissableBackButton, onDismissCallback, visible]);

const transitionTimingFunction = cubicBezier(1 / 3, 1, 2 / 3, 1);

const backdropTransitionStyle: AnimatedStyle<ViewStyle> = {
Expand All @@ -234,21 +212,27 @@ function Modal({
return null;
}

const overlayDismisses = dismissable && dismissableOverlay;

return (
<Portal modal={visibleInternal} theme={themeOverrides}>
<Portal
modal={visible}
onDismiss={onDismissCallback}
dismissable={dismissable}
theme={themeOverrides}
>
<Animated.View
pointerEvents={visible ? 'auto' : 'none'}
aria-live="polite"
style={StyleSheet.absoluteFill}
onAccessibilityEscape={dismissable ? onDismissCallback : undefined}
testID={testID}
>
<AnimatedPressable
aria-hidden
accessible={false}
tabIndex={-1}
disabled={!dismissable}
onPress={dismissable ? onDismissCallback : undefined}
disabled={!overlayDismisses}
onPress={overlayDismisses ? onDismissCallback : undefined}
style={[styles.backdrop, backdropStyle, backdropTransitionStyle]}
testID={overlayTestID}
/>
Expand Down
31 changes: 28 additions & 3 deletions src/components/Portal/Portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ export type Props = {
*/
children: React.ReactNode;
/**
* Whether the portal hides items below it from screen readers and focus order.
* Whether the portal hides items below it from screen readers and focus order,
* and listens for the Android back button, the Escape key on web
* and the screen reader's escape gesture.
*
* Ensure it's set to true only when the modal is open.
*/
modal?: boolean;
/**
* Callback that is called when the user asks to close a `modal` portal.
* Only the topmost `modal` portal in a `Portal.Host` can be dismissed.
*/
onDismiss?: () => void;
/**
* Whether the user can dismiss a `modal` portal.
*/
dismissable?: boolean;
/**
* @optional
*/
Expand All @@ -34,6 +45,9 @@ export type Props = {
* It requires a [`Portal.Host`](PortalHost) component to be rendered somewhere in the parent tree.
* Note that if you're using the `Provider` component, this already includes a `Portal.Host`.
*
* A topmost `modal` portal can be closed with the Android back button, the Escape key on web
* and the screen reader's escape gesture through `onDismiss`.
*
* ## Usage
* ```js
* import * as React from 'react';
Expand All @@ -48,15 +62,26 @@ export type Props = {
* export default MyComponent;
* ```
*/
const Portal = ({ children, modal, theme: themeOverrides }: Props) => {
const Portal = ({
children,
modal,
onDismiss,
dismissable = true,
theme: themeOverrides,
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();
const settings = React.useContext(SettingsContext);
const manager = React.useContext(PortalContext);
const reduceMotion = React.useContext(ReduceMotionContext);

return (
<PortalConsumer manager={manager} modal={modal}>
<PortalConsumer
manager={manager}
modal={modal}
onDismiss={onDismiss}
dismissable={dismissable}
>
<SettingsProvider value={settings}>
<ReduceMotionContext.Provider value={reduceMotion}>
<LocaleProvider direction={direction}>
Expand Down
Loading
Loading