Skip to content

Commit

Permalink
"experimental_layoutConformance" ViewProp -> "experimental_LayoutConf…
Browse files Browse the repository at this point in the history
…ormance" component (facebook#48188)

Summary:
Pull Request resolved: facebook#48188

Yoga is full of bugs! Some of these bugs cannot be fixed without breaking large swaths of product code. To get around this, we introduced "errata" to Yoga as a mechanism to preserve bug compatibility, and an `experimental_layoutConformance` prop in React Native to create layout conformance contexts. This has allowed us to create more compliant layout behavior for XPR.

This prop was originally designed as a context-like component, so you could set a conformance level at the root of your app, and individual components could change it for compatibility. This was difficult to achieve at the time, without introducing a primitive like `LayoutConformanceView`, which itself participated in the view tree. This prop has not been the desired end-goal, since it does not make clear that it is setting a whole new context, effecting children as well!

Now that we've landed support for `display: contents`, we can achieve this desired API pretty easily.

**Before**

```
import {View} from 'react-native';

// Root of the app
<View {...props} experimental_layoutConformance="strict">
  {content}
</View>

```

**After**

```
import {View, experimental_LayoutConformance as LayoutConformance} from 'react-native';

// Root of the app
<LayoutConformance mode="strict">
  <View {...props}>
    {content}
  </View>
</LayoutConformance>

```

Changelog: [Internal]

Differential Revision: D66910054
  • Loading branch information
NickGerleman authored and facebook-github-bot committed Dec 10, 2024
1 parent 3b079f3 commit 3c98563
Show file tree
Hide file tree
Showing 28 changed files with 361 additions and 134 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

import type * as React from 'react';

type LayoutConformanceProps = {
/**
* strict: Layout in accordance with W3C spec, even when breaking
* compatibility: Layout with the same behavior as previous versions of React Native
*/
mode: 'strict' | 'compatibility';
children: React.ReactNode;
};

export const experimental_LayoutConformance: React.ComponentType<LayoutConformanceProps>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/

import StyleSheet from '../../StyleSheet/StyleSheet';
import LayoutConformanceNativeComponent from './LayoutConformanceNativeComponent';
import * as React from 'react';

type Props = $ReadOnly<{
/**
* strict: Layout in accordance with W3C spec, even when breaking
* compatibility: Layout with the same behavior as previous versions of React Native
*/
mode: 'strict' | 'compatibility',

children: React.Node,
}>;

// We want a graceful fallback for apps using legacy arch, but need to know
// ahead of time whether the component is available, so we test for global.
// This does not correctly handle mixed arch apps (which is okay, since we just
// degrade the error experience).
const isFabricUIManagerInstalled = global?.nativeFabricUIManager != null;

function LayoutConformance(props: Props): React.Node {
return (
<LayoutConformanceNativeComponent {...props} style={styles.container} />
);
}

function UnimplementedLayoutConformance(props: Props): React.Node {
if (__DEV__) {
const warnOnce = require('../../Utilities/warnOnce');

warnOnce(
'layoutconformance-unsupported',
'"LayoutConformance" is only supported in the New Architecture',
);
}

return props.children;
}

export default (isFabricUIManagerInstalled
? LayoutConformance
: UnimplementedLayoutConformance) as component(...Props);

