diff --git a/Libraries/Modal/Modal.d.ts b/Libraries/Modal/Modal.d.ts index 4cc2df22367b..b09c25c02f72 100644 --- a/Libraries/Modal/Modal.d.ts +++ b/Libraries/Modal/Modal.d.ts @@ -56,6 +56,15 @@ export interface ModalPropsIOS { | 'overFullScreen' | undefined; + /** + * The `detents` determines the height for modal, + * based on the detents value. + * It can be a combination of `large`, `medium` or + * any number. + * for eg: ['140', 'medium', 'large'] + */ + detents?: Array; + /** * The `supportedOrientations` prop allows the modal to be rotated to any of the specified orientations. * On iOS, the modal is still restricted by what's specified in your app's Info.plist's UISupportedInterfaceOrientations field. diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index b41ec143d633..394ad9dd50f3 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -79,6 +79,14 @@ export type Props = $ReadOnly<{| | 'overFullScreen' ), + /** + * The `detents` determines the height for modal, + * based on the detents value. + * It can be a combination of `large`, `medium` or + * any number. + * for eg: ['140', 'medium', 'large'] + */ + detents?: ?$ReadOnlyArray, /** * The `transparent` prop determines whether your modal will fill the * entire view. @@ -246,6 +254,7 @@ class Modal extends React.Component { return ( , + /** + * The `detents` determines the height for modal, + * based on the detents value. + * It can be a combination of `large`, `medium` or + * any number. + * for eg: ['140', 'medium', 'large'] + */ + detents?: ?$ReadOnlyArray, /** * The `transparent` prop determines whether your modal will fill the * entire view. diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h index 149a9788e2c1..73e4a0996fe3 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTFabricModalHostViewController.h @@ -15,6 +15,7 @@ @property (nonatomic, weak) id delegate; +@property (nonatomic, copy) NSArray *detents; @property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; @end diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h index 75a134f7d010..019e91c62d1b 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.h @@ -11,7 +11,7 @@ /** * UIView class for root component. */ -@interface RCTModalHostViewComponentView : RCTViewComponentView +@interface RCTModalHostViewComponentView : RCTViewComponentView /** * Subclasses may override this method and present the modal on different view controller. diff --git a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm index 28c4bd227b7c..d4569a49efb3 100644 --- a/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm +++ b/React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm @@ -150,6 +150,22 @@ - (void)ensurePresentedOnlyIfNeeded BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window; if (shouldBePresented) { _isPresented = YES; + if (@available(iOS 13.0, *)) { + _viewController.presentationController.delegate = self; + } +// if (@available(iOS 15.0, *)) { +// UISheetPresentationController *sheetPresentationController = +// _viewController.sheetPresentationController; +// +// if (sheetPresentationController && self.detents != nil) { +// NSArray *detents = self.detents != nil +// ? [RCTConvert UISheetPresentationControllerDetentArray:self.detents] +// // The default value is an array that contains the value largeDetent +// : @[[UISheetPresentationControllerDetent largeDetent]]; +// sheetPresentationController.detents = detents; +// sheetPresentationController.prefersGrabberVisible = detents.count > 1; +// } +// } [self presentViewController:self.viewController animated:_shouldAnimatePresentation completion:^{ @@ -227,6 +243,14 @@ - (void)boundsDidChange:(CGRect)newBounds } } +- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)controller +{ + auto eventEmitter = [self modalEventEmitter]; + if (eventEmitter) { + eventEmitter->onRequestClose(ModalHostViewEventEmitter::OnRequestClose{}); + } +} + #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider @@ -257,6 +281,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & self.viewController.modalPresentationStyle = presentationConfiguration(newProps); + self.viewController.detents = @[]; + _shouldPresent = newProps.visible; [self ensurePresentedOnlyIfNeeded]; diff --git a/React/Views/RCTConvert+Modal.h b/React/Views/RCTConvert+Modal.h new file mode 100644 index 000000000000..ec9a12a3f1dc --- /dev/null +++ b/React/Views/RCTConvert+Modal.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@interface RCTConvert (Modal) + ++ (UISheetPresentationControllerDetent *)UISheetPresentationControllerDetent:(id)json API_AVAILABLE(ios(15.0)); ++ (NSArray *)UISheetPresentationControllerDetentArray:(id)json API_AVAILABLE(ios(15.0)); + +@end diff --git a/React/Views/RCTConvert+Modal.m b/React/Views/RCTConvert+Modal.m new file mode 100644 index 000000000000..6db134546172 --- /dev/null +++ b/React/Views/RCTConvert+Modal.m @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTConvert+Modal.h" + +@implementation RCTConvert (Modal) + ++ (UISheetPresentationControllerDetent *)UISheetPresentationControllerDetent:(id)json { + if (@available(iOS 16.0, *)) { + if ([json isKindOfClass:[NSNumber class]]) { + float value = [json floatValue]; + return [UISheetPresentationControllerDetent + customDetentWithIdentifier:nil + resolver:^CGFloat(id context) { + return value; + }]; + } + } + + if ([json isEqualToString:@"medium"]) { + return [UISheetPresentationControllerDetent mediumDetent]; + } + + return [UISheetPresentationControllerDetent largeDetent]; +} + +RCT_ARRAY_CONVERTER(UISheetPresentationControllerDetent) + +@end diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h index 2fcdcaea83f5..a9539431e3cf 100644 --- a/React/Views/RCTModalHostView.h +++ b/React/Views/RCTModalHostView.h @@ -20,6 +20,7 @@ @property (nonatomic, copy) NSString *animationType; @property (nonatomic, assign) UIModalPresentationStyle presentationStyle; +@property (nonatomic, assign) id detents; @property (nonatomic, assign, getter=isTransparent) BOOL transparent; @property (nonatomic, copy) RCTDirectEventBlock onShow; diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 65428a037d7a..b1150f445ac2 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -16,6 +16,7 @@ #import "RCTUIManager.h" #import "RCTUtils.h" #import "UIView+React.h" +#import "RCTConvert+Modal.h" @implementation RCTModalHostView { __weak RCTBridge *_bridge; @@ -185,6 +186,19 @@ - (void)ensurePresentedOnlyIfNeeded if (@available(iOS 13.0, *)) { _modalViewController.presentationController.delegate = self; } + if (@available(iOS 15.0, *)) { + UISheetPresentationController *sheetPresentationController = + _modalViewController.sheetPresentationController; + + if (sheetPresentationController && self.detents != nil) { + NSArray *detents = self.detents != nil + ? [RCTConvert UISheetPresentationControllerDetentArray:self.detents] + // The default value is an array that contains the value largeDetent + : @[[UISheetPresentationControllerDetent largeDetent]]; + sheetPresentationController.detents = detents; + sheetPresentationController.prefersGrabberVisible = detents.count > 1; + } + } [_delegate presentModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]]; _isPresented = YES; } diff --git a/React/Views/RCTModalHostViewManager.m b/React/Views/RCTModalHostViewManager.m index 7fab70ef6363..d354301e2949 100644 --- a/React/Views/RCTModalHostViewManager.m +++ b/React/Views/RCTModalHostViewManager.m @@ -125,6 +125,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(detents, NSArray) // Fabric only RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java index 15ce94cb0ad6..d51577bb6425 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostManager.java @@ -102,6 +102,12 @@ public void setVisible(ReactModalHostView view, boolean visible) { @ReactProp(name = "presentationStyle") public void setPresentationStyle(ReactModalHostView view, @Nullable String value) {} + @Override + @ReactProp(name = "detent") + public void setDetent(ReactModalHostView view, @Nullable String value) { + // iOS only + } + @Override @ReactProp(name = "animated") public void setAnimated(ReactModalHostView view, boolean value) {}