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
9 changes: 9 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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<string> | undefined;

/**
* 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.
Expand Down
31 changes: 31 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ export type ModalPropsIOS = {
| '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<string>,

/**
* 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.
Expand Down Expand Up @@ -208,6 +217,27 @@ function confirmProps(props: ModalProps) {
'Modal requires the onRequestClose prop when used with `allowSwipeDismissal`. This is necessary to prevent state corruption.',
);
}

if (
Platform.OS === 'ios' &&
props.detents &&
!props.detents.every(detent => typeof detent === 'string')
) {
console.warn(
'Modal detents should be a combination of `large`, `medium` or any number represented as a string.',
);
}

if (
Platform.OS === 'ios' &&
(props.detents?.length ?? 0) > 0 &&
props.presentationStyle !== 'pageSheet' &&
props.presentationStyle !== 'formSheet'
) {
console.warn(
'Modal detents are only supported with `pageSheet` or `formSheet` presentation styles.',
);
}
}
}

Expand Down Expand Up @@ -344,6 +374,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}
allowSwipeDismissal={this.props.allowSwipeDismissal}
detents={this.props.detents}
testID={this.props.testID}>
<VirtualizedListContextResetter>
<ScrollView.Context.Provider value={null}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
#import <react/renderer/components/modal/ModalHostViewState.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>

#import "RCTConversions.h"

Expand Down Expand Up @@ -107,6 +108,7 @@ @implementation RCTModalHostViewComponentView {
BOOL _shouldPresent;
BOOL _isPresented;
BOOL _modalInPresentation;
NSArray<UISheetPresentationControllerDetent *>* _detents;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand Down Expand Up @@ -156,6 +158,13 @@ - (void)ensurePresentedOnlyIfNeeded
self.viewController.presentationController.delegate = self;
self.viewController.modalInPresentation = _modalInPresentation;

UISheetPresentationController *sheetPresentationController =
_viewController.sheetPresentationController;

if (sheetPresentationController) {
sheetPresentationController.detents = _detents;
}

_isPresented = YES;
[self presentViewController:self.viewController
animated:_shouldAnimatePresentation
Expand Down Expand Up @@ -243,7 +252,10 @@ - (void)boundsDidChange:(CGRect)newBounds

if (_state != nullptr) {
auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)};
_state->updateState(std::move(newState));
BOOL enableImmediateUpdateForModalDetents =
ReactNativeFeatureFlags::enableImmediateUpdateForModalDetents();

_state->updateState(std::move(newState), enableImmediateUpdateForModalDetents ? EventQueue::UpdateMode::unstable_Immediate : EventQueue::UpdateMode::Asynchronous);
}
}

Expand Down Expand Up @@ -282,6 +294,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
_modalInPresentation = !newProps.allowSwipeDismissal;
self.viewController.modalInPresentation = _modalInPresentation;
}

if (oldViewProps.detents != newProps.detents) {
_detents = [self calculateDetents:newProps.detents];
if (_viewController.sheetPresentationController) {
_viewController.sheetPresentationController.detents = _detents;
}
}

_shouldPresent = newProps.visible;
[self ensurePresentedOnlyIfNeeded];
Expand Down Expand Up @@ -325,6 +344,41 @@ - (void)presentationControllerDidDismiss:(UIPresentationController *)presentatio
}
}

#pragma mark - Helpers

- (NSArray<UISheetPresentationControllerDetent *> *)calculateDetents:(const std::vector<std::string> &)detents {
if (@available(iOS 16.0, *)) {
NSMutableArray<UISheetPresentationControllerDetent *> *detentsArray = [NSMutableArray new];

for (const auto &detent : detents) {
NSString *detentString = [NSString stringWithUTF8String:detent.c_str()];

if ([detentString isEqualToString:@"medium"]) {
[detentsArray addObject:[UISheetPresentationControllerDetent mediumDetent]];
} else if ([detentString isEqualToString:@"large"]) {
[detentsArray addObject:[UISheetPresentationControllerDetent largeDetent]];
} else {
// Try to parse as a number (custom detent)
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
NSNumber *number = [formatter numberFromString:detentString];

if (number != nil) {
float value = [number floatValue];
[detentsArray addObject:[UISheetPresentationControllerDetent
customDetentWithIdentifier:nil
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
return value;
}]];
}
}
}

return detentsArray;
}

return [NSMutableArray new];
}

@end

#ifdef __cplusplus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<3ea1ee77358d99334a7c40bed44f2d90>>
* @generated SignedSource<<dc6fe1b0ab9d22a8c9b2034f1724e522>>
*/

/**
Expand Down Expand Up @@ -192,6 +192,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableImagePrefetchingOnUiThreadAndroid(): Boolean = accessor.enableImagePrefetchingOnUiThreadAndroid()

/**
* When enabled, updates to modal detents will be applied immediately instead of being deferred to the next layout pass.
*/
@JvmStatic
public fun enableImmediateUpdateForModalDetents(): Boolean = accessor.enableImmediateUpdateForModalDetents()

/**
* Dispatches state updates for content offset changes synchronously on the main thread.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<007a5a1235a999716b382ffd4ca23158>>
* @generated SignedSource<<2df72858c4ff2678f885dc8c6cffda14>>
*/

