Skip to content

Commit

Permalink
Allow <Modal /> to be presented in different orientations
Browse files Browse the repository at this point in the history
Reviewed By: javache

Differential Revision: D3760002

fbshipit-source-id: 01f5c246fb0fc041ec2d63b4ef80de858fb6fdf2
  • Loading branch information
Mehdi Mulani authored and Facebook Github Bot 3 committed Sep 7, 2016
1 parent a13e1c4 commit de3457f
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 11 deletions.
38 changes: 37 additions & 1 deletion Examples/UIExplorer/js/ModalExample.js
Expand Up @@ -26,12 +26,15 @@ var React = require('react');
var ReactNative = require('react-native');
var {
Modal,
Picker,
StyleSheet,
Switch,
Text,
TouchableHighlight,
View,
} = ReactNative;
// $FlowFixMe Picker.Item not properly defined for flow.
const Item = Picker.Item;

exports.displayName = (undefined: ?string);
exports.framework = 'React';
Expand Down Expand Up @@ -68,11 +71,22 @@ class Button extends React.Component {
}
}

const supportedOrientationsPickerValues = [
['portrait'],
['landscape'],
['landscape-left'],
['portrait', 'landscape-right'],
['portrait', 'landscape'],
[],
];

class ModalExample extends React.Component {
state = {
animationType: 'none',
modalVisible: false,
transparent: false,
selectedSupportedOrientation: 0,
currentOrientation: 'unknown',
};

_setModalVisible = (visible) => {
Expand Down Expand Up @@ -104,11 +118,14 @@ class ModalExample extends React.Component {
animationType={this.state.animationType}
transparent={this.state.transparent}
visible={this.state.modalVisible}
onRequestClose={() => {this._setModalVisible(false)}}
onRequestClose={() => this._setModalVisible(false)}
supportedOrientations={supportedOrientationsPickerValues[this.state.selectedSupportedOrientation]}
onOrientationChange={evt => this.setState({currentOrientation: evt.nativeEvent.orientation})}
>
<View style={[styles.container, modalBackgroundStyle]}>
<View style={[styles.innerContainer, innerContainerTransparentStyle]}>
<Text>This modal was presented {this.state.animationType === 'none' ? 'without' : 'with'} animation.</Text>
<Text>It is currently displayed in {this.state.currentOrientation} mode.</Text>
<Button
onPress={this._setModalVisible.bind(this, false)}
style={styles.modalButton}>
Expand All @@ -135,6 +152,22 @@ class ModalExample extends React.Component {
<Switch value={this.state.transparent} onValueChange={this._toggleTransparent} />
</View>

<View>
<Text style={styles.rowTitle}>Supported orientations</Text>
<Picker
selectedValue={this.state.selectedSupportedOrientation}
onValueChange={(_, i) => this.setState({selectedSupportedOrientation: i})}
itemStyle={styles.pickerItem}
>
<Item label="Portrait" value={0} />
<Item label="Landscape" value={1} />
<Item label="Landscape left" value={2} />
<Item label="Portrait and landscape right" value={3} />
<Item label="Portrait and landscape" value={4} />
<Item label="Default supportedOrientations" value={5} />
</Picker>
</View>

<Button onPress={this._setModalVisible.bind(this, true)}>
Present
</Button>
Expand Down Expand Up @@ -187,4 +220,7 @@ var styles = StyleSheet.create({
modalButton: {
marginTop: 10,
},
pickerItem: {
fontSize: 16,
},
});
14 changes: 14 additions & 0 deletions Libraries/Modal/Modal.js
Expand Up @@ -111,6 +111,18 @@ class Modal extends React.Component {
PropTypes.bool,
'Use the `animationType` prop instead.'
),
/**
* 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.
* @platform ios
*/
supportedOrientations: PropTypes.arrayOf(PropTypes.oneOf(['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right'])),
/**
* The `onOrientationChange` callback is called when the orientation changes while the modal is being displayed.
* The orientation provided is only 'portrait' or 'landscape'. This callback is also called on initial render, regardless of the current orientation.
* @platform ios
*/
onOrientationChange: PropTypes.func,
};

static defaultProps = {
Expand Down Expand Up @@ -144,6 +156,8 @@ class Modal extends React.Component {
onShow={this.props.onShow}
style={styles.modal}
onStartShouldSetResponder={this._shouldSetResponder}
supportedOrientations={this.props.supportedOrientations}
onOrientationChange={this.props.onOrientationChange}
>
<View style={[styles.container, containerStyles]}>
{this.props.children}
Expand Down
3 changes: 3 additions & 0 deletions React/Views/RCTModalHostView.h
Expand Up @@ -27,6 +27,9 @@

@property (nonatomic, weak) id<RCTModalHostViewInteractor> delegate;

@property (nonatomic, copy) NSArray<NSString *> *supportedOrientations;
@property (nonatomic, copy) RCTDirectEventBlock onOrientationChange;

- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;

@end
Expand Down
52 changes: 52 additions & 0 deletions React/Views/RCTModalHostView.m
Expand Up @@ -16,13 +16,16 @@
#import "RCTUIManager.h"
#import "UIView+React.h"

#import <UIKit/UIKit.h>

@implementation RCTModalHostView
{
__weak RCTBridge *_bridge;
BOOL _isPresented;
RCTModalHostViewController *_modalViewController;
RCTTouchHandler *_touchHandler;
UIView *_reactSubview;
UIInterfaceOrientation _lastKnownOrientation;
}

RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
Expand Down Expand Up @@ -52,7 +55,28 @@ - (void)notifyForBoundsChange:(CGRect)newBounds
{
if (_reactSubview && _isPresented) {
[_bridge.uiManager setFrame:newBounds forView:_reactSubview];
[self notifyForOrientationChange];
}
}

- (void)notifyForOrientationChange
{
if (!_onOrientationChange) {
return;
}

UIInterfaceOrientation currentOrientation = [[UIApplication sharedApplication] statusBarOrientation];
if (currentOrientation == _lastKnownOrientation) {
return;
}
_lastKnownOrientation = currentOrientation;

BOOL isPortrait = currentOrientation == UIInterfaceOrientationPortrait || currentOrientation == UIInterfaceOrientationPortraitUpsideDown;
NSDictionary *eventPayload =
@{
@"orientation": isPortrait ? @"portrait" : @"landscape",
};
_onOrientationChange(eventPayload);
}

- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
Expand Down Expand Up @@ -95,6 +119,7 @@ - (void)didMoveToWindow
if (!_isPresented && self.window) {
RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller");

_modalViewController.supportedInterfaceOrientations = [self supportedOrientationsMask];
if ([self.animationType isEqualToString:@"fade"]) {
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
} else if ([self.animationType isEqualToString:@"slide"]) {
Expand Down Expand Up @@ -136,4 +161,31 @@ - (void)setTransparent:(BOOL)transparent
_modalViewController.modalPresentationStyle = transparent ? UIModalPresentationCustom : UIModalPresentationFullScreen;
}

- (UIInterfaceOrientationMask)supportedOrientationsMask
{
if (_supportedOrientations.count == 0) {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}

UIInterfaceOrientationMask supportedOrientations = 0;
for (NSString *orientation in _supportedOrientations) {
if ([orientation isEqualToString:@"portrait"]) {
supportedOrientations |= UIInterfaceOrientationMaskPortrait;
} else if ([orientation isEqualToString:@"portrait-upside-down"]) {
supportedOrientations |= UIInterfaceOrientationMaskPortraitUpsideDown;
} else if ([orientation isEqualToString:@"landscape"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscape;
} else if ([orientation isEqualToString:@"landscape-left"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeLeft;
} else if ([orientation isEqualToString:@"landscape-right"]) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeRight;
}
}
return supportedOrientations;
}

@end
2 changes: 2 additions & 0 deletions React/Views/RCTModalHostViewController.h
Expand Up @@ -13,4 +13,6 @@

@property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds);

@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations;

@end
12 changes: 2 additions & 10 deletions React/Views/RCTModalHostViewController.m
Expand Up @@ -9,6 +9,8 @@

#import "RCTModalHostViewController.h"

#import "RCTModalHostView.h"

@implementation RCTModalHostViewController
{
CGRect _lastViewFrame;
Expand Down Expand Up @@ -38,16 +40,6 @@ - (void)viewDidLayoutSubviews
}
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
// Picking some defaults here, we should probably make this configurable
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}

- (UIStatusBarStyle)preferredStatusBarStyle
{
return _preferredStatusBarStyle;
Expand Down
2 changes: 2 additions & 0 deletions React/Views/RCTModalHostViewManager.m
Expand Up @@ -95,5 +95,7 @@ - (void)invalidate
RCT_EXPORT_VIEW_PROPERTY(animationType, NSString)
RCT_EXPORT_VIEW_PROPERTY(transparent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)

@end

0 comments on commit de3457f

Please sign in to comment.