Allow <Modal /> to be presented in different orientations
Reviewed By: javache Differential Revision: D3760002 fbshipit-source-id: 01f5c246fb0fc041ec2d63b4ef80de858fb6fdf2
This commit is contained in:
parent
a13e1c4e2c
commit
de3457f31d
|
@ -26,12 +26,15 @@ var React = require('react');
|
||||||
var ReactNative = require('react-native');
|
var ReactNative = require('react-native');
|
||||||
var {
|
var {
|
||||||
Modal,
|
Modal,
|
||||||
|
Picker,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
TouchableHighlight,
|
TouchableHighlight,
|
||||||
View,
|
View,
|
||||||
} = ReactNative;
|
} = ReactNative;
|
||||||
|
// $FlowFixMe Picker.Item not properly defined for flow.
|
||||||
|
const Item = Picker.Item;
|
||||||
|
|
||||||
exports.displayName = (undefined: ?string);
|
exports.displayName = (undefined: ?string);
|
||||||
exports.framework = 'React';
|
exports.framework = 'React';
|
||||||
|
@ -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 {
|
class ModalExample extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
animationType: 'none',
|
animationType: 'none',
|
||||||
modalVisible: false,
|
modalVisible: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
|
selectedSupportedOrientation: 0,
|
||||||
|
currentOrientation: 'unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
_setModalVisible = (visible) => {
|
_setModalVisible = (visible) => {
|
||||||
|
@ -104,11 +118,14 @@ class ModalExample extends React.Component {
|
||||||
animationType={this.state.animationType}
|
animationType={this.state.animationType}
|
||||||
transparent={this.state.transparent}
|
transparent={this.state.transparent}
|
||||||
visible={this.state.modalVisible}
|
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.container, modalBackgroundStyle]}>
|
||||||
<View style={[styles.innerContainer, innerContainerTransparentStyle]}>
|
<View style={[styles.innerContainer, innerContainerTransparentStyle]}>
|
||||||
<Text>This modal was presented {this.state.animationType === 'none' ? 'without' : 'with'} animation.</Text>
|
<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
|
<Button
|
||||||
onPress={this._setModalVisible.bind(this, false)}
|
onPress={this._setModalVisible.bind(this, false)}
|
||||||
style={styles.modalButton}>
|
style={styles.modalButton}>
|
||||||
|
@ -135,6 +152,22 @@ class ModalExample extends React.Component {
|
||||||
<Switch value={this.state.transparent} onValueChange={this._toggleTransparent} />
|
<Switch value={this.state.transparent} onValueChange={this._toggleTransparent} />
|
||||||
</View>
|
</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)}>
|
<Button onPress={this._setModalVisible.bind(this, true)}>
|
||||||
Present
|
Present
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -187,4 +220,7 @@ var styles = StyleSheet.create({
|
||||||
modalButton: {
|
modalButton: {
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
},
|
},
|
||||||
|
pickerItem: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,6 +111,18 @@ class Modal extends React.Component {
|
||||||
PropTypes.bool,
|
PropTypes.bool,
|
||||||
'Use the `animationType` prop instead.'
|
'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 = {
|
static defaultProps = {
|
||||||
|
@ -144,6 +156,8 @@ class Modal extends React.Component {
|
||||||
onShow={this.props.onShow}
|
onShow={this.props.onShow}
|
||||||
style={styles.modal}
|
style={styles.modal}
|
||||||
onStartShouldSetResponder={this._shouldSetResponder}
|
onStartShouldSetResponder={this._shouldSetResponder}
|
||||||
|
supportedOrientations={this.props.supportedOrientations}
|
||||||
|
onOrientationChange={this.props.onOrientationChange}
|
||||||
>
|
>
|
||||||
<View style={[styles.container, containerStyles]}>
|
<View style={[styles.container, containerStyles]}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
|
|
||||||
@property (nonatomic, weak) id<RCTModalHostViewInteractor> delegate;
|
@property (nonatomic, weak) id<RCTModalHostViewInteractor> delegate;
|
||||||
|
|
||||||
|
@property (nonatomic, copy) NSArray<NSString *> *supportedOrientations;
|
||||||
|
@property (nonatomic, copy) RCTDirectEventBlock onOrientationChange;
|
||||||
|
|
||||||
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
|
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
#import "RCTUIManager.h"
|
#import "RCTUIManager.h"
|
||||||
#import "UIView+React.h"
|
#import "UIView+React.h"
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@implementation RCTModalHostView
|
@implementation RCTModalHostView
|
||||||
{
|
{
|
||||||
__weak RCTBridge *_bridge;
|
__weak RCTBridge *_bridge;
|
||||||
|
@ -23,6 +25,7 @@
|
||||||
RCTModalHostViewController *_modalViewController;
|
RCTModalHostViewController *_modalViewController;
|
||||||
RCTTouchHandler *_touchHandler;
|
RCTTouchHandler *_touchHandler;
|
||||||
UIView *_reactSubview;
|
UIView *_reactSubview;
|
||||||
|
UIInterfaceOrientation _lastKnownOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
||||||
|
@ -52,9 +55,30 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder)
|
||||||
{
|
{
|
||||||
if (_reactSubview && _isPresented) {
|
if (_reactSubview && _isPresented) {
|
||||||
[_bridge.uiManager setFrame:newBounds forView:_reactSubview];
|
[_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
|
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
|
||||||
{
|
{
|
||||||
RCTAssert(_reactSubview == nil, @"Modal view can only have one subview");
|
RCTAssert(_reactSubview == nil, @"Modal view can only have one subview");
|
||||||
|
@ -95,6 +119,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder)
|
||||||
if (!_isPresented && self.window) {
|
if (!_isPresented && self.window) {
|
||||||
RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller");
|
RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller");
|
||||||
|
|
||||||
|
_modalViewController.supportedInterfaceOrientations = [self supportedOrientationsMask];
|
||||||
if ([self.animationType isEqualToString:@"fade"]) {
|
if ([self.animationType isEqualToString:@"fade"]) {
|
||||||
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
|
_modalViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
|
||||||
} else if ([self.animationType isEqualToString:@"slide"]) {
|
} else if ([self.animationType isEqualToString:@"slide"]) {
|
||||||
|
@ -136,4 +161,31 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder)
|
||||||
_modalViewController.modalPresentationStyle = transparent ? UIModalPresentationCustom : UIModalPresentationFullScreen;
|
_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
|
@end
|
||||||
|
|
|
@ -13,4 +13,6 @@
|
||||||
|
|
||||||
@property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds);
|
@property (nonatomic, copy) void (^boundsDidChangeBlock)(CGRect newBounds);
|
||||||
|
|
||||||
|
@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
#import "RCTModalHostViewController.h"
|
#import "RCTModalHostViewController.h"
|
||||||
|
|
||||||
|
#import "RCTModalHostView.h"
|
||||||
|
|
||||||
@implementation RCTModalHostViewController
|
@implementation RCTModalHostViewController
|
||||||
{
|
{
|
||||||
CGRect _lastViewFrame;
|
CGRect _lastViewFrame;
|
||||||
|
@ -38,16 +40,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
|
||||||
{
|
|
||||||
// Picking some defaults here, we should probably make this configurable
|
|
||||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
|
|
||||||
return UIInterfaceOrientationMaskAll;
|
|
||||||
} else {
|
|
||||||
return UIInterfaceOrientationMaskPortrait;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||||
{
|
{
|
||||||
return _preferredStatusBarStyle;
|
return _preferredStatusBarStyle;
|
||||||
|
|
|
@ -95,5 +95,7 @@ RCT_EXPORT_MODULE()
|
||||||
RCT_EXPORT_VIEW_PROPERTY(animationType, NSString)
|
RCT_EXPORT_VIEW_PROPERTY(animationType, NSString)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(transparent, BOOL)
|
RCT_EXPORT_VIEW_PROPERTY(transparent, BOOL)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onShow, RCTDirectEventBlock)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
Loading…
Reference in New Issue