Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows interactive dismissal of modals on iOS #42309

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
75e3e85
interactiveDismissal allows the interactive dismissal of the modal vi…
ChrisSchofieldCheckatrade Jan 16, 2024
6904dff
add example
ChrisSchofieldCheckatrade Jan 16, 2024
c7eaee3
Merge branch 'main' of github.com:ChrisSchofieldCheckatrade/react-native
ChrisSchofieldCheckatrade Jan 16, 2024
bc42c06
fix prettier issues
ChrisSchofieldCheckatrade Jan 16, 2024
9f72db2
fix more linting issues
ChrisSchofieldCheckatrade Jan 16, 2024
c299bbd
explicit truthy check
ChrisSchofieldCheckatrade Jan 16, 2024
cead58d
explicit truthy check
ChrisSchofieldCheckatrade Jan 16, 2024
824dd16
update snapshot
ChrisSchofieldCheckatrade Jan 16, 2024
16c9636
Merge branch 'main' into main
ChrisSchofieldCheckatrade Jan 17, 2024
ad6fa52
add fabric support
ChrisSchofieldCheckatrade Jan 22, 2024
fef3578
Merge branch 'main' of github.com:ChrisSchofieldCheckatrade/react-native
ChrisSchofieldCheckatrade Jan 22, 2024
85467e6
Merge branch 'main' into main
ChrisSchofieldCheckatrade Jan 22, 2024
69d078d
implement UIAdaptivePresentationControllerDelegate so that interactiv…
ChrisSchofieldCheckatrade Jan 22, 2024
48618c0
merge
ChrisSchofieldCheckatrade Jan 22, 2024
2979a66
Merge branch 'main' into main
ChrisSchofieldCheckatrade Jan 22, 2024
01d0799
move interactiveDismissal declaration to new home for NativeProps
ChrisSchofieldCheckatrade Jan 22, 2024
15a35e6
remove duplicate declaration of view property (should no longer be f…
ChrisSchofieldCheckatrade Jan 22, 2024
1962a9d
revert src dir changes
ChrisSchofieldCheckatrade Jan 22, 2024
afd456b
put back the src dir changes again
ChrisSchofieldCheckatrade Jan 22, 2024
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
5 changes: 5 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export interface ModalPropsIOS {
onOrientationChange?:
| ((event: NativeSyntheticEvent<any>) => void)
| undefined;

/**
* Allows the modal to be dismissed by an interactive gesture
*/
interactiveDismissal?: boolean | undefined;
}

export interface ModalPropsAndroid {
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/Libraries/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ export type Props = $ReadOnly<{|
* See https://reactnative.dev/docs/modal#onorientationchange
*/
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,

/**
* Allows the modal to be dismissed by an interactive gesture
*/
interactiveDismissal?: boolean,
|}>;

function confirmProps(props: Props) {
Expand Down Expand Up @@ -265,6 +270,7 @@ class Modal extends React.Component<Props> {
onStartShouldSetResponder={this._shouldSetResponder}
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}
interactiveDismissal={this.props.interactiveDismissal}
testID={this.props.testID}>
<VirtualizedListContextResetter>
<ScrollView.Context.Provider value={null}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/

#import <UIKit/UIKit.h>

#import <React/RCTMountingTransactionObserving.h>
#import <React/RCTViewComponentView.h>

/**
* UIView class for root <ModalHostView> component.
*/
@interface RCTModalHostViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving>
@interface RCTModalHostViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving, UIAdaptivePresentationControllerDelegate>

/**
* Subclasses may override this method and present the modal on different view controller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ @implementation RCTModalHostViewComponentView {
ModalHostViewShadowNode::ConcreteState::Shared _state;
BOOL _shouldAnimatePresentation;
BOOL _shouldPresent;
BOOL _interactiveDismissal;
BOOL _isPresented;
UIView *_modalContentsSnapshot;
}
Expand All @@ -113,6 +114,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_props = ModalHostViewShadowNode::defaultSharedProps();
_shouldAnimatePresentation = YES;

_interactiveDismissal = NO;
_isPresented = NO;
}

Expand Down Expand Up @@ -148,7 +150,10 @@ - (void)ensurePresentedOnlyIfNeeded
{
BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window;
if (shouldBePresented) {
_viewController.modalInPresentation = !self->_interactiveDismissal;
_viewController.presentationController.delegate = self;
_isPresented = YES;

[self presentViewController:self.viewController
animated:_shouldAnimatePresentation
completion:^{
Expand Down Expand Up @@ -189,6 +194,16 @@ - (void)ensurePresentedOnlyIfNeeded
return std::static_pointer_cast<const ModalHostViewEventEmitter>(_eventEmitter);
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onDismiss(ModalHostViewEventEmitter::OnDismiss{});
}
}

#pragma mark - RCTMountingTransactionObserving

- (void)mountingTransactionWillMount:(const MountingTransaction &)transaction
Expand Down Expand Up @@ -240,6 +255,7 @@ - (void)prepareForRecycle
_viewController = nil;
_isPresented = NO;
_shouldPresent = NO;
_interactiveDismissal = NO;
}

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
Expand All @@ -256,6 +272,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &

self.viewController.modalPresentationStyle = presentationConfiguration(newProps);

_interactiveDismissal = newProps.interactiveDismissal;
_shouldPresent = newProps.visible;
[self ensurePresentedOnlyIfNeeded];

Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/React/Views/RCTModalHostView.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
@property (nonatomic, copy) RCTDirectEventBlock onShow;
@property (nonatomic, assign) BOOL visible;

// iOS only
@property (nonatomic, assign) BOOL interactiveDismissal;

// Android only
@property (nonatomic, assign) BOOL statusBarTranslucent;
@property (nonatomic, assign) BOOL hardwareAccelerated;
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 @@ -39,6 +39,7 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_modalViewController.view = containerView;
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
_interactiveDismissal = NO;
_isPresented = NO;

__weak typeof(self) weakSelf = self;
Expand All @@ -63,13 +64,25 @@ - (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose
_onRequestClose = onRequestClose;
}

- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
return _interactiveDismissal;
}

- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)controller
{
if (_onRequestClose != nil) {
_onRequestClose(nil);
}
}

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if (_onDismiss) {
_onDismiss(nil);
}
}

- (void)notifyForOrientationChange
{
if (!_onOrientationChange) {
Expand Down Expand Up @@ -173,6 +186,7 @@ - (void)ensurePresentedOnlyIfNeeded
RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller");

_modalViewController.supportedInterfaceOrientations = [self supportedOrientationsMask];
_modalViewController.modalInPresentation = !self.interactiveDismissal;

if ([self.animationType isEqualToString:@"fade"]) {
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
Expand Down
6 changes: 2 additions & 4 deletions packages/react-native/React/Views/RCTModalHostViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,11 @@ - (void)invalidate
RCT_EXPORT_VIEW_PROPERTY(hardwareAccelerated, BOOL)
RCT_EXPORT_VIEW_PROPERTY(animated, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(identifier, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(interactiveDismissal, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(visible, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock)

// Fabric only
RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)

@end
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return eventTypeConstants;
}

@Override
@ReactProp(name = "interactiveDismissal")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enableGesture like RNSScreen seems like a better name?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interactiveDismissal matches the terminology Apple uses for this feature, so I'd argue no, but happy to revise to match consensus. (Reference)

public void setInteractiveDismissal(ReactModalHostView view, boolean interactiveDismissal) {
// iOS only
}

@Override
protected void onAfterUpdateTransaction(ReactModalHostView view) {
super.onAfterUpdateTransaction(view);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ type NativeProps = $ReadOnly<{|
*/
onOrientationChange?: ?DirectEventHandler<OrientationChangeEvent>,

/**
* Allows the modal to be dismissed by an interactive gesture
*/
interactiveDismissal?: WithDefault<boolean, false>,

/**
* The `identifier` is the unique number for identifying Modal components.
*/
Expand Down
34 changes: 33 additions & 1 deletion packages/rn-tester/js/examples/Modal/ModalPresentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function ModalPresentation() {
transparent: false,
hardwareAccelerated: false,
statusBarTranslucent: false,
interactiveDismissal: false,
presentationStyle: Platform.select({
ios: 'fullScreen',
default: undefined,
Expand All @@ -75,6 +76,16 @@ function ModalPresentation() {
const onOrientationChange = event =>
setCurrentOrientation(event.nativeEvent.orientation);

const onDismissInternal = () => {
if (props.interactiveDismissal === true) {
setProps(prev => ({...prev, visible: false}));
}

if (props.onDismiss) {
props.onDismiss();
}
};

const controls = (
<>
<View style={styles.inlineBlock}>
Expand Down Expand Up @@ -146,6 +157,26 @@ function ModalPresentation() {
</Text>
) : null}
</View>
{Platform.OS === 'ios' && (
<View style={styles.block}>
<View style={styles.rowWithSpaceBetween}>
<Text style={styles.title}>Interactive Dismissal</Text>
<Switch
value={props.interactiveDismissal}
onValueChange={enabled =>
setProps(prev => ({...prev, interactiveDismissal: enabled}))
}
/>
</View>
{!['pageSheet', 'formSheet'].includes(presentationStyle) ||
props.animationType !== 'slide' ? (
<Text style={styles.warning}>
Modal can only be dismissed interactively whilst using 'pageSheet'
or 'formSheet' Presentation Style, and 'slide' Animation Type
</Text>
) : null}
</View>
)}
<View style={styles.block}>
<Text style={styles.title}>Supported Orientation ⚫️</Text>
<View style={styles.row}>
Expand Down Expand Up @@ -223,7 +254,8 @@ function ModalPresentation() {
<Modal
{...props}
onRequestClose={onRequestClose}
onOrientationChange={onOrientationChange}>
onOrientationChange={onOrientationChange}
onDismiss={onDismissInternal}>
<View style={styles.modalContainer}>
<View style={styles.modalInnerContainer}>
<Text testID="modal_animationType_text">
Expand Down