Skip to content

Commit

Permalink
BREAKING - Fix unconstraint sizing in main axis
Browse files Browse the repository at this point in the history
Summary:
This fixes measuring of items in the main axis of a container. Previously items were in a lot of cases measured with UNSPECIFIED instead of AT_MOST. This was to support scrolling containers. The correct way to handle scrolling containers is to instead provide them with their own overflow value to activate this behavior. This is also similar to how the web works.

This is a breaking change. Most of your layouts will continue to function as before however some of them might not. Typically this is due to having a `flex: 1` style where it is currently a no-op due to being measured with an undefined size but after this change it may collapse your component to take zero size due to the implicit `flexBasis: 0` now being correctly treated. Removing the bad `flex: 1` style or changing it to `flexGrow: 1` should solve most if not all layout issues your see after this diff.

Reviewed By: majak

Differential Revision: D3876927

fbshipit-source-id: 81ea1c9d6574dd4564a3333f1b3617cf84b4022f
  • Loading branch information
Emil Sjolander authored and Facebook Github Bot 6 committed Sep 26, 2016
1 parent 295ee16 commit 0a9b6be
Show file tree
Hide file tree
Showing 15 changed files with 90 additions and 36 deletions.
8 changes: 5 additions & 3 deletions Libraries/Components/ScrollView/ScrollView.js
Expand Up @@ -529,7 +529,7 @@ const ScrollView = React.createClass({
return React.cloneElement(
refreshControl,
{style: props.style},
<ScrollViewClass {...props} style={baseStyle} ref={this._setScrollViewRef}>
<ScrollViewClass {...props} ref={this._setScrollViewRef}>
{contentContainer}
</ScrollViewClass>
);
Expand All @@ -545,12 +545,14 @@ const ScrollView = React.createClass({

const styles = StyleSheet.create({
baseVertical: {
flex: 1,
flexGrow: 1,
flexDirection: 'column',
overflow: 'scroll',
},
baseHorizontal: {
flex: 1,
flexGrow: 1,
flexDirection: 'row',
overflow: 'scroll',
},
contentContainerHorizontal: {
flexDirection: 'row',
Expand Down
1 change: 0 additions & 1 deletion Libraries/Components/View/ViewStylePropTypes.js
Expand Up @@ -43,7 +43,6 @@ var ViewStylePropTypes = {
borderBottomWidth: ReactPropTypes.number,
borderLeftWidth: ReactPropTypes.number,
opacity: ReactPropTypes.number,
overflow: ReactPropTypes.oneOf(['visible', 'hidden']),
/**
* (Android-only) Sets the elevation of a view, using Android's underlying
* [elevation API](https://developer.android.com/training/material/shadows-clipping.html#Elevation).
Expand Down
13 changes: 13 additions & 0 deletions Libraries/StyleSheet/LayoutPropTypes.js
Expand Up @@ -328,6 +328,19 @@ var LayoutPropTypes = {
'stretch'
]),

/** `overflow` controls how a children are measured and displayed.
* `overflow: hidden` causes views to be clipped while `overflow: scroll`
* causes views to be measured independently of their parents main axis.`
* It works like `overflow` in CSS (default: visible).
* See https://developer.mozilla.org/en/docs/Web/CSS/overflow
* for more details.
*/
overflow: ReactPropTypes.oneOf([
'visible',
'hidden',
'scroll',
]),

/** In React Native `flex` does not work the same way that it does in CSS.
* `flex` is a number rather than a string, and it works
* according to the `css-layout` library
Expand Down
3 changes: 2 additions & 1 deletion React/Base/RCTConvert.m
Expand Up @@ -612,7 +612,8 @@ + (NSPropertyList)NSPropertyList:(id)json

RCT_ENUM_CONVERTER(CSSOverflow, (@{
@"hidden": @(CSSOverflowHidden),
@"visible": @(CSSOverflowVisible)
@"visible": @(CSSOverflowVisible),
@"scroll": @(CSSOverflowScroll),
}), CSSOverflowVisible, intValue)

RCT_ENUM_CONVERTER(CSSFlexDirection, (@{
Expand Down
34 changes: 19 additions & 15 deletions React/CSSLayout/CSSLayout.c
Expand Up @@ -183,6 +183,7 @@ void CSSNodeInit(const CSSNodeRef node) {
// Such that the comparison is always going to be false
node->layout.lastParentDirection = (CSSDirection) -1;
node->layout.nextCachedMeasurementsIndex = 0;
node->layout.computedFlexBasis = CSSUndefined;

node->layout.measuredDimensions[CSSDimensionWidth] = CSSUndefined;
node->layout.measuredDimensions[CSSDimensionHeight] = CSSUndefined;
Expand All @@ -193,6 +194,7 @@ void CSSNodeInit(const CSSNodeRef node) {
void _CSSNodeMarkDirty(const CSSNodeRef node) {
if (!node->isDirty) {
node->isDirty = true;
node->layout.computedFlexBasis = CSSUndefined;
if (node->parent) {
_CSSNodeMarkDirty(node->parent);
}
Expand Down Expand Up @@ -451,6 +453,8 @@ _CSSNodePrint(const CSSNodeRef node, const CSSPrintOptions options, const uint32
printf("overflow: 'hidden', ");
} else if (node->style.overflow == CSSOverflowVisible) {
printf("overflow: 'visible', ");
} else if (node->style.overflow == CSSOverflowScroll) {
printf("overflow: 'scroll', ");
}

if (eqFour(node->style.margin)) {
Expand Down Expand Up @@ -1117,8 +1121,10 @@ static void layoutNodeImpl(const CSSNodeRef node,
getPaddingAndBorderAxis(child, CSSFlexDirectionColumn));
} else if (!CSSValueIsUndefined(child->style.flexBasis) &&
!CSSValueIsUndefined(availableInnerMainDim)) {
child->layout.computedFlexBasis =
fmaxf(child->style.flexBasis, getPaddingAndBorderAxis(child, mainAxis));
if (CSSValueIsUndefined(child->layout.computedFlexBasis)) {
child->layout.computedFlexBasis =
fmaxf(child->style.flexBasis, getPaddingAndBorderAxis(child, mainAxis));
}
} else {
// Compute the flex basis and hypothetical main size (i.e. the clamped
// flex basis).
Expand All @@ -1138,21 +1144,19 @@ static void layoutNodeImpl(const CSSNodeRef node,
childHeightMeasureMode = CSSMeasureModeExactly;
}

// According to the spec, if the main size is not definite and the
// child's inline axis is parallel to the main axis (i.e. it's
// horizontal), the child should be sized using "UNDEFINED" in
// the main size. Otherwise use "AT_MOST" in the cross axis.
if (!isMainAxisRow && CSSValueIsUndefined(childWidth) &&
!CSSValueIsUndefined(availableInnerWidth)) {
childWidth = availableInnerWidth;
childWidthMeasureMode = CSSMeasureModeAtMost;
}

// The W3C spec doesn't say anything about the 'overflow' property,
// but all major browsers appear to implement the following logic.
if (node->style.overflow == CSSOverflowHidden) {
if (isMainAxisRow && CSSValueIsUndefined(childHeight) &&
!CSSValueIsUndefined(availableInnerHeight)) {
if ((!isMainAxisRow && node->style.overflow == CSSOverflowScroll) ||
node->style.overflow != CSSOverflowScroll) {
if (CSSValueIsUndefined(childWidth) && !CSSValueIsUndefined(availableInnerWidth)) {
childWidth = availableInnerWidth;
childWidthMeasureMode = CSSMeasureModeAtMost;
}
}

if ((isMainAxisRow && node->style.overflow == CSSOverflowScroll) ||
node->style.overflow != CSSOverflowScroll) {
if (CSSValueIsUndefined(childHeight) && !CSSValueIsUndefined(availableInnerHeight)) {
childHeight = availableInnerHeight;
childHeightMeasureMode = CSSMeasureModeAtMost;
}
Expand Down
1 change: 1 addition & 0 deletions React/CSSLayout/CSSLayout.h
Expand Up @@ -55,6 +55,7 @@ typedef enum CSSJustify {
typedef enum CSSOverflow {
CSSOverflowVisible,
CSSOverflowHidden,
CSSOverflowScroll,
} CSSOverflow;

// Note: auto is only a valid value for alignSelf. It is NOT a valid value for
Expand Down
10 changes: 10 additions & 0 deletions React/Views/RCTScrollViewManager.m
Expand Up @@ -11,6 +11,7 @@

#import "RCTBridge.h"
#import "RCTScrollView.h"
#import "RCTShadowView.h"
#import "RCTUIManager.h"

@interface RCTScrollView (Private)
Expand Down Expand Up @@ -79,6 +80,15 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onScrollAnimationEnd, RCTDirectEventBlock)

// overflow is used both in css-layout as well as by reac-native. In css-layout
// we always want to treat overflow as scroll but depending on what the overflow
// is set to from js we want to clip drawing or not. This piece of code ensures
// that css-layout is always treating the contents of a scroll container as
// overflow: 'scroll'.
RCT_CUSTOM_SHADOW_PROPERTY(overflow, CSSOverflow, RCTShadowView) {
view.overflow = CSSOverflowScroll;
}

RCT_EXPORT_METHOD(getContentSize:(nonnull NSNumber *)reactTag
callback:(RCTResponseSenderBlock)callback)
{
Expand Down
15 changes: 15 additions & 0 deletions React/Views/RCTViewManager.h
Expand Up @@ -117,4 +117,19 @@ RCT_REMAP_VIEW_PROPERTY(name, __custom__, type) \
#define RCT_EXPORT_SHADOW_PROPERTY(name, type) \
+ (NSArray<NSString *> *)propConfigShadow_##name { return @[@#type]; }

/**
* This macro maps a named property to an arbitrary key path in the shadow view.
*/
#define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \
+ (NSArray<NSString *> *)propConfigShadow_##name { return @[@#type, @#keyPath]; }

/**
* This macro can be used when you need to provide custom logic for setting
* shadow view properties. The macro should be followed by a method body, which can
* refer to "json", "view" and "defaultView" to implement the required logic.
*/
#define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \
RCT_REMAP_SHADOW_PROPERTY(name, __custom__, type) \
- (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView

@end
2 changes: 1 addition & 1 deletion React/Views/RCTViewManager.m
Expand Up @@ -122,7 +122,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio
RCT_CUSTOM_VIEW_PROPERTY(overflow, CSSOverflow, RCTView)
{
if (json) {
view.clipsToBounds = [RCTConvert CSSOverflow:json] == CSSOverflowHidden;
view.clipsToBounds = [RCTConvert CSSOverflow:json] != CSSOverflowVisible;
} else {
view.clipsToBounds = defaultView.clipsToBounds;
}
Expand Down
Expand Up @@ -51,7 +51,7 @@ public void resetResult() {
Arrays.fill(dimensions, CSSConstants.UNDEFINED);
direction = CSSDirection.LTR;

computedFlexBasis = 0;
computedFlexBasis = CSSConstants.UNDEFINED;

generationCount = 0;
lastParentDirection = null;
Expand Down
Expand Up @@ -181,6 +181,7 @@ public void dirty() {
}

mLayoutState = LayoutState.DIRTY;
layout.computedFlexBasis = CSSConstants.UNDEFINED;

if (mParent != null) {
mParent.dirty();
Expand Down
Expand Up @@ -12,4 +12,5 @@
public enum CSSOverflow {
VISIBLE,
HIDDEN,
SCROLL,
}
26 changes: 12 additions & 14 deletions ReactAndroid/src/main/java/com/facebook/csslayout/LayoutEngine.java
Expand Up @@ -695,9 +695,9 @@ private static void layoutNodeImpl(
// The height is definite, so use that as the flex basis.
child.layout.computedFlexBasis = Math.max(child.style.dimensions[DIMENSION_HEIGHT], ((child.style.padding.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN]) + child.style.border.getWithFallback(leadingSpacing[CSS_FLEX_DIRECTION_COLUMN], leading[CSS_FLEX_DIRECTION_COLUMN])) + (child.style.padding.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]) + child.style.border.getWithFallback(trailingSpacing[CSS_FLEX_DIRECTION_COLUMN], trailing[CSS_FLEX_DIRECTION_COLUMN]))));
} else if (!isFlexBasisAuto(child) && !Float.isNaN(availableInnerMainDim)) {

// If the basis isn't 'auto', it is assumed to be zero.
child.layout.computedFlexBasis = Math.max(child.style.flexBasis, ((child.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (child.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + child.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))));
if (Float.isNaN(child.layout.computedFlexBasis)) {
child.layout.computedFlexBasis = Math.max(child.style.flexBasis, ((child.style.padding.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis]) + child.style.border.getWithFallback(leadingSpacing[mainAxis], leading[mainAxis])) + (child.style.padding.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]) + child.style.border.getWithFallback(trailingSpacing[mainAxis], trailing[mainAxis]))));
}
} else {

// Compute the flex basis and hypothetical main size (i.e. the clamped flex basis).
Expand All @@ -715,19 +715,17 @@ private static void layoutNodeImpl(
childHeightMeasureMode = CSSMeasureMode.EXACTLY;
}

// According to the spec, if the main size is not definite and the
// child's inline axis is parallel to the main axis (i.e. it's
// horizontal), the child should be sized using "UNDEFINED" in
// the main size. Otherwise use "AT_MOST" in the cross axis.
if (!isMainAxisRow && Float.isNaN(childWidth) && !Float.isNaN(availableInnerWidth)) {
childWidth = availableInnerWidth;
childWidthMeasureMode = CSSMeasureMode.AT_MOST;
}

// The W3C spec doesn't say anything about the 'overflow' property,
// but all major browsers appear to implement the following logic.
if (node.style.overflow == CSSOverflow.HIDDEN) {
if (isMainAxisRow && Float.isNaN(childHeight) && !Float.isNaN(availableInnerHeight)) {
if ((!isMainAxisRow && node.style.overflow == CSSOverflow.SCROLL) || node.style.overflow != CSSOverflow.SCROLL) {
if (Float.isNaN(childWidth) && !Float.isNaN(availableInnerWidth)) {
childWidth = availableInnerWidth;
childWidthMeasureMode = CSSMeasureMode.AT_MOST;
}
}

if ((isMainAxisRow && node.style.overflow == CSSOverflow.SCROLL) || node.style.overflow != CSSOverflow.SCROLL) {
if (Float.isNaN(childHeight) && !Float.isNaN(availableInnerHeight)) {
childHeight = availableInnerHeight;
childHeightMeasureMode = CSSMeasureMode.AT_MOST;
}
Expand Down
Expand Up @@ -10,6 +10,7 @@
import com.facebook.csslayout.CSSConstants;
import com.facebook.csslayout.CSSFlexDirection;
import com.facebook.csslayout.CSSJustify;
import com.facebook.csslayout.CSSOverflow;
import com.facebook.csslayout.CSSPositionType;
import com.facebook.csslayout.CSSWrap;
import com.facebook.react.uimanager.annotations.ReactProp;
Expand Down Expand Up @@ -102,6 +103,12 @@ public void setJustifyContent(@Nullable String justifyContent) {
justifyContent.toUpperCase(Locale.US).replace("-", "_")));
}

@ReactProp(name = ViewProps.OVERFLOW)
public void setOverflow(@Nullable String overflow) {
setOverflow(overflow == null ? CSSOverflow.VISIBLE : CSSOverflow.valueOf(
overflow.toUpperCase(Locale.US).replace("-", "_")));
}

@ReactPropGroup(names = {
ViewProps.MARGIN,
ViewProps.MARGIN_VERTICAL,
Expand Down
Expand Up @@ -26,6 +26,7 @@ public class ViewProps {
// !!! Keep in sync with LAYOUT_ONLY_PROPS below
public static final String ALIGN_ITEMS = "alignItems";
public static final String ALIGN_SELF = "alignSelf";
public static final String OVERFLOW = "overflow";
public static final String BOTTOM = "bottom";
public static final String COLLAPSABLE = "collapsable";
public static final String FLEX = "flex";
Expand Down Expand Up @@ -113,6 +114,7 @@ public class ViewProps {
FLEX_DIRECTION,
FLEX_WRAP,
JUSTIFY_CONTENT,
OVERFLOW,

/* position */
POSITION,
Expand Down

1 comment on commit 0a9b6be

@obibring
Copy link

@obibring obibring commented on 0a9b6be Feb 15, 2017

Choose a reason for hiding this comment

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

@emilsjolander, can you provide a more detailed explanation on what the behavior was before, and what it is now? I just upgraded react-native and am unable to fix my layout as simply as suggested (and it's driving me crazy). I'm sure there are many others that have the same question. Thanks

Please sign in to comment.