Skip to content

Commit c00ea41

Browse files
committed
feat: make it a prop
1 parent 43e545b commit c00ea41

8 files changed

Lines changed: 66 additions & 7 deletions

File tree

packages/react-native/Libraries/Modal/Modal.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export interface ModalBaseProps {
3535
*/
3636
visible?: boolean | undefined;
3737
/**
38-
* The `onRequestClose` callback is called when the user taps the hardware back button on Android or the menu button on Apple TV.
38+
* 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.
3939
*
40-
* This is required on Apple TV and Android.
40+
* This is required on iOS and Android.
4141
*/
4242
onRequestClose?: ((event: NativeSyntheticEvent<any>) => void) | undefined;
4343
/**
@@ -89,6 +89,12 @@ export interface ModalPropsIOS {
8989
onOrientationChange?:
9090
| ((event: NativeSyntheticEvent<any>) => void)
9191
| undefined;
92+
93+
/**
94+
* Controls whether the modal can be dismissed by swiping down on iOS.
95+
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
96+
*/
97+
allowSwipeDismissal?: boolean | undefined;
9298
}
9399

94100
export interface ModalPropsAndroid {

packages/react-native/Libraries/Modal/Modal.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export type ModalBaseProps = {
8686
*/
8787
visible?: ?boolean,
8888
/**
89-
* The `onRequestClose` callback is called when the user taps the hardware back button on Android, dismisses the sheet using a gesture on iOS or the menu button on Apple TV.
89+
* 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.
9090
*
9191
* This is required on iOS and Android.
9292
*/
@@ -147,6 +147,12 @@ export type ModalPropsIOS = {
147147
// | ((event: NativeSyntheticEvent<any>) => void)
148148
// | undefined;
149149
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,
150+
151+
/**
152+
* Controls whether the modal can be dismissed by swiping down on iOS.
153+
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
154+
*/
155+
allowSwipeDismissal?: ?boolean,
150156
};
151157

152158
export type ModalPropsAndroid = {
@@ -193,9 +199,13 @@ function confirmProps(props: ModalProps) {
193199
);
194200
}
195201

196-
if (!props.onRequestClose) {
202+
if (
203+
Platform.OS === 'ios' &&
204+
props.allowSwipeDismissal === true &&
205+
!props.onRequestClose
206+
) {
197207
console.warn(
198-
'Modal requires the onRequestClose prop. This is necessary to prevent state corruption.',
208+
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
199209
);
200210
}
201211
}
@@ -333,6 +343,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
333343
onStartShouldSetResponder={this._shouldSetResponder}
334344
supportedOrientations={this.props.supportedOrientations}
335345
onOrientationChange={this.props.onOrientationChange}
346+
allowSwipeDismissal={this.props.allowSwipeDismissal}
336347
testID={this.props.testID}>
337348
<VirtualizedListContextResetter>
338349
<ScrollView.Context.Provider value={null}>

packages/react-native/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ - (RCTFabricModalHostViewController *)viewController
124124
_viewController = [RCTFabricModalHostViewController new];
125125
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
126126
_viewController.delegate = self;
127+
_viewController.modalInPresentation = YES;
127128
}
128129
return _viewController;
129130
}
@@ -239,6 +240,7 @@ - (void)prepareForRecycle
239240

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

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

252254
self.viewController.modalPresentationStyle = presentationConfiguration(newProps);
255+
256+
if (oldViewProps.allowSwipeDismissal != newProps.allowSwipeDismissal) {
257+
self.viewController.modalInPresentation = !newProps.allowSwipeDismissal;
258+
}
259+
253260

254261
_shouldPresent = newProps.visible;
255262
[self ensurePresentedOnlyIfNeeded];
@@ -285,7 +292,9 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
285292

286293
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
287294
auto eventEmitter = [self modalEventEmitter];
288-
if (eventEmitter) {
295+
const auto &props = static_cast<const ModalHostViewProps &>(*_props);
296+
297+
if (eventEmitter && props.allowSwipeDismissal) {
289298
eventEmitter->onRequestClose({});
290299
}
291300
}

