Skip to content
Closed
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
10 changes: 8 additions & 2 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export interface ModalBaseProps {
*/
visible?: boolean | undefined;
/**
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
* The `onRequestClose` callback is called when the user taps the hardware back button on Android, dismisses the sheet using a gesture on iOS (when `allowSwipeDismissal` is set to true) or the menu button on Apple TV.
*
* This is required on Apple TV and Android.
* This is required on iOS and Android.
*/
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
/**
Expand Down Expand Up @@ -89,6 +89,12 @@ export interface ModalPropsIOS {
onOrientationChange?:
| ((event: NativeSyntheticEvent<any>) => void)
| undefined;

/**
* Controls whether the modal can be dismissed by swiping down on iOS.
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
*/
allowSwipeDismissal?: boolean | undefined;
}

export interface ModalPropsAndroid {
Expand Down
21 changes: 19 additions & 2 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ export type ModalBaseProps = {
*/
visible?: ?boolean,
/**
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
* The `onRequestClose` callback is called when the user taps the hardware back button on Android, dismisses the sheet using a gesture on iOS (when `allowSwipeDismissal` is set to true) or the menu button on Apple TV.
*
* This is required on Apple TV and Android.
* This is required on iOS and Android.
*/
// onRequestClose?: (event: NativeSyntheticEvent<any>) => void;
onRequestClose?: ?DirectEventHandler<null>,
Expand Down Expand Up @@ -147,6 +147,12 @@ export type ModalPropsIOS = {
// | ((event: NativeSyntheticEvent<any>) => void)
// | undefined;
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,

/**
* Controls whether the modal can be dismissed by swiping down on iOS.
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
*/
allowSwipeDismissal?: ?boolean,
};

export type ModalPropsAndroid = {
Expand Down Expand Up @@ -192,6 +198,16 @@ function confirmProps(props: ModalProps) {
'Modal with translucent navigation bar and without translucent status bar is not supported.',
);
}

if (
Platform.OS === 'ios' &&
props.allowSwipeDismissal === true &&
!props.onRequestClose
) {
console.warn(
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
);
}
}
}

Expand Down Expand Up @@ -327,6 +343,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
onStartShouldSetResponder={this._shouldSetResponder}
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}
allowSwipeDismissal={this.props.allowSwipeDismissal}
testID={this.props.testID}>
<VirtualizedListContextResetter>
<ScrollView.Context.Provider value={null}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ - (instancetype)init
}
_touchHandler = [RCTSurfaceTouchHandler new];

self.modalInPresentation = YES;

return self;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ - (RCTFabricModalHostViewController *)viewController
_viewController = [RCTFabricModalHostViewController new];
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
_viewController.delegate = self;
_viewController.modalInPresentation = YES;
}
return _viewController;
}
Expand Down Expand Up @@ -239,6 +240,7 @@ - (void)prepareForRecycle

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldViewProps = static_cast<const ModalHostViewProps &>(*_props);
const auto &newProps = static_cast<const ModalHostViewProps &>(*props);

#if !TARGET_OS_TV
Expand All @@ -250,6 +252,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
self.viewController.modalTransitionStyle = transitionStyle;

self.viewController.modalPresentationStyle = presentationConfiguration(newProps);

if (oldViewProps.allowSwipeDismissal != newProps.allowSwipeDismissal) {
self.viewController.modalInPresentation = !newProps.allowSwipeDismissal;
}


_shouldPresent = newProps.visible;
[self ensurePresentedOnlyIfNeeded];
Expand Down Expand Up @@ -283,6 +290,15 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
}
}

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
auto eventEmitter = [self modalEventEmitter];
const auto &props = static_cast<const ModalHostViewProps &>(*_props);

if (eventEmitter && props.allowSwipeDismissal) {
eventEmitter->onRequestClose({});
}
}

@end

#ifdef __cplusplus
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTModalHostView.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

@property (nonatomic, copy) RCTDirectEventBlock onShow;
@property (nonatomic, assign) BOOL visible;
@property (nonatomic, assign) BOOL allowSwipeDismissal;

// Android only
@property (nonatomic, assign) BOOL statusBarTranslucent;
Expand Down
14 changes: 14 additions & 0 deletions packages/react-native/React/Views/RCTModalHostView.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
if ((self = [super initWithFrame:CGRectZero])) {
_bridge = bridge;
_modalViewController = [RCTModalHostViewController new];
_modalViewController.modalInPresentation = YES;
UIView *containerView = [UIView new];
containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_modalViewController.view = containerView;
Expand All @@ -50,6 +51,13 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
return self;
}

- (void)setAllowSwipeDismissal:(BOOL)allowSwipeDismissal {
if (_allowSwipeDismissal != allowSwipeDismissal) {
_allowSwipeDismissal = allowSwipeDismissal;
_modalViewController.modalInPresentation = !allowSwipeDismissal;
}
}

- (void)notifyForBoundsChange:(CGRect)newBounds
{
if (_reactSubview && _isPresented) {
Expand All @@ -70,6 +78,12 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
}
}

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
if (_onRequestClose != nil && _allowSwipeDismissal) {
_onRequestClose(nil);
}
}

- (void)notifyForOrientationChange
{
if (!_onOrientationChange) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ - (instancetype)init
return nil;
}

self.modalInPresentation = YES;

_preferredStatusBarStyle = [RCTUIStatusBarManager() statusBarStyle];
_preferredStatusBarHidden = [RCTUIStatusBarManager() isStatusBarHidden];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ - (void)invalidate
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(visible, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(allowSwipeDismissal, BOOL)

// Fabric only
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ type NativeProps = $ReadOnly<{
*/
animated?: WithDefault<boolean, false>,

/**
* Controls whether the modal can be dismissed by swiping down on iOS.
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
*/
allowSwipeDismissal?: WithDefault<boolean, false>,

/**
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
*
Expand Down
18 changes: 18 additions & 0 deletions packages/rn-tester/js/examples/Modal/ModalPresentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function ModalPresentation() {

const onRequestClose = useCallback(() => {
console.log('onRequestClose');
setProps(prev => ({...prev, visible: false}));
}, []);

const [props, setProps] = useState<ModalProps>({
Expand All @@ -61,6 +62,7 @@ function ModalPresentation() {
ios: 'fullScreen',
default: undefined,
}),
allowSwipeDismissal: false,
supportedOrientations: Platform.select({
ios: ['portrait'],
default: undefined,
Expand All @@ -74,6 +76,7 @@ function ModalPresentation() {
const hardwareAccelerated = props.hardwareAccelerated;
const statusBarTranslucent = props.statusBarTranslucent;
const navigationBarTranslucent = props.navigationBarTranslucent;
const allowSwipeDismissal = props.allowSwipeDismissal;
const backdropColor = props.backdropColor;
const backgroundColor = useContext(RNTesterThemeContext).BackgroundColor;

Expand Down Expand Up @@ -131,6 +134,21 @@ function ModalPresentation() {
}
/>
</View>

<View style={styles.inlineBlock}>
<RNTesterText style={styles.title}>
Allow Swipe Dismissal ⚫️
</RNTesterText>
<Switch
value={allowSwipeDismissal}
onValueChange={enabled =>
setProps(prev => ({
...prev,
allowSwipeDismissal: enabled,
}))
}
/>
</View>
<View style={styles.block}>
<RNTesterText style={styles.title}>Presentation Style ⚫️</RNTesterText>
<View style={styles.row}>
Expand Down
Loading