const styles = StyleSheet.create({
container: {
display: 'contents',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
import type {ViewProps} from '../View/ViewPropTypes';

import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry';

type Props = $ReadOnly<{
mode: 'strict' | 'compatibility',
...ViewProps,
}>;

const LayoutConformanceNativeComponent: HostComponent<Props> =
NativeComponentRegistry.get<Props>('LayoutConformance', () => ({
uiViewClassName: 'LayoutConformance',
validAttributes: {
mode: true,
},
}));

export default LayoutConformanceNativeComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,4 @@ export interface ViewProps
* Used to reference react managed views from native code.
*/
nativeID?: string | undefined;

/**
* 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -578,15 +578,6 @@ export type ViewProps = $ReadOnly<{|
*/
collapsableChildren?: ?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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,6 @@ const validAttributesForNonEventProps = {

style: ReactNativeStyleAttributes,

experimental_layoutConformance: true,

// ReactClippingViewManager @ReactProps
removeClippedSubviews: true,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,6 @@ const validAttributesForNonEventProps = {
direction: true,

style: ReactNativeStyleAttributes,

experimental_layoutConformance: true,
};

// Props for bubbling and direct events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1887,6 +1887,25 @@ declare export default typeof NativeKeyboardObserver;
"
`;

exports[`public API should not change unintentionally Libraries/Components/LayoutConformance/LayoutConformance.js 1`] = `
"type Props = $ReadOnly<{
mode: \\"strict\\" | \\"compatibility\\",
children: React.Node,
}>;
declare export default component(...Props);
"
`;

exports[`public API should not change unintentionally Libraries/Components/LayoutConformance/LayoutConformanceNativeComponent.js 1`] = `
"type Props = $ReadOnly<{
mode: \\"strict\\" | \\"compatibility\\",
...ViewProps,
}>;
declare const LayoutConformanceNativeComponent: HostComponent<Props>;
declare export default typeof LayoutConformanceNativeComponent;
"
`;

exports[`public API should not change unintentionally Libraries/Components/Pressable/Pressable.js 1`] = `
"type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, \\"style\\">;
export type StateCallbackType = $ReadOnly<{|
Expand Down Expand Up @@ -4268,7 +4287,6 @@ export type ViewProps = $ReadOnly<{|
\\"aria-hidden\\"?: ?boolean,
collapsable?: ?boolean,
collapsableChildren?: ?boolean,
experimental_layoutConformance?: ?(\\"strict\\" | \\"classic\\"),
id?: string,
testID?: ?string,
nativeID?: ?string,
Expand Down Expand Up @@ -9623,6 +9641,7 @@ declare module.exports: {
get Image(): Image,
get ImageBackground(): ImageBackground,
get InputAccessoryView(): InputAccessoryView,
get experimental_LayoutConformance(): LayoutConformance,
get KeyboardAvoidingView(): KeyboardAvoidingView,
get Modal(): Modal,
get Pressable(): Pressable,
Expand Down
7 changes: 0 additions & 7 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -424,13 +424,6 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie
// 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.
}

typedef NSArray *FilterArray; // Custom type to make the StaticViewConfigValidator Happy
RCT_CUSTOM_VIEW_PROPERTY(filter, FilterArray, RCTView)
{
Expand Down
1 change: 0 additions & 1 deletion packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -4175,7 +4175,6 @@ public class com/facebook/react/uimanager/LayoutShadowNode : com/facebook/react/
public fun setInsetBlock (ILcom/facebook/react/bridge/Dynamic;)V
public fun setInsetInline (ILcom/facebook/react/bridge/Dynamic;)V
public fun setJustifyContent (Ljava/lang/String;)V
public fun setLayoutConformance (Ljava/lang/String;)V
public fun setMarginBlock (ILcom/facebook/react/bridge/Dynamic;)V
public fun setMarginInline (ILcom/facebook/react/bridge/Dynamic;)V
public fun setMargins (ILcom/facebook/react/bridge/Dynamic;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,9 +972,4 @@ public void setShouldNotifyPointerLeave(boolean value) {
public void setShouldNotifyPointerMove(boolean value) {
// Do Nothing: Align with static ViewConfigs
}

@ReactProp(name = "experimental_layoutConformance")
public void setLayoutConformance(String value) {
// Do Nothing: Align with static ViewConfigs
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <react/renderer/components/text/ParagraphComponentDescriptor.h>
#include <react/renderer/components/text/RawTextComponentDescriptor.h>
#include <react/renderer/components/text/TextComponentDescriptor.h>
#include <react/renderer/components/view/LayoutConformanceComponentDescriptor.h>
#include <react/renderer/components/view/ViewComponentDescriptor.h>

namespace facebook::react::CoreComponentsRegistry {
Expand Down Expand Up @@ -66,6 +67,8 @@ sharedProviderRegistry() {
AndroidDrawerLayoutComponentDescriptor>());
providerRegistry->add(concreteComponentDescriptorProvider<
DebuggingOverlayComponentDescriptor>());
providerRegistry->add(concreteComponentDescriptorProvider<
LayoutConformanceComponentDescriptor>());

return providerRegistry;
}();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,16 +342,7 @@ BaseViewProps::BaseViewProps(
rawProps,
"removeClippedSubviews",
sourceProps.removeClippedSubviews,
false)),
experimental_layoutConformance(
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
? sourceProps.experimental_layoutConformance
: convertRawProp(
context,
rawProps,
"experimental_layoutConformance",
sourceProps.experimental_layoutConformance,
{})) {}
false)) {}

#define VIEW_EVENT_CASE(eventType) \
case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \
Expand Down Expand Up @@ -397,7 +388,6 @@ void BaseViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable);
RAW_SET_PROP_SWITCH_CASE_BASIC(collapsableChildren);
RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews);
RAW_SET_PROP_SWITCH_CASE_BASIC(experimental_layoutConformance);
RAW_SET_PROP_SWITCH_CASE_BASIC(cursor);
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineColor);
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineOffset);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {

bool removeClippedSubviews{false};

LayoutConformance experimental_layoutConformance{};

#pragma mark - Convenience Methods

CascadedBorderWidths getBorderWidths() const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <react/renderer/components/view/LayoutConformanceShadowNode.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>

namespace facebook::react {

using LayoutConformanceComponentDescriptor =
ConcreteComponentDescriptor<LayoutConformanceShadowNode>;

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <react/renderer/components/view/YogaStylableProps.h>
#include <react/renderer/components/view/propsConversions.h>

namespace facebook::react {

struct LayoutConformanceProps final : public YogaStylableProps {
/**
* Whether to layout the subtree with strict conformance to W3C standard
* (YGErrataNone) or for compatibility with legacy RN bugs (YGErrataAll)
*/
LayoutConformance mode{LayoutConformance::Strict};

LayoutConformanceProps() = default;
LayoutConformanceProps(
const PropsParserContext& context,
const LayoutConformanceProps& sourceProps,
const RawProps& rawProps)
: YogaStylableProps(context, sourceProps, rawProps),
mode{convertRawProp(
context,
rawProps,
"mode",
mode,
LayoutConformance::Strict)} {}
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/LayoutConformanceProps.h>
#include <react/renderer/components/view/YogaLayoutableShadowNode.h>

namespace facebook::react {

constexpr const char LayoutConformanceShadowNodeComponentName[] =
"LayoutConformance";

using LayoutConformanceShadowNode = ConcreteShadowNode<
LayoutConformanceShadowNodeComponentName,
YogaLayoutableShadowNode,
LayoutConformanceProps>;

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <react/debug/flags.h>
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/components/view/LayoutConformanceShadowNode.h>
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/components/view/ViewShadowNode.h>
#include <react/renderer/components/view/conversions.h>
Expand Down Expand Up @@ -501,15 +502,13 @@ void YogaLayoutableShadowNode::configureYogaTree(
}

YGErrata YogaLayoutableShadowNode::resolveErrata(YGErrata defaultErrata) const {
if (auto viewShadowNode = dynamic_cast<const ViewShadowNode*>(this)) {
const auto& props = viewShadowNode->getConcreteProps();
switch (props.experimental_layoutConformance) {
case LayoutConformance::Classic:
return YGErrataAll;
if (auto layoutConformanceNode =
dynamic_cast<const LayoutConformanceShadowNode*>(this)) {
switch (layoutConformanceNode->getConcreteProps().mode) {
case LayoutConformance::Strict:
return YGErrataNone;
case LayoutConformance::Undefined:
return defaultErrata;
case LayoutConformance::Compatibility:
return YGErrataAll;
}
}

Expand Down
Loading

0 comments on commit 3c98563

Please sign in to comment.