From f965da97f3cc45e34fd946ea34181dcaeba03f05 Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Mon, 7 Aug 2023 19:37:41 -0700 Subject: [PATCH] Add `experimental_layoutConformance` prop (#38746) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38746 This adds a view prop which controls the layout conformance of it and its transitive children (including non-view ShadowNodes). The implementation here is to traverse down before layout, updating any context-specific configuration. Normally at the time of layout, unchanged child ShadowNodes are already sealed. These must be mutated already if layout results change, and we do this today via Yoga node clone callback, where Yoga clones nodes where it will do layout work (but not all of them). We down-propagate configuration, but can terminate if we observe an already sealed node whose values match those of our current context, since any nodes under must also follow the same contexts (or any nested contexts of the sealed node). This means that calling `layoutTree()` on a mostly clean ShadowTree will cause configuration to not traverse most nodes. `Errata` and `PointScaleFactor` (DPI) are threaded to each Node's per-node config. The same values are retained when Yoga nodes are cloned (by cloning the ShadowNode). Since these are set before the first layout, config setting should only ever dirty layout [when layout-effecting values change](https://www.internalfb.com/code/fbsource/[1c95e981c740]/xplat/yoga/yoga/YGConfig.cpp?lines=13). The prop is prefixed with `experimental` because we are likely to make more breaking conformance fixes in the strict mode, and I'm not sure this is the final API yet. I was previously looking at a context-like component which is more challenging to implement. Changelog: [Internal] bypass-github-export-checks Reviewed By: rozele, sammy-SC Differential Revision: D47940100 fbshipit-source-id: f72e1c73f537e0312c2bba19ae8c2e882c82aced --- .../Components/View/ViewNativeComponent.js | 1 + .../Components/View/ViewPropTypes.js | 9 + .../NativeComponent/BaseViewConfig.android.js | 2 + .../NativeComponent/BaseViewConfig.ios.js | 2 + .../react-native/React/Views/RCTViewManager.m | 7 + .../react/views/view/ReactViewManager.java | 6 + .../components/view/BaseViewProps.cpp | 12 +- .../renderer/components/view/BaseViewProps.h | 2 + .../view/YogaLayoutableShadowNode.cpp | 183 ++++++++++++------ .../view/YogaLayoutableShadowNode.h | 32 ++- .../renderer/components/view/conversions.h | 33 ++++ .../renderer/components/view/primitives.h | 2 + .../react/renderer/core/LayoutMetrics.h | 4 + packages/react-native/types/experimental.d.ts | 9 + .../rn-tester/js/examples/View/ViewExample.js | 50 +++++ 15 files changed, 287 insertions(+), 67 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js index d8052fb9889..df119f6c5ed 100644 --- a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js @@ -98,6 +98,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = focusable: true, overflow: true, backfaceVisibility: true, + experimental_layoutConformance: true, }, } : { diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 501eb311a7c..1ead9ba053a 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -568,6 +568,15 @@ export type ViewProps = $ReadOnly<{| */ collapsable?: ?boolean, + /** + * Contols whether this view, and its transitive children, are laid in a way + * consistent with web browsers ('strict'), or consistent with existing + * React Native code which may rely on incorrect behavior ('classic'). + * + * This prop only works when using Fabric. + */ + experimental_layoutConformance?: ?('strict' | 'classic'), + /** * Used to locate this view from native classes. Has precedence over `nativeID` prop. * diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index f0dfd8d6628..249e81199a5 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -273,6 +273,8 @@ const validAttributesForNonEventProps = { position: true, style: ReactNativeStyleAttributes, + + experimental_layoutConformance: true, }; // Props for bubbling and direct events diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index 92b2959aad1..61628802a85 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -335,6 +335,8 @@ const validAttributesForNonEventProps = { direction: true, style: ReactNativeStyleAttributes, + + experimental_layoutConformance: true, }; // Props for bubbling and direct events diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index eb3ff9c00d8..a88064337cf 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -398,6 +398,13 @@ RCT_CUSTOM_VIEW_PROPERTY(collapsable, BOOL, RCTView) // filtered by view configs. } +RCT_CUSTOM_VIEW_PROPERTY(experimental_layoutConformance, NSString *, RCTView) +{ + // Property is only to be used in the new renderer. + // It is necessary to add it here, otherwise it gets + // filtered by view configs. +} + #define RCT_VIEW_BORDER_PROPERTY(SIDE) \ RCT_CUSTOM_VIEW_PROPERTY(border##SIDE##Width, float, RCTView) \ { \ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 4b3d624907d..a30e31d0f79 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -151,6 +151,12 @@ public class ReactViewManager extends ReactClippingViewManager { view.setBorderStyle(borderStyle); } + // This is unused by the view manager, and not wired to be sent to Java, but + // must be present for the prop to show up in the view config. + @ReactProp(name = "experimental_layoutConformance") + public void setexperimental_layoutConformance( + ReactViewGroup view, @Nullable String layoutConformance) {} + @ReactProp(name = "hitSlop") public void setHitSlop(final ReactViewGroup view, Dynamic hitSlop) { switch (hitSlop.getType()) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 2f0f22023b2..97f3caf2b89 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -190,7 +190,16 @@ BaseViewProps::BaseViewProps( rawProps, "removeClippedSubviews", sourceProps.removeClippedSubviews, - false)) {} + false)), + experimental_layoutConformance( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.experimental_layoutConformance + : convertRawProp( + context, + rawProps, + "experimental_layoutConformance", + sourceProps.experimental_layoutConformance, + {})) {} #define VIEW_EVENT_CASE(eventType) \ case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ @@ -233,6 +242,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout); RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); + RAW_SET_PROP_SWITCH_CASE_BASIC(experimental_layoutConformance); // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 706620f7b5a..9d2530e7061 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -70,6 +70,8 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { bool removeClippedSubviews{false}; + LayoutConformance experimental_layoutConformance{}; + Float elevation{}; /* Android-only */ #pragma mark - Convenience Methods diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index 12c8ebd1255..6d0a2f4807a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -115,9 +115,11 @@ YogaLayoutableShadowNode::YogaLayoutableShadowNode( .yogaNode_.isDirty() == yogaNode_.isDirty() && "Yoga node must inherit dirty flag."); - for (auto &child : getChildren()) { - if (auto layoutableChild = traitCast(child)) { - yogaLayoutableChildren_.push_back(layoutableChild); + if (!getTraits().check(ShadowNodeTraits::Trait::LeafYogaNode)) { + for (auto &child : getChildren()) { + if (auto layoutableChild = traitCast(child)) { + yogaLayoutableChildren_.push_back(layoutableChild); + } } } @@ -138,6 +140,15 @@ YogaLayoutableShadowNode::YogaLayoutableShadowNode( yogaNode_.setDirty(true); } + // We do not need to reconfigure this subtree before the next layout pass if + // the previous node with the same props and children has already been + // configured. + if (!fragment.props && !fragment.children) { + yogaTreeHasBeenConfigured_ = + static_cast(sourceShadowNode) + .yogaTreeHasBeenConfigured_; + } + if (fragment.props) { updateYogaProps(); } @@ -436,6 +447,85 @@ void YogaLayoutableShadowNode::updateYogaProps() { return result; } +void YogaLayoutableShadowNode::configureYogaTree( + float pointScaleFactor, + YGErrata defaultErrata, + bool swapLeftAndRight) { + ensureUnsealed(); + + // Set state on our own Yoga node + YGErrata errata = resolveErrata(defaultErrata); + YGConfigSetErrata(&yogaConfig_, errata); + YGConfigSetPointScaleFactor(&yogaConfig_, pointScaleFactor); + + // TODO: `swapLeftAndRight` modified backing props and cannot be undone + if (swapLeftAndRight) { + swapStyleLeftAndRight(); + } + + yogaTreeHasBeenConfigured_ = true; + + // Recursively propagate the configuration to child nodes. If a child was + // already configured as part of a previous ShadowTree generation, we only + // need to reconfigure it if the context values passed to the Node have + // changed. + for (size_t i = 0; i < yogaLayoutableChildren_.size(); i++) { + const auto &child = *yogaLayoutableChildren_[i]; + auto childLayoutMetrics = child.getLayoutMetrics(); + auto childErrata = + YGConfigGetErrata(const_cast(&child.yogaConfig_)); + + if (child.yogaTreeHasBeenConfigured_ && + childLayoutMetrics.pointScaleFactor == pointScaleFactor && + childLayoutMetrics.wasLeftAndRightSwapped == swapLeftAndRight && + childErrata == child.resolveErrata(errata)) { + continue; + } + + if (doesOwn(child)) { + auto &mutableChild = const_cast(child); + mutableChild.configureYogaTree( + pointScaleFactor, child.resolveErrata(errata), swapLeftAndRight); + } else { + cloneChildInPlace(i).configureYogaTree( + pointScaleFactor, errata, swapLeftAndRight); + } + } +} + +YGErrata YogaLayoutableShadowNode::resolveErrata(YGErrata defaultErrata) const { + if (auto viewShadowNode = traitCast(this)) { + const auto &props = viewShadowNode->getConcreteProps(); + switch (props.experimental_layoutConformance) { + case LayoutConformance::Classic: + return YGErrataAll; + case LayoutConformance::Strict: + return YGErrataNone; + case LayoutConformance::Undefined: + return defaultErrata; + } + } + + return defaultErrata; +} + +YogaLayoutableShadowNode &YogaLayoutableShadowNode::cloneChildInPlace( + int32_t layoutableChildIndex) { + ensureUnsealed(); + + const auto &childNode = *yogaLayoutableChildren_[layoutableChildIndex]; + + // TODO: Why does this not use `ShadowNodeFragment::statePlaceholder()` like + // `adoptYogaChild()`? + auto clonedChildNode = childNode.clone( + {ShadowNodeFragment::propsPlaceholder(), + ShadowNodeFragment::childrenPlaceholder(), + childNode.getState()}); + + replaceChild(childNode, clonedChildNode, layoutableChildIndex); + return static_cast(*clonedChildNode); +} + void YogaLayoutableShadowNode::setSize(Size size) const { ensureUnsealed(); @@ -484,16 +574,19 @@ void YogaLayoutableShadowNode::layoutTree( LayoutConstraints layoutConstraints) { ensureUnsealed(); - /* - * In Yoga, every single Yoga Node has to have a (non-null) pointer to - * Yoga Config (this config can be shared between many nodes), - * so every node can be individually configured. This does *not* mean - * however that Yoga consults with every single Yoga Node Config for every - * config parameter. Especially in case of `pointScaleFactor`, - * the only value in the config of the root node is taken into account - * (and this is by design). - */ - YGConfigSetPointScaleFactor(&yogaConfig_, layoutContext.pointScaleFactor); + SystraceSection s1("YogaLayoutableShadowNode::layoutTree"); + + bool swapLeftAndRight = layoutContext.swapLeftAndRightInRTL && + (layoutConstraints.layoutDirection == LayoutDirection::RightToLeft || + !CoreFeatures::doNotSwapLeftAndRightOnAndroidInLTR); + + { + SystraceSection s2("YogaLayoutableShadowNode::configureYogaTree"); + configureYogaTree( + layoutContext.pointScaleFactor, + YGErrataAll /*defaultErrata*/, + swapLeftAndRight); + } auto minimumSize = layoutConstraints.minimumSize; auto maximumSize = layoutConstraints.maximumSize; @@ -551,25 +644,17 @@ void YogaLayoutableShadowNode::layoutTree( threadLocalLayoutContext = layoutContext; - if (CoreFeatures::doNotSwapLeftAndRightOnAndroidInLTR) { - if (layoutConstraints.layoutDirection == LayoutDirection::RightToLeft && - layoutContext.swapLeftAndRightInRTL) { - swapLeftAndRightInTree(*this); - } - } else { - if (layoutContext.swapLeftAndRightInRTL) { - swapLeftAndRightInTree(*this); - } - } - { - SystraceSection s("YogaLayoutableShadowNode::YGNodeCalculateLayout"); + SystraceSection s3("YogaLayoutableShadowNode::YGNodeCalculateLayout"); YGNodeCalculateLayout(&yogaNode_, ownerWidth, ownerHeight, direction); } + // Update layout metrics for root node. Updated for children in + // YogaLayoutableShadowNode::layout if (yogaNode_.getHasNewLayout()) { auto layoutMetrics = layoutMetricsFromYogaNode(yogaNode_); layoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + layoutMetrics.wasLeftAndRightSwapped = swapLeftAndRight; setLayoutMetrics(layoutMetrics); yogaNode_.setHasNewLayout(false); } @@ -617,6 +702,10 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { auto newLayoutMetrics = layoutMetricsFromYogaNode(*childYogaNode); newLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + newLayoutMetrics.wasLeftAndRightSwapped = + layoutContext.swapLeftAndRightInRTL && + (newLayoutMetrics.layoutDirection == LayoutDirection::RightToLeft || + !CoreFeatures::doNotSwapLeftAndRightOnAndroidInLTR); // Child node's layout has changed. When a node is added to // `affectedNodes`, onLayout event is called on the component. Comparing @@ -683,22 +772,13 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { #pragma mark - Yoga Connectors YGNode *YogaLayoutableShadowNode::yogaNodeCloneCallbackConnector( - YGNode *oldYogaNode, + YGNode * /*oldYogaNode*/, YGNode *parentYogaNode, int childIndex) { SystraceSection s("YogaLayoutableShadowNode::yogaNodeCloneCallbackConnector"); - // At this point it is guaranteed that all shadow nodes associated with yoga - // nodes are `YogaLayoutableShadowNode` subclasses. auto &parentNode = shadowNodeFromContext(parentYogaNode); - auto &oldNode = shadowNodeFromContext(oldYogaNode); - - auto clonedNode = oldNode.clone( - {ShadowNodeFragment::propsPlaceholder(), - ShadowNodeFragment::childrenPlaceholder(), - oldNode.getState()}); - parentNode.replaceChild(oldNode, clonedNode, childIndex); - return &traitCast(*clonedNode).yogaNode_; + return &parentNode.cloneChildInPlace(childIndex).yogaNode_; } YGSize YogaLayoutableShadowNode::yogaNodeMeasureCallbackConnector( @@ -759,10 +839,10 @@ YGConfig &YogaLayoutableShadowNode::initializeYogaConfig( const YGConfigRef previousConfig) { YGConfigSetCloneNodeFunc( &config, YogaLayoutableShadowNode::yogaNodeCloneCallbackConnector); - YGConfigSetErrata(&config, YGErrataAll); if (previousConfig != nullptr) { YGConfigSetPointScaleFactor( &config, YGConfigGetPointScaleFactor(previousConfig)); + YGConfigSetErrata(&config, YGConfigGetErrata(previousConfig)); } #ifdef RN_DEBUG_YOGA_LOGGER @@ -773,16 +853,11 @@ YGConfig &YogaLayoutableShadowNode::initializeYogaConfig( #pragma mark - RTL left and right swapping -void YogaLayoutableShadowNode::swapLeftAndRightInTree( - YogaLayoutableShadowNode const &shadowNode) { - swapLeftAndRightInYogaStyleProps(shadowNode); - swapLeftAndRightInViewProps(shadowNode); +void YogaLayoutableShadowNode::swapStyleLeftAndRight() { + ensureUnsealed(); - for (auto &child : shadowNode.yogaLayoutableChildren_) { - if (!child->doesOwn(shadowNode)) { - swapLeftAndRightInTree(*child); - } - } + swapLeftAndRightInYogaStyleProps(*this); + swapLeftAndRightInViewProps(*this); } void YogaLayoutableShadowNode::swapLeftAndRightInYogaStyleProps( @@ -893,22 +968,6 @@ void YogaLayoutableShadowNode::swapLeftAndRightInViewProps( void YogaLayoutableShadowNode::ensureConsistency() const { ensureYogaChildrenLookFine(); ensureYogaChildrenAlignment(); - ensureYogaChildrenOwnersConsistency(); -} - -void YogaLayoutableShadowNode::ensureYogaChildrenOwnersConsistency() const { -#ifdef REACT_NATIVE_DEBUG - // Checking that all Yoga node children have the same `owner`. - // The owner might be not equal to the `yogaNode_` though. - auto &yogaChildren = yogaNode_.getChildren(); - - if (!yogaChildren.empty()) { - auto owner = yogaChildren.at(0)->getOwner(); - for (auto const &child : yogaChildren) { - react_native_assert(child->getOwner() == owner); - } - } -#endif } void YogaLayoutableShadowNode::ensureYogaChildrenLookFine() const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h index d59e515487b..fee4c635810 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h @@ -132,6 +132,27 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { */ void adoptYogaChild(size_t index); + /** + * Applies contextual values to the ShadowNode's Yoga tree after the + * ShadowTree has been constructed, but before it has been is laid out or + * committed. + */ + void configureYogaTree( + float pointScaleFactor, + YGErrata defaultErrata, + bool swapLeftAndRight); + + /** + * Return an errata based on a `layoutConformance` prop if given, otherwise + * the passed default + */ + YGErrata resolveErrata(YGErrata defaultErrata) const; + + /** + * Replcaes a child with a mutable clone of itself, returning the clone. + */ + YogaLayoutableShadowNode &cloneChildInPlace(int32_t layoutableChildIndex); + static YGConfig &initializeYogaConfig( YGConfig &config, YGConfigRef previousConfig = nullptr); @@ -150,7 +171,7 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { #pragma mark - RTL Legacy Autoflip /* - * Walks though shadow node hierarchy and reassign following values: + * Reassigns the following values: * - (left|right) → (start|end) * - margin(Left|Right) → margin(Start|End) * - padding(Left|Right) → padding(Start|End) @@ -161,8 +182,7 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { * This is neccesarry to be backwards compatible with old renderer, it swaps * the values as well in https://fburl.com/diffusion/kl7bjr3h */ - static void swapLeftAndRightInTree( - YogaLayoutableShadowNode const &shadowNode); + void swapStyleLeftAndRight(); /* * In shadow node passed as argument, reassigns following values * - borderTop(Left|Right)Radius → borderTop(Start|End)Radius @@ -193,7 +213,6 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { void ensureConsistency() const; void ensureYogaChildrenAlignment() const; - void ensureYogaChildrenOwnersConsistency() const; void ensureYogaChildrenLookFine() const; #pragma mark - Private member variables @@ -201,6 +220,11 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { * List of children which derive from YogaLayoutableShadowNode */ ListOfShared yogaLayoutableChildren_; + + /* + * Whether the full Yoga subtree of this Node has been configured. + */ + bool yogaTreeHasBeenConfigured_{false}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 85191d84373..7d3d9d0586f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -660,6 +660,28 @@ inline void fromRawValue( react_native_expect(false); } +inline void fromRawValue( + const PropsParserContext & /*context*/, + const RawValue &value, + LayoutConformance &result) { + result = LayoutConformance::Classic; + react_native_expect(value.hasType()); + if (!value.hasType()) { + return; + } + auto stringValue = (std::string)value; + if (stringValue == "classic") { + result = LayoutConformance::Classic; + return; + } + if (stringValue == "strict") { + result = LayoutConformance::Strict; + return; + } + LOG(ERROR) << "Could not parse LayoutConformance:" << stringValue; + react_native_expect(false); +} + inline std::string toString( const std::array()> &dimensions) { return "{" + folly::to(dimensions[0]) + ", " + @@ -838,4 +860,15 @@ inline std::string toString(const YGStyle::Edges &value) { return "{" + result + "}"; } +inline std::string toString(const LayoutConformance &value) { + switch (value) { + case LayoutConformance::Undefined: + return "undefined"; + case LayoutConformance::Classic: + return "classic"; + case LayoutConformance::Strict: + return "strict"; + } +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index 9e7889a7612..e3591f7c41f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -87,6 +87,8 @@ enum class BorderCurve : uint8_t { Circular, Continuous }; enum class BorderStyle : uint8_t { Solid, Dotted, Dashed }; +enum class LayoutConformance : uint8_t { Undefined, Classic, Strict }; + template struct CascadedRectangleEdges { using Counterpart = RectangleEdges; diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h index c4f4b82c592..714b37f43b4 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h @@ -31,6 +31,10 @@ struct LayoutMetrics { DisplayType displayType{DisplayType::Flex}; // See `LayoutDirection` for all possible options. LayoutDirection layoutDirection{LayoutDirection::Undefined}; + // Whether React Native treated cardinal directions as flow-relative + // directions due to being laid out in RTL with `doLeftAndRightSwapInRTL` + // enabled. + bool wasLeftAndRightSwapped{false}; // Pixel density. Number of device pixels per density-independent pixel. Float pointScaleFactor{1.0}; // How much the children of the node actually overflow in each direction. diff --git a/packages/react-native/types/experimental.d.ts b/packages/react-native/types/experimental.d.ts index c58c9ffa10a..2e9f69d0c84 100644 --- a/packages/react-native/types/experimental.d.ts +++ b/packages/react-native/types/experimental.d.ts @@ -133,4 +133,13 @@ declare module '.' { */ paddingInlineStart?: DimensionValue | undefined; } + + export interface ViewProps { + /** + * Contols whether this view, and its transitive children, are laid in a way + * consistent with web browsers ('strict'), or consistent with existing + * React Native code which may rely on incorrect behavior ('classic'). + */ + experimental_layoutConformance?: 'strict' | 'classic' | undefined; + } } diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index b9fe52ef7ba..bd278ccd7ed 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -342,6 +342,51 @@ class FlexGapExample extends React.Component<$ReadOnly<{|testID?: ?string|}>> { } } +function LayoutConformanceExample(): React.Node { + return ( + + + Unset + + + + Classic + + + + Strict + + + + ); +} + +function LayoutConformanceBox(): React.Node { + return ( + + + + + + ); +} + export default ({ title: 'View', documentationURL: 'https://reactnative.dev/docs/view', @@ -847,5 +892,10 @@ export default ({ ); }, }, + { + title: 'Layout conformance', + name: 'layout-conformance', + render: LayoutConformanceExample, + }, ], }: RNTesterModule);