packages/react-native/React/Views/RCTModalHostView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
@property (nonatomic, copy) RCTDirectEventBlock onShow;
2626
@property (nonatomic, assign) BOOL visible;
27+
@property (nonatomic, assign) BOOL allowSwipeDismissal;
2728

2829
// Android only
2930
@property (nonatomic, assign) BOOL statusBarTranslucent;

packages/react-native/React/Views/RCTModalHostView.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
3535
if ((self = [super initWithFrame:CGRectZero])) {
3636
_bridge = bridge;
3737
_modalViewController = [RCTModalHostViewController new];
38+
_modalViewController.modalInPresentation = YES;
3839
UIView *containerView = [UIView new];
3940
containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
4041
_modalViewController.view = containerView;
@@ -50,6 +51,13 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
5051
return self;
5152
}
5253

54+
- (void)setAllowSwipeDismissal:(BOOL)allowSwipeDismissal {
55+
if (_allowSwipeDismissal != allowSwipeDismissal) {
56+
_allowSwipeDismissal = allowSwipeDismissal;
57+
_modalViewController.modalInPresentation = !allowSwipeDismissal;
58+
}
59+
}
60+
5361
- (void)notifyForBoundsChange:(CGRect)newBounds
5462
{
5563
if (_reactSubview && _isPresented) {
@@ -71,7 +79,7 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)co
7179
}
7280

7381
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
74-
if (_onRequestClose != nil) {
82+
if (_onRequestClose != nil && _allowSwipeDismissal) {
7583
_onRequestClose(nil);
7684
}
7785
}

packages/react-native/React/Views/RCTModalHostViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ - (void)invalidate
119119
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
120120
RCT_EXPORT_VIEW_PROPERTY(visible, BOOL)
121121
RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock)
122+
RCT_EXPORT_VIEW_PROPERTY(allowSwipeDismissal, BOOL)
122123

123124
// Fabric only
124125
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)

packages/react-native/src/private/specs_DEPRECATED/components/RCTModalHostViewNativeComponent.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ type NativeProps = $ReadOnly<{
112112
*/
113113
animated?: WithDefault<boolean, false>,
114114

115+
/**
116+
* Controls whether the modal can be dismissed by swiping down on iOS.
117+
* This requires you to implement the `onRequestClose` prop to handle the dismissal.
118+
*/
119+
allowSwipeDismissal?: WithDefault<boolean, false>,
120+
115121
/**
116122
* The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations.
117123
*

packages/rn-tester/js/examples/Modal/ModalPresentation.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function ModalPresentation() {
6262
ios: 'fullScreen',
6363
default: undefined,
6464
}),
65+
allowSwipeDismissal: false,
6566
supportedOrientations: Platform.select({
6667
ios: ['portrait'],
6768
default: undefined,
@@ -75,6 +76,7 @@ function ModalPresentation() {
7576
const hardwareAccelerated = props.hardwareAccelerated;
7677
const statusBarTranslucent = props.statusBarTranslucent;
7778
const navigationBarTranslucent = props.navigationBarTranslucent;
79+
const allowSwipeDismissal = props.allowSwipeDismissal;
7880
const backdropColor = props.backdropColor;
7981
const backgroundColor = useContext(RNTesterThemeContext).BackgroundColor;
8082

@@ -132,6 +134,21 @@ function ModalPresentation() {
132134
}
133135
/>
134136
</View>
137+
138+
<View style={styles.inlineBlock}>
139+
<RNTesterText style={styles.title}>
140+
Allow Swipe Dismissal ⚫️
141+
</RNTesterText>
142+
<Switch
143+
value={allowSwipeDismissal}
144+
onValueChange={enabled =>
145+
setProps(prev => ({
146+
...prev,
147+
allowSwipeDismissal: enabled,
148+
}))
149+
}
150+
/>
151+
</View>
135152
<View style={styles.block}>
136153
<RNTesterText style={styles.title}>Presentation Style ⚫️</RNTesterText>
137154
<View style={styles.row}>

0 commit comments

Comments
 (0)