Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,13 @@ export type ScrollViewPropsIOS = Readonly<{
*/
zoomScale?: ?number,
/**
* This property specifies how the safe area insets are used to modify the
* content area of the scroll view.
* How the safe area insets modify the content area of the scroll view.
*
* Along the scrollable axis, the insets become content insets, so content
* scrolls under the system UI. Across it, every value except `'never'` pads
* the content container by the insets, so content is laid out clear of the
* system UI, such as the rail on iPhone Duo or camera cutout in landscape,
* while the container's background still extends edge to edge.
*
* @default `'never'`
* @platform ios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
endDraggingSensitivityMultiplier: true,
directionalLockEnabled: true,
disableIntervalMomentum: true,
horizontal: true,
indicatorStyle: true,
inverted: true,
keyboardDismissMode: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Class<RCTComponentViewProtocol> RCTInputAccessoryCls(void) __attribute__((used))
Class<RCTComponentViewProtocol> RCTParagraphCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTPullToRefreshViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSafeAreaViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTScrollContentViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTScrollViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSwitchCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTTextInputCls(void) __attribute__((used));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{"Paragraph", RCTParagraphCls},
{"PullToRefreshView", RCTPullToRefreshViewCls},
{"SafeAreaView", RCTSafeAreaViewCls},
{"ScrollContentView", RCTScrollContentViewCls},
{"ScrollView", RCTScrollViewCls},
{"Switch", RCTSwitchCls},
{"TextInput", RCTTextInputCls},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) BOOL horizontal;

/*
* Whether JavaScript pads the content container by the safe area across the scrollable axis. Defaults to NO.
* Whether `ScrollView` pads the content container by the safe area across the scrollable axis, which it does when
* its `contentInsetAdjustmentBehavior` prop is not `never`. UIKit's own `contentInsetAdjustmentBehavior` can differ,
* as react-native-screens sets it natively on the first scroll view of a screen.
*/
@property (nonatomic, assign) BOOL padsContentForSafeArea;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.
*/

#import <UIKit/UIKit.h>

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

/*
* UIView class for the <ScrollContentView> component, the content container of a <ScrollView>.
*/
@interface RCTScrollContentViewComponentView : RCTViewComponentView

/*
* The safe area the content is laid out clear of, as padding on top of the content container's own. The enclosing
* scroll view sets the pair of insets across its scrollable axis, and zero while its `contentInsetAdjustmentBehavior`
* is `never`.
*/
@property (nonatomic, assign) UIEdgeInsets safeAreaPadding;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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.
*/

#import "RCTScrollContentViewComponentView.h"

#import <React/RCTI18nUtil.h>
#import <React/RCTUtils.h>
#import <react/renderer/components/scrollview/ScrollContentViewComponentDescriptor.h>
#import <react/renderer/components/scrollview/ScrollContentViewState.h>
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"

using namespace facebook::react;

@implementation RCTScrollContentViewComponentView {
ScrollContentViewShadowNode::ConcreteState::Shared _state;
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ScrollContentViewShadowNode::defaultSharedProps();
}

return self;
}

- (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding
{
_safeAreaPadding = safeAreaPadding;
[self _updateStateIfNecessary];
}

- (void)_updateStateIfNecessary
{
if (!_state) {
return;
}

UIEdgeInsets insets = _safeAreaPadding;
insets.left = RCTRoundPixelValue(insets.left);
insets.top = RCTRoundPixelValue(insets.top);
insets.right = RCTRoundPixelValue(insets.right);
insets.bottom = RCTRoundPixelValue(insets.bottom);

auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets);
RCTI18nUtil *i18nUtil = [RCTI18nUtil sharedInstance];
auto layoutDirection = i18nUtil.isRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight;
bool swapLeftAndRightInRTL = i18nUtil.isRTL && i18nUtil.doLeftAndRightSwapInRTL;
auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold.

_state->updateState(
[=](const ScrollContentViewShadowNode::ConcreteState::Data &oldData)
-> ScrollContentViewShadowNode::ConcreteState::SharedData {
auto deltaPadding = newPadding - oldData.safeAreaPadding;

if (std::abs(deltaPadding.left) < threshold && std::abs(deltaPadding.top) < threshold &&
std::abs(deltaPadding.right) < threshold && std::abs(deltaPadding.bottom) < threshold &&
oldData.layoutDirection == layoutDirection && oldData.swapLeftAndRightInRTL == swapLeftAndRightInRTL) {
return nullptr;
}

auto newData = oldData;
newData.safeAreaPadding = newPadding;
newData.layoutDirection = layoutDirection;
newData.swapLeftAndRightInRTL = swapLeftAndRightInRTL;
return std::make_shared<const ScrollContentViewShadowNode::ConcreteState::Data>(newData);
});
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ScrollContentViewComponentDescriptor>();
}

- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<const ScrollContentViewShadowNode::ConcreteState>(state);
}

- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
[self _updateStateIfNecessary];
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_safeAreaPadding = UIEdgeInsetsZero;
}

@end

Class<RCTComponentViewProtocol> RCTScrollContentViewCls(void)
{
return RCTScrollContentViewComponentView.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#import "RCTCustomPullToRefreshViewProtocol.h"
#import "RCTEnhancedScrollView.h"
#import "RCTFabricComponentsPlugins.h"
#import "RCTScrollContentViewComponentView.h"
#import "RCTVirtualViewContainerState.h"

using namespace facebook::react;
Expand Down Expand Up @@ -449,9 +450,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
((RCTEnhancedScrollView *)_scrollView).padsContentForSafeArea =
contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never;
_shouldUpdateContentInsetAdjustmentBehavior = NO;
}
scrollView.padsContentForSafeArea = newScrollViewProps.padsContentForSafeArea;

