diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js
index afc4306d2..080fc8018 100644
--- a/Libraries/Components/ScrollView/ScrollView.js
+++ b/Libraries/Components/ScrollView/ScrollView.js
@@ -483,6 +483,30 @@ const ScrollView = React.createClass({
},
render: function() {
+ let ScrollViewClass;
+ let ScrollContentContainerViewClass;
+ if (Platform.OS === 'ios') {
+ ScrollViewClass = RCTScrollView;
+ ScrollContentContainerViewClass = RCTScrollContentView;
+ } else if (Platform.OS === 'android') {
+ if (this.props.horizontal) {
+ ScrollViewClass = AndroidHorizontalScrollView;
+ } else {
+ ScrollViewClass = AndroidScrollView;
+ }
+ ScrollContentContainerViewClass = View;
+ }
+
+ invariant(
+ ScrollViewClass !== undefined,
+ 'ScrollViewClass must not be undefined'
+ );
+
+ invariant(
+ ScrollContentContainerViewClass !== undefined,
+ 'ScrollContentContainerViewClass must not be undefined'
+ );
+
const contentContainerStyle = [
this.props.horizontal && styles.contentContainerHorizontal,
this.props.contentContainerStyle,
@@ -507,14 +531,14 @@ const ScrollView = React.createClass({
}
const contentContainer =
-
{this.props.children}
- ;
+ ;
const alwaysBounceHorizontal =
this.props.alwaysBounceHorizontal !== undefined ?
@@ -559,21 +583,6 @@ const ScrollView = React.createClass({
props.decelerationRate = processDecelerationRate(decelerationRate);
}
- let ScrollViewClass;
- if (Platform.OS === 'ios') {
- ScrollViewClass = RCTScrollView;
- } else if (Platform.OS === 'android') {
- if (this.props.horizontal) {
- ScrollViewClass = AndroidHorizontalScrollView;
- } else {
- ScrollViewClass = AndroidScrollView;
- }
- }
- invariant(
- ScrollViewClass !== undefined,
- 'ScrollViewClass must not be undefined'
- );
-
const refreshControl = this.props.refreshControl;
if (refreshControl) {
if (Platform.OS === 'ios') {
@@ -626,7 +635,7 @@ const styles = StyleSheet.create({
},
});
-let nativeOnlyProps, AndroidScrollView, AndroidHorizontalScrollView, RCTScrollView;
+let nativeOnlyProps, AndroidScrollView, AndroidHorizontalScrollView, RCTScrollView, RCTScrollContentView;
if (Platform.OS === 'android') {
nativeOnlyProps = {
nativeOnly: {
@@ -649,6 +658,7 @@ if (Platform.OS === 'android') {
}
};
RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps);
+ RCTScrollContentView = requireNativeComponent('RCTScrollContentView', View);
}
module.exports = ScrollView;
diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m
index bfa854b22..0ea13179b 100644
--- a/Libraries/Text/RCTTextField.m
+++ b/Libraries/Text/RCTTextField.m
@@ -227,6 +227,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
key:nil
eventCount:_nativeEventCount];
}
+
- (void)textFieldSubmitEditing
{
_submitted = YES;
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index b72323589..627e2e0f4 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -552,6 +552,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
typedef struct {
CGRect frame;
+ UIUserInterfaceLayoutDirection layoutDirection;
BOOL isNew;
BOOL parentIsNew;
BOOL isHidden;
@@ -568,6 +569,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
reactTags[index] = shadowView.reactTag;
frameDataArray[index++] = (RCTFrameData){
shadowView.frame,
+ shadowView.effectiveLayoutDirection,
shadowView.isNewView,
shadowView.superview.isNewView,
shadowView.isHidden,
@@ -634,6 +636,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
CGRect frame = frameData.frame;
BOOL isHidden = frameData.isHidden;
+ UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection;
BOOL isNew = frameData.isNew;
RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation;
BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew;
@@ -654,6 +657,10 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
view.hidden = isHidden;
}
+ if (view.reactLayoutDirection != layoutDirection) {
+ view.reactLayoutDirection = layoutDirection;
+ }
+
RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag];
if (createAnimation) {
diff --git a/React/Views/RCTScrollContentShadowView.h b/React/Views/RCTScrollContentShadowView.h
new file mode 100644
index 000000000..8b94dd2d6
--- /dev/null
+++ b/React/Views/RCTScrollContentShadowView.h
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import
+
+#import
+
+@interface RCTScrollContentShadowView : RCTShadowView
+
+@end
diff --git a/React/Views/RCTScrollContentShadowView.m b/React/Views/RCTScrollContentShadowView.m
new file mode 100644
index 000000000..51854ebfd
--- /dev/null
+++ b/React/Views/RCTScrollContentShadowView.m
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTScrollContentShadowView.h"
+
+#import
+
+#import "RCTUtils.h"
+
+@interface RCTShadowView () {
+ // This will be removed after t15757916, which will remove
+ // side-effects from `setFrame:` method.
+ @public CGRect _frame;
+}
+@end
+
+@implementation RCTScrollContentShadowView
+
+- (void)applyLayoutNode:(YGNodeRef)node
+ viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame
+ absolutePosition:(CGPoint)absolutePosition
+{
+ // Call super method if LTR layout is enforced.
+ if (self.effectiveLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
+ [super applyLayoutNode:node
+ viewsWithNewFrame:viewsWithNewFrame
+ absolutePosition:absolutePosition];
+ return;
+ }
+
+ // Motivation:
+ // Yoga place `contentView` on the right side of `scrollView` when RTL layout is enfoced.
+ // That breaks everything; it is completly pointless to (re)position `contentView`
+ // because it is `contentView`'s job. So, we work around it here.
+
+ // Step 1. Compensate `absolutePosition` change.
+ CGFloat xCompensation = YGNodeLayoutGetRight(node) - YGNodeLayoutGetLeft(node);
+ absolutePosition.x += xCompensation;
+
+ // Step 2. Call super method.
+ [super applyLayoutNode:node
+ viewsWithNewFrame:viewsWithNewFrame
+ absolutePosition:absolutePosition];
+
+ // Step 3. Reset the position.
+ _frame.origin.x = RCTRoundPixelValue(YGNodeLayoutGetRight(node));
+}
+
+@end
diff --git a/React/Views/RCTScrollContentViewManager.h b/React/Views/RCTScrollContentViewManager.h
new file mode 100644
index 000000000..e1c90e31b
--- /dev/null
+++ b/React/Views/RCTScrollContentViewManager.h
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import
+
+@interface RCTScrollContentViewManager : RCTViewManager
+
+@end
diff --git a/React/Views/RCTScrollContentViewManager.m b/React/Views/RCTScrollContentViewManager.m
new file mode 100644
index 000000000..5639b4fc9
--- /dev/null
+++ b/React/Views/RCTScrollContentViewManager.m
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#import "RCTScrollContentViewManager.h"
+
+#import "RCTScrollContentShadowView.h"
+
+@implementation RCTScrollContentViewManager
+
+RCT_EXPORT_MODULE()
+
+- (RCTShadowView *)shadowView
+{
+ return [RCTScrollContentShadowView new];
+}
+
+@end
diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m
index 5cb59d632..3115b9142 100644
--- a/React/Views/RCTScrollView.m
+++ b/React/Views/RCTScrollView.m
@@ -159,6 +159,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
{
if ((self = [super initWithFrame:frame])) {
[self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)];
+
+ // We intentionaly force `UIScrollView`s `semanticContentAttribute` to `LTR` here
+ // because this attribute affects a position of vertical scrollbar; we don't want this
+ // scrollbar flip because we also flip it with whole `UIScrollView` flip.
+ self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
}
return self;
}
@@ -191,7 +196,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
self.panGestureRecognizer.enabled = YES;
// TODO: If mid bounce, animate the scroll view to a non-bounced position
// while disabling (but only if `stopScrollInteractionIfJSHasResponder` was
- // called *during* a `pan`. Currently, it will just snap into place which
+ // called *during* a `pan`). Currently, it will just snap into place which
// is not so bad either.
// Another approach:
// self.scrollEnabled = NO;
@@ -278,9 +283,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
{
UIView *contentView = [self contentView];
CGFloat scrollTop = self.bounds.origin.y + self.contentInset.top;
+
+#if !TARGET_OS_TV
// If the RefreshControl is refreshing, remove it's height so sticky headers are
// positioned properly when scrolling down while refreshing.
-#if !TARGET_OS_TV
if (_rctRefreshControl != nil && _rctRefreshControl.refreshing) {
scrollTop -= _rctRefreshControl.frame.size.height;
}
@@ -451,6 +457,21 @@ static inline BOOL isRectInvalid(CGRect rect) {
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
+static inline void RCTApplyTranformationAccordingLayoutDirection(UIView *view, UIUserInterfaceLayoutDirection layoutDirection) {
+ view.transform =
+ layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
+ CGAffineTransformIdentity :
+ CGAffineTransformMakeScale(-1, 1);
+}
+
+- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
+{
+ [super setReactLayoutDirection:layoutDirection];
+
+ RCTApplyTranformationAccordingLayoutDirection(_scrollView, layoutDirection);
+ RCTApplyTranformationAccordingLayoutDirection(_contentView, layoutDirection);
+}
+
- (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
{
// Does nothing
@@ -467,6 +488,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
{
RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview");
_contentView = view;
+ RCTApplyTranformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
[_scrollView addSubview:view];
}
}
@@ -921,16 +943,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
{
if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) {
return _contentSize;
- } else if (!_contentView) {
- return CGSizeZero;
- } else {
- CGSize singleSubviewSize = _contentView.frame.size;
- CGPoint singleSubviewPosition = _contentView.frame.origin;
- return (CGSize){
- singleSubviewSize.width + singleSubviewPosition.x,
- singleSubviewSize.height + singleSubviewPosition.y
- };
}
+
+ return _contentView.frame.size;
}
- (void)reactBridgeDidFinishTransaction
diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h
index 92c1b56dc..fc51df83a 100644
--- a/React/Views/RCTShadowView.h
+++ b/React/Views/RCTShadowView.h
@@ -62,6 +62,11 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry
*/
@property (nonatomic, assign, getter=isHidden) BOOL hidden;
+/**
+ * Computed layout direction for the view backed to Yoga node value.
+ */
+@property (nonatomic, assign, readonly) UIUserInterfaceLayoutDirection effectiveLayoutDirection;
+
/**
* Position and dimensions.
* Defaults to { 0, 0, NAN, NAN }.
diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m
index e46e2b306..d300d1ba7 100644
--- a/React/Views/RCTShadowView.m
+++ b/React/Views/RCTShadowView.m
@@ -438,6 +438,12 @@ static void RCTProcessMetaPropsBorder(const YGValue metaProps[META_PROP_COUNT],
return description;
}
+// Layout Direction
+
+- (UIUserInterfaceLayoutDirection)effectiveLayoutDirection {
+ return YGNodeLayoutGetDirection(self.cssNode) == YGDirectionRTL ? UIUserInterfaceLayoutDirectionRightToLeft : UIUserInterfaceLayoutDirectionLeftToRight;
+}
+
// Margin
#define RCT_MARGIN_PROPERTY(prop, metaProp) \
diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h
index 2e1081bdb..bf8e0b43f 100644
--- a/React/Views/RCTView.h
+++ b/React/Views/RCTView.h
@@ -40,6 +40,13 @@
*/
+ (UIEdgeInsets)contentInsetsForView:(UIView *)curView;
+/**
+ * Layout direction of the view.
+ * This is inherited from UIView+React, but we override it here
+ * to improve perfomance and make subclassing/overriding possible/easier.
+ */
+@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
+
/**
* z-index, used to override sibling order in didUpdateReactSubviews. This is
* inherited from UIView+React, but we override it here to reduce the boxing
diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m
index a832022ed..ba7f84342 100644
--- a/React/Views/RCTView.m
+++ b/React/Views/RCTView.m
@@ -124,6 +124,18 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
+- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
+{
+ _reactLayoutDirection = layoutDirection;
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
+ self.semanticContentAttribute =
+ layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
+ UISemanticContentAttributeForceLeftToRight :
+ UISemanticContentAttributeForceRightToLeft;
+#endif
+}
+
- (NSString *)accessibilityLabel
{
if (super.accessibilityLabel) {
diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h
index c263fb0b6..a5950af7b 100644
--- a/React/Views/UIView+React.h
+++ b/React/Views/UIView+React.h
@@ -23,6 +23,13 @@
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER;
- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER;
+/**
+ * Layout direction of the view.
+ * Internally backed to `semanticContentAttribute` property.
+ * Defaults to `LeftToRight` in case of ambiguity.
+ */
+@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
+
/**
* z-index, used to override sibling order in didUpdateReactSubviews.
*/
diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m
index d637e7efa..84217a805 100644
--- a/React/Views/UIView+React.m
+++ b/React/Views/UIView+React.m
@@ -87,6 +87,27 @@
[subview removeFromSuperview];
}
+- (UIUserInterfaceLayoutDirection)reactLayoutDirection
+{
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
+ return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute];
+#else
+ return [objc_getAssociatedObject(self, @selector(reactLayoutDirection)) integerValue];
+#endif
+}
+
+- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
+{
+#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
+ self.semanticContentAttribute =
+ layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
+ UISemanticContentAttributeForceLeftToRight :
+ UISemanticContentAttributeForceRightToLeft;
+#else
+ objc_setAssociatedObject(self, @selector(reactLayoutDirection), @(layoutDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+#endif
+}
+
- (NSInteger)reactZIndex
{
return [objc_getAssociatedObject(self, _cmd) integerValue];