/**
Expand Down Expand Up @@ -47,6 +47,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableImagePrefetchingAndroidCache: Boolean? = null
private var enableImagePrefetchingOnUiThreadAndroidCache: Boolean? = null
private var enableImmediateUpdateForModalDetentsCache: Boolean? = null
private var enableImmediateUpdateModeForContentOffsetChangesCache: Boolean? = null
private var enableImperativeFocusCache: Boolean? = null
private var enableInteropViewManagerClassLookUpOptimizationIOSCache: Boolean? = null
Expand Down Expand Up @@ -345,6 +346,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableImmediateUpdateForModalDetents(): Boolean {
var cached = enableImmediateUpdateForModalDetentsCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableImmediateUpdateForModalDetents()
enableImmediateUpdateForModalDetentsCache = cached
}
return cached
}

override fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean {
var cached = enableImmediateUpdateModeForContentOffsetChangesCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<8a775646d455cdb6d017fd08aee3e807>>
* @generated SignedSource<<86eaaf389de7ba9e424eb5797b2fcb8a>>
*/

/**
Expand Down Expand Up @@ -82,6 +82,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enableImagePrefetchingOnUiThreadAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enableImmediateUpdateForModalDetents(): Boolean

@DoNotStrip @JvmStatic public external fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean

@DoNotStrip @JvmStatic public external fun enableImperativeFocus(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<afccaed3066598724041c48bfb524234>>
* @generated SignedSource<<ffeefbd7bbb64bd62e5818407bfd4efe>>
*/

/**
Expand Down Expand Up @@ -77,6 +77,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enableImagePrefetchingOnUiThreadAndroid(): Boolean = false

override fun enableImmediateUpdateForModalDetents(): Boolean = false

override fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean = false

override fun enableImperativeFocus(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<ebeb9b37b8d3d0e31f4d4966cbf42b3f>>
* @generated SignedSource<<e7028f968b274fb1b425b0b310752a9d>>
*/

/**
Expand Down Expand Up @@ -51,6 +51,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableImagePrefetchingAndroidCache: Boolean? = null
private var enableImagePrefetchingOnUiThreadAndroidCache: Boolean? = null
private var enableImmediateUpdateForModalDetentsCache: Boolean? = null
private var enableImmediateUpdateModeForContentOffsetChangesCache: Boolean? = null
private var enableImperativeFocusCache: Boolean? = null
private var enableInteropViewManagerClassLookUpOptimizationIOSCache: Boolean? = null
Expand Down Expand Up @@ -376,6 +377,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enableImmediateUpdateForModalDetents(): Boolean {
var cached = enableImmediateUpdateForModalDetentsCache
if (cached == null) {
cached = currentProvider.enableImmediateUpdateForModalDetents()
accessedFeatureFlags.add("enableImmediateUpdateForModalDetents")
enableImmediateUpdateForModalDetentsCache = cached
}
return cached
}

override fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean {
var cached = enableImmediateUpdateModeForContentOffsetChangesCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<dffc5db0f80501f6246f9d6087c6fb4a>>
* @generated SignedSource<<144aa9984009c2c26681ccbb23129927>>
*/

/**
Expand All @@ -25,6 +25,8 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android :

override fun enableAccessibilityOrder(): Boolean = true

override fun enableImmediateUpdateForModalDetents(): Boolean = true

override fun enableSwiftUIBasedFilters(): Boolean = true

override fun preventShadowTreeCommitExhaustion(): Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<1c4ea60d119996f37742542976fedcc8>>
* @generated SignedSource<<bd69726ef1f51033123a1e70588a25e4>>
*/

/**
Expand Down Expand Up @@ -77,6 +77,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enableImagePrefetchingOnUiThreadAndroid(): Boolean

@DoNotStrip public fun enableImmediateUpdateForModalDetents(): Boolean

@DoNotStrip public fun enableImmediateUpdateModeForContentOffsetChanges(): Boolean

@DoNotStrip public fun enableImperativeFocus(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ internal class ReactModalHostManager :
// iOS only
}

@ReactProp(name = "detent")
override fun setDetent(view: ReactModalHostView, value: String?) {
// iOS only
}

@ReactProp(name = "presentationStyle")
override fun setPresentationStyle(view: ReactModalHostView, value: String?): Unit = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<d04720bbbd42eaf338be612a0f3e36a6>>
* @generated SignedSource<<9d6ca4ab85f104654c347ed949eb669e>>
*/

/**
Expand Down Expand Up @@ -201,6 +201,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}

bool enableImmediateUpdateForModalDetents() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableImmediateUpdateForModalDetents");
return method(javaProvider_);
}

bool enableImmediateUpdateModeForContentOffsetChanges() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableImmediateUpdateModeForContentOffsetChanges");
Expand Down Expand Up @@ -664,6 +670,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableImagePrefetchingOnUiThreadAndroid
return ReactNativeFeatureFlags::enableImagePrefetchingOnUiThreadAndroid();
}

bool JReactNativeFeatureFlagsCxxInterop::enableImmediateUpdateForModalDetents(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableImmediateUpdateForModalDetents();
}

bool JReactNativeFeatureFlagsCxxInterop::enableImmediateUpdateModeForContentOffsetChanges(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableImmediateUpdateModeForContentOffsetChanges();
Expand Down Expand Up @@ -1046,6 +1057,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableImagePrefetchingOnUiThreadAndroid",
JReactNativeFeatureFlagsCxxInterop::enableImagePrefetchingOnUiThreadAndroid),
makeNativeMethod(
"enableImmediateUpdateForModalDetents",
JReactNativeFeatureFlagsCxxInterop::enableImmediateUpdateForModalDetents),
makeNativeMethod(
"enableImmediateUpdateModeForContentOffsetChanges",
JReactNativeFeatureFlagsCxxInterop::enableImmediateUpdateModeForContentOffsetChanges),
Expand Down
Loading
Loading