From a9e44d4643f89ada886c80aa8560597799fdee63 Mon Sep 17 00:00:00 2001 From: Nishan Date: Thu, 20 Oct 2022 14:53:32 -0700 Subject: [PATCH] feat: flex gap bindings (#34974) Summary: This PR adds React native binding for https://github.com/facebook/yoga/pull/1116 ## Changelog [General] [Added] - Flex gap yoga bindings Pull Request resolved: https://github.com/facebook/react-native/pull/34974 Test Plan: Run rn tester app and go to view example. You'll find a flex gap example. Example location - `packages/rn-tester/js/examples/View/ViewExample.js` ### Tested on - [x] iOS Fabric - [x] iOS non-fabric - [x] Android Fabric - [x] Android non-fabric To test on non-fabric Android, I just switched these booleans. Let me know if there's anything else I might have missed. Screenshot 2022-10-14 at 3 30 48 AM Reviewed By: mdvacca Differential Revision: D40527474 Pulled By: NickGerleman fbshipit-source-id: 81c2c97c76f58fad3bb40fb378aaf8b6ebd30d63 --- .../View/ReactNativeStyleAttributes.js | 3 ++ .../NativeComponent/BaseViewConfig.android.js | 3 ++ .../NativeComponent/BaseViewConfig.ios.js | 3 ++ Libraries/StyleSheet/StyleSheetTypes.d.ts | 3 ++ Libraries/StyleSheet/StyleSheetTypes.js | 13 ++++++ Libraries/StyleSheet/splitLayoutProps.js | 3 ++ React/Views/RCTShadowView.h | 3 ++ React/Views/RCTShadowView.m | 14 +++++++ React/Views/RCTViewManager.m | 3 ++ .../react/uimanager/LayoutShadowNode.java | 24 +++++++++++ .../react/uimanager/ReactShadowNode.java | 6 +++ .../react/uimanager/ReactShadowNodeImpl.java | 16 ++++++++ .../facebook/react/uimanager/ViewProps.java | 6 +++ .../components/view/YogaStylableProps.cpp | 18 +++++++++ .../components/view/propsConversions.h | 22 ++++++++++ .../react/test_utils/shadowTreeGeneration.h | 6 +++ .../rn-tester/js/examples/View/ViewExample.js | 40 +++++++++++++++++++ 17 files changed, 186 insertions(+) diff --git a/Libraries/Components/View/ReactNativeStyleAttributes.js b/Libraries/Components/View/ReactNativeStyleAttributes.js index 2511ad4755d851..926c0c7efa6441 100644 --- a/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -32,6 +32,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderRightWidth: true, borderStartWidth: true, borderTopWidth: true, + columnGap: true, borderWidth: true, bottom: true, direction: true, @@ -43,6 +44,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { flexGrow: true, flexShrink: true, flexWrap: true, + gap: true, height: true, justifyContent: true, left: true, @@ -71,6 +73,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { paddingVertical: true, position: true, right: true, + rowGap: true, start: true, top: true, width: true, diff --git a/Libraries/NativeComponent/BaseViewConfig.android.js b/Libraries/NativeComponent/BaseViewConfig.android.js index bb2908a74b54c8..860e83d9b5c57d 100644 --- a/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/Libraries/NativeComponent/BaseViewConfig.android.js @@ -197,6 +197,9 @@ const validAttributesForNonEventProps = { maxHeight: true, flex: true, flexGrow: true, + rowGap: true, + columnGap: true, + gap: true, flexShrink: true, flexBasis: true, aspectRatio: true, diff --git a/Libraries/NativeComponent/BaseViewConfig.ios.js b/Libraries/NativeComponent/BaseViewConfig.ios.js index 5ceb5bafade92f..093588c9c7b266 100644 --- a/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -271,6 +271,9 @@ const validAttributesForNonEventProps = { flex: true, flexGrow: true, + rowGap: true, + columnGap: true, + gap: true, flexShrink: true, flexBasis: true, flexDirection: true, diff --git a/Libraries/StyleSheet/StyleSheetTypes.d.ts b/Libraries/StyleSheet/StyleSheetTypes.d.ts index 1df286e2141cc5..5e0b8d8e1ab309 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -52,6 +52,9 @@ export interface FlexStyle { | 'row-reverse' | 'column-reverse' | undefined; + rowGap?: number | undefined; + gap?: number | undefined; + columnGap?: number | undefined; flexGrow?: number | undefined; flexShrink?: number | undefined; flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse' | undefined; diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 5099d49f313d47..14cf6d7dc9471a 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -518,6 +518,19 @@ export type ____ShadowStyle_InternalCore = $ReadOnly<{ * @platform ios */ shadowRadius?: number, + + /** + * In React Native, gap works the same way it does in CSS. + * If there are two or more children in a container, they will be separated from each other + * by the value of the gap - but the children will not be separated from the edges of their parent container. + * For horizontal gaps, use columnGap, for vertical gaps, use rowGap, and to apply both at the same time, it's gap. + * When align-content or justify-content are set to space-between or space-around, the separation + * between children may be larger than the gap value. + * See https://developer.mozilla.org/en-US/docs/Web/CSS/gap for more details. + */ + rowGap?: number, + columnGap?: number, + gap?: number, }>; export type ____ShadowStyle_Internal = $ReadOnly<{ diff --git a/Libraries/StyleSheet/splitLayoutProps.js b/Libraries/StyleSheet/splitLayoutProps.js index 9fdf9e86c9e061..b92f86d3ad2559 100644 --- a/Libraries/StyleSheet/splitLayoutProps.js +++ b/Libraries/StyleSheet/splitLayoutProps.js @@ -49,6 +49,9 @@ export default function splitLayoutProps(props: ?____ViewStyle_Internal): { case 'bottom': case 'top': case 'transform': + case 'rowGap': + case 'columnGap': + case 'gap': // $FlowFixMe[cannot-write] // $FlowFixMe[incompatible-use] // $FlowFixMe[prop-missing] diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 226672fb37635a..4cf8323be55701 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -152,6 +152,9 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry @property (nonatomic, assign) float flex; @property (nonatomic, assign) float flexGrow; +@property (nonatomic, assign) float rowGap; +@property (nonatomic, assign) float columnGap; +@property (nonatomic, assign) float gap; @property (nonatomic, assign) float flexShrink; @property (nonatomic, assign) YGValue flexBasis; diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 016060a226c465..baca3cef65e4bf 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -642,6 +642,20 @@ - (YGValue)flexBasis return YGNodeStyleGetFlexBasis(_yogaNode); } +#define RCT_GAP_PROPERTY(setProp, getProp, cssProp, type, gap) \ + -(void)set##setProp : (type)value \ + { \ + YGNodeStyleSet##cssProp(_yogaNode, gap, value); \ + } \ + -(type)getProp \ + { \ + return YGNodeStyleGet##cssProp(_yogaNode, gap); \ + } + +RCT_GAP_PROPERTY(RowGap, rowGap, Gap, float, YGGutterRow); +RCT_GAP_PROPERTY(ColumnGap, columnGap, Gap, float, YGGutterColumn); +RCT_GAP_PROPERTY(Gap, gap, Gap, float, YGGutterAll); + #define RCT_STYLE_PROPERTY(setProp, getProp, cssProp, type) \ -(void)set##setProp : (type)value \ { \ diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index f1bb13eb7cfa39..a777d5f8271736 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -426,6 +426,9 @@ - (RCTShadowView *)shadowView RCT_EXPORT_SHADOW_PROPERTY(alignContent, YGAlign) RCT_EXPORT_SHADOW_PROPERTY(position, YGPositionType) RCT_EXPORT_SHADOW_PROPERTY(aspectRatio, float) +RCT_EXPORT_SHADOW_PROPERTY(rowGap, float) +RCT_EXPORT_SHADOW_PROPERTY(columnGap, float) +RCT_EXPORT_SHADOW_PROPERTY(gap, float) RCT_EXPORT_SHADOW_PROPERTY(overflow, YGOverflow) RCT_EXPORT_SHADOW_PROPERTY(display, YGDisplay) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java index f5a7d8a9ca603a..bfcf2be62e5366 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java @@ -224,6 +224,30 @@ public void setFlexGrow(float flexGrow) { super.setFlexGrow(flexGrow); } + @ReactProp(name = ViewProps.ROW_GAP, defaultFloat = YogaConstants.UNDEFINED) + public void setRowGap(float rowGap) { + if (isVirtual()) { + return; + } + super.setRowGap(PixelUtil.toPixelFromDIP(rowGap)); + } + + @ReactProp(name = ViewProps.COLUMN_GAP, defaultFloat = YogaConstants.UNDEFINED) + public void setColumnGap(float columnGap) { + if (isVirtual()) { + return; + } + super.setColumnGap(PixelUtil.toPixelFromDIP(columnGap)); + } + + @ReactProp(name = ViewProps.GAP, defaultFloat = YogaConstants.UNDEFINED) + public void setGap(float gap) { + if (isVirtual()) { + return; + } + super.setGap(PixelUtil.toPixelFromDIP(gap)); + } + @ReactProp(name = ViewProps.FLEX_SHRINK, defaultFloat = 0f) public void setFlexShrink(float flexShrink) { if (isVirtual()) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java index 18da96c97c8462..db10bf379e46ba 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -308,6 +308,12 @@ public interface ReactShadowNode { void setFlexGrow(float flexGrow); + void setRowGap(float rowGap); + + void setColumnGap(float columnGap); + + void setGap(float gap); + void setFlexShrink(float flexShrink); void setFlexBasis(float flexBasis); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java index ad64a3a120a1f7..f62e6085ae459f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNodeImpl.java @@ -18,6 +18,7 @@ import com.facebook.yoga.YogaDisplay; import com.facebook.yoga.YogaEdge; import com.facebook.yoga.YogaFlexDirection; +import com.facebook.yoga.YogaGutter; import com.facebook.yoga.YogaJustify; import com.facebook.yoga.YogaMeasureFunction; import com.facebook.yoga.YogaNode; @@ -794,6 +795,21 @@ public void setFlexGrow(float flexGrow) { mYogaNode.setFlexGrow(flexGrow); } + @Override + public void setRowGap(float rowGap) { + mYogaNode.setGap(YogaGutter.ROW, rowGap); + } + + @Override + public void setColumnGap(float columnGap) { + mYogaNode.setGap(YogaGutter.COLUMN, columnGap); + } + + @Override + public void setGap(float gap) { + mYogaNode.setGap(YogaGutter.ALL, gap); + } + @Override public void setFlexShrink(float flexShrink) { mYogaNode.setFlexShrink(flexShrink); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 5ebc907f81930b..462fd25ee28949 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -32,6 +32,9 @@ public class ViewProps { public static final String FLEX_BASIS = "flexBasis"; public static final String FLEX_DIRECTION = "flexDirection"; public static final String FLEX_WRAP = "flexWrap"; + public static final String ROW_GAP = "rowGap"; + public static final String COLUMN_GAP = "columnGap"; + public static final String GAP = "gap"; public static final String HEIGHT = "height"; public static final String JUSTIFY_CONTENT = "justifyContent"; public static final String LEFT = "left"; @@ -203,6 +206,9 @@ public class ViewProps { FLEX_BASIS, FLEX_DIRECTION, FLEX_GROW, + ROW_GAP, + COLUMN_GAP, + GAP, FLEX_SHRINK, FLEX_WRAP, JUSTIFY_CONTENT, diff --git a/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp b/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp index 85f6c5f405daa6..f03dc21c55fb6f 100644 --- a/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp +++ b/ReactCommon/react/renderer/components/view/YogaStylableProps.cpp @@ -65,6 +65,11 @@ static inline T const getFieldValue( REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, YGDimensionWidth, widthStr); \ REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, YGDimensionHeight, heightStr); +#define REBUILD_FIELD_YG_GUTTER(field, rowGapStr, columnGapStr, gapStr) \ + REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, YGGutterRow, rowGapStr); \ + REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, YGGutterColumn, columnGapStr); \ + REBUILD_YG_FIELD_SWITCH_CASE_INDEXED(field, YGGutterAll, gapStr); + #define REBUILD_FIELD_YG_EDGES(field, prefix, suffix) \ REBUILD_YG_FIELD_SWITCH_CASE_INDEXED( \ field, YGEdgeLeft, prefix "Left" suffix); \ @@ -114,6 +119,7 @@ void YogaStylableProps::setProp( REBUILD_FIELD_SWITCH_CASE_YSP(flexShrink); REBUILD_FIELD_SWITCH_CASE_YSP(flexBasis); REBUILD_FIELD_SWITCH_CASE2(positionType, "position"); + REBUILD_FIELD_YG_GUTTER(gap, "rowGap", "columnGap", "gap"); REBUILD_FIELD_SWITCH_CASE_YSP(aspectRatio); REBUILD_FIELD_YG_DIMENSION(dimensions, "width", "height"); REBUILD_FIELD_YG_DIMENSION(minDimensions, "minWidth", "minHeight"); @@ -163,6 +169,18 @@ SharedDebugStringConvertibleList YogaStylableProps::getDebugProps() const { "flex", yogaStyle.flex(), defaultYogaStyle.flex()), debugStringConvertibleItem( "flexGrow", yogaStyle.flexGrow(), defaultYogaStyle.flexGrow()), + debugStringConvertibleItem( + "rowGap", + yogaStyle.gap()[YGGutterRow], + defaultYogaStyle.gap()[YGGutterRow]), + debugStringConvertibleItem( + "columnGap", + yogaStyle.gap()[YGGutterColumn], + defaultYogaStyle.gap()[YGGutterColumn]), + debugStringConvertibleItem( + "gap", + yogaStyle.gap()[YGGutterAll], + defaultYogaStyle.gap()[YGGutterAll]), debugStringConvertibleItem( "flexShrink", yogaStyle.flexShrink(), defaultYogaStyle.flexShrink()), debugStringConvertibleItem( diff --git a/ReactCommon/react/renderer/components/view/propsConversions.h b/ReactCommon/react/renderer/components/view/propsConversions.h index 1bf357a4a79f84..3973637eaec9e8 100644 --- a/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/ReactCommon/react/renderer/components/view/propsConversions.h @@ -266,6 +266,28 @@ static inline YGStyle convertRawProp( "", sourceValue.padding(), yogaStyle.padding()); + + yogaStyle.gap()[YGGutterRow] = convertRawProp( + context, + rawProps, + "rowGap", + sourceValue.gap()[YGGutterRow], + yogaStyle.gap()[YGGutterRow]); + + yogaStyle.gap()[YGGutterColumn] = convertRawProp( + context, + rawProps, + "columnGap", + sourceValue.gap()[YGGutterColumn], + yogaStyle.gap()[YGGutterColumn]); + + yogaStyle.gap()[YGGutterAll] = convertRawProp( + context, + rawProps, + "gap", + sourceValue.gap()[YGGutterAll], + yogaStyle.gap()[YGGutterAll]); + yogaStyle.border() = convertRawProp( context, rawProps, diff --git a/ReactCommon/react/test_utils/shadowTreeGeneration.h b/ReactCommon/react/test_utils/shadowTreeGeneration.h index 63f037b9b4930b..0734c6e0bed909 100644 --- a/ReactCommon/react/test_utils/shadowTreeGeneration.h +++ b/ReactCommon/react/test_utils/shadowTreeGeneration.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -233,6 +234,11 @@ static inline ShadowNode::Unshared messWithYogaStyles( "maxWidth", "maxHeight", "minWidth", "minHeight", }; + // It is not safe to add new Yoga properties to this list. Unit tests + // validate specific seeds, and what they test may change and cause unrelated + // failures if the size of properties also changes. + EXPECT_EQ(properties.size(), 20); + for (auto const &property : properties) { if (entropy.random(0.1)) { dynamic[property] = entropy.random(0, 1024); diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index 44634d2aa1d088..7a073a3a491697 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -299,6 +299,40 @@ class DisplayNoneStyle extends React.Component< this.setState({index: this.state.index + 1}); }; } + +class FlexGapExample extends React.Component<$ReadOnly<{||}>> { + render(): React.Node { + return ( + + + + + + + + + + + + + + ); + } +} + exports.title = 'View'; exports.documentationURL = 'https://reactnative.dev/docs/view'; exports.category = 'Basic'; @@ -612,4 +646,10 @@ exports.examples = [ ); }, }, + { + title: 'FlexGap', + render(): React.Node { + return ; + }, + }, ];