MAP_SCROLL_VIEW_PROP(disableIntervalMomentum);
MAP_SCROLL_VIEW_PROP(snapToInterval);
Expand All @@ -464,6 +466,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &

// Must run after `super updateProps:` so it is based on the latest calculated view.
((RCTEnhancedScrollView *)_scrollView).inverted = [self isInverted];
[self _updateContentViewSafeAreaPadding];
}

- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
Expand Down Expand Up @@ -563,6 +566,7 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
[_containerView insertSubview:childComponentView atIndex:index];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) {
_contentView = childComponentView;
[self _updateContentViewSafeAreaPadding];
}
}

Expand Down Expand Up @@ -866,6 +870,46 @@ - (void)didMoveToWindow
// The view is being removed, ensure that the scroll end event is dispatched
[self _handleScrollEndIfNeeded];
}
[self _updateContentViewSafeAreaPadding];
}

- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
[self _updateContentViewSafeAreaPadding];
}

- (void)layoutSubviews
{
[super layoutSubviews];
// Moving or resizing the view changes its insets without `safeAreaInsetsDidChange` firing; that only reports
// window-level changes.
[self _updateContentViewSafeAreaPadding];
}

/*
* Hands the content container the safe area to pad its content by: the pair of insets across the scrollable axis,
* unless `contentInsetAdjustmentBehavior` is `never`. Along the scrollable axis UIKit turns the safe area into content
* insets instead, so content scrolls under the system UI.
*/
- (void)_updateContentViewSafeAreaPadding
{
if (![_contentView isKindOfClass:[RCTScrollContentViewComponentView class]]) {
return;
}
const auto &props = static_cast<const ScrollViewProps &>(*_props);
UIEdgeInsets padding = UIEdgeInsetsZero;
if (props.contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never) {
UIEdgeInsets insets = self.safeAreaInsets;
if (props.horizontal) {
padding.top = insets.top;
padding.bottom = insets.bottom;
} else {
padding.left = insets.left;
padding.right = insets.right;
}
}
((RCTScrollContentViewComponentView *)_contentView).safeAreaPadding = padding;
}

- (void)_handleScrollEndIfNeeded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,6 @@ std::string componentNameByReactViewName(std::string viewName) {
return "PullToRefreshView";
}

// We need this temporarily for testing purposes until we have proper
// implementation of core components.
// iOS-only
if (viewName == "ScrollContentView") {
return "View";
}

// iOS-only
if (viewName == "MultilineTextInputView" ||
viewName == "SinglelineTextInputView") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,6 @@ BaseScrollViewProps::BaseScrollViewProps(
"horizontal",
sourceProps.horizontal,
true)),
padsContentForSafeArea(convertRawProp(
context,
rawProps,
"padsContentForSafeArea",
sourceProps.padsContentForSafeArea,
false)),
scrollEventThrottle(convertRawProp(
context,
rawProps,
Expand Down Expand Up @@ -300,7 +294,6 @@ void BaseScrollViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(showsVerticalScrollIndicator);
RAW_SET_PROP_SWITCH_CASE_BASIC(persistentScrollbar);
RAW_SET_PROP_SWITCH_CASE_BASIC(horizontal);
RAW_SET_PROP_SWITCH_CASE_BASIC(padsContentForSafeArea);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEventThrottle);
RAW_SET_PROP_SWITCH_CASE_BASIC(zoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(contentInset);
Expand Down Expand Up @@ -412,10 +405,6 @@ SharedDebugStringConvertibleList BaseScrollViewProps::getDebugProps() const {
defaultScrollViewProps.persistentScrollbar),
debugStringConvertibleItem(
"horizontal", horizontal, defaultScrollViewProps.horizontal),
debugStringConvertibleItem(
"padsContentForSafeArea",
padsContentForSafeArea,
defaultScrollViewProps.padsContentForSafeArea),
debugStringConvertibleItem(
"scrollEventThrottle",
scrollEventThrottle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class BaseScrollViewProps : public ViewProps {
bool showsVerticalScrollIndicator{true};
bool persistentScrollbar{false};
bool horizontal{false};
bool padsContentForSafeArea{false};
Float scrollEventThrottle{};
Float zoomScale{1.0f};
EdgeInsets contentInset{};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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/cxxstableapi/PrivateGuard.h>

#include <react/renderer/components/scrollview/ScrollContentViewShadowNode.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>

namespace facebook::react {

/*
* Descriptor for the <ScrollContentView> component.
*/
class ScrollContentViewComponentDescriptor final : public ConcreteComponentDescriptor<ScrollContentViewShadowNode> {
public:
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;

void adopt(ShadowNode &shadowNode) const override
{
static_cast<ScrollContentViewShadowNode &>(shadowNode).applySafeAreaPadding();
ConcreteComponentDescriptor::adopt(shadowNode);
}
};

} // namespace facebook::react
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.
*/

#include "ScrollContentViewProps.h"

namespace facebook::react {

ScrollContentViewProps::ScrollContentViewProps(
const PropsParserContext& context,
const ScrollContentViewProps& sourceProps,
const RawProps& rawProps)
: ViewProps(context, sourceProps, rawProps) {}

} // namespace facebook::react
Loading
Loading