diff --git a/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetector.kt b/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetector.kt new file mode 100644 index 0000000000..899c11f763 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetector.kt @@ -0,0 +1,14 @@ +package com.swmansion.rnscreens + +import android.annotation.SuppressLint +import com.facebook.react.bridge.ReactContext +import com.facebook.react.views.view.ReactViewGroup + +@SuppressLint("ViewConstructor") +class ContentScrollViewDetector( + val reactContext: ReactContext, +) : ReactViewGroup(reactContext) { + override fun onAttachedToWindow() { + super.onAttachedToWindow() + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetectorViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetectorViewManager.kt new file mode 100644 index 0000000000..0b3d19c526 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ContentScrollViewDetectorViewManager.kt @@ -0,0 +1,32 @@ +package com.swmansion.rnscreens + +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNSContentScrollViewDetectorManagerDelegate +import com.facebook.react.viewmanagers.RNSContentScrollViewDetectorManagerInterface + +@ReactModule(name = ContentScrollViewDetectorViewManager.REACT_CLASS) +open class ContentScrollViewDetectorViewManager : + ViewGroupManager(), + RNSContentScrollViewDetectorManagerInterface { + private val delegate: ViewManagerDelegate + + init { + delegate = + RNSContentScrollViewDetectorManagerDelegate( + this, + ) + } + + override fun getName() = REACT_CLASS + + override fun createViewInstance(reactContext: ThemedReactContext) = ContentScrollViewDetector(reactContext) + + override fun getDelegate(): ViewManagerDelegate = delegate + + companion object Companion { + const val REACT_CLASS = "RNSContentScrollViewDetector" + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt index ad027bf46a..39d47a904f 100644 --- a/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +++ b/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt @@ -51,6 +51,7 @@ class RNScreensPackage : BaseReactPackage() { TabsHostViewManager(), TabScreenViewManager(), SafeAreaViewManager(), + ContentScrollViewDetectorViewManager(), ) } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerDelegate.java new file mode 100644 index 0000000000..7c5f59b09e --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerDelegate.java @@ -0,0 +1,27 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaDelegate.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.BaseViewManagerDelegate; +import com.facebook.react.uimanager.LayoutShadowNode; + +@SuppressWarnings("deprecation") +public class RNSContentScrollViewDetectorManagerDelegate & RNSContentScrollViewDetectorManagerInterface> extends BaseViewManagerDelegate { + public RNSContentScrollViewDetectorManagerDelegate(U viewManager) { + super(viewManager); + } + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + super.setProperty(view, propName, value); + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerInterface.java new file mode 100644 index 0000000000..dec6ab3507 --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSContentScrollViewDetectorManagerInterface.java @@ -0,0 +1,17 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaInterface.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import com.facebook.react.uimanager.ViewManagerWithGeneratedInterface; + +public interface RNSContentScrollViewDetectorManagerInterface extends ViewManagerWithGeneratedInterface { + // No props +} diff --git a/ios/RNSContentScrollViewDetector.h b/ios/RNSContentScrollViewDetector.h new file mode 100644 index 0000000000..e102861b3b --- /dev/null +++ b/ios/RNSContentScrollViewDetector.h @@ -0,0 +1,30 @@ +#pragma once + +#import "ContentScrollViewConsumer.h" +#import "RNSReactBaseView.h" +#import "RNSScrollViewFinder.h" + +#if !RCT_NEW_ARCH_ENABLED +#import +#endif // !RCT_NEW_ARCH_ENABLED + +NS_ASSUME_NONNULL_BEGIN + +@interface RNSContentScrollViewDetector : RNSReactBaseView + +- (nullable UIScrollView *)findContentScrollViewWithinDetector; + +- (void)registerContentScrollViewInAncestors; + +- (void)unregisterContentScrollViewInAncestors; + +@end + +#if !RCT_NEW_ARCH_ENABLED + +@interface RNSContentScrollViewDetectorViewManager : RCTViewManager +@end + +#endif // !RCT_NEW_ARCH_ENABLED + +NS_ASSUME_NONNULL_END diff --git a/ios/RNSContentScrollViewDetector.mm b/ios/RNSContentScrollViewDetector.mm new file mode 100644 index 0000000000..df17166b1b --- /dev/null +++ b/ios/RNSContentScrollViewDetector.mm @@ -0,0 +1,104 @@ +#import "RNSContentScrollViewDetector.h" +#import +#import + +namespace react = facebook::react; + +@implementation RNSContentScrollViewDetector { + UIScrollView *_scrollView; + UIView *_scrollViewConsumer; +} + +- (void)didMoveToWindow +{ + if (self.window == nil) { + // The view is detached from window + if (_scrollView) { + [self unregisterContentScrollViewInAncestors]; + } + } else { + if ((_scrollView = [self findContentScrollViewWithinDetector])) { + [self registerContentScrollViewInAncestors]; + } + } +} + +- (nullable UIScrollView *)findContentScrollViewWithinDetector +{ + auto scrollView = [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:self]; + if (scrollView) { + RCTLogInfo(@"Found content ScrollView"); + return scrollView; + } + + return nullptr; +} + +- (void)registerContentScrollViewInAncestors +{ + if (_scrollViewConsumer != nullptr) { + RCTLogWarn( + @"Content ScrollView has already been registered. Make sure to only have one ScrollViewWrapper for content ScrollView."); + [self unregisterContentScrollViewInAncestors]; + } + + UIView *parent = self.superview; + while (parent) { + if ([parent respondsToSelector:@selector(findContentScrollView)]) { + RCTLogWarn( + @"Nested ScrollViewWrapper detected. Make sure to only have one ScrollViewWrapper for content ScrollView."); + } + + if ([parent respondsToSelector:@selector(registerContentScrollView:)]) { + [(id)parent registerContentScrollView:_scrollView]; + _scrollViewConsumer = parent; + RCTLogInfo(@"registered content ScrollView for: %@", parent); + break; + } + + parent = parent.superview; + } +} + +- (void)unregisterContentScrollViewInAncestors +{ + if (_scrollViewConsumer == nullptr) { + return; + } + + [(id)_scrollViewConsumer unregisterContentScrollView:_scrollView]; + + RCTLogInfo(@"unregistered content ScrollView for: %@", _scrollViewConsumer); + _scrollViewConsumer = nullptr; +} + +#pragma mark - RCTViewComponentViewProtocol + ++ (react::ComponentDescriptorProvider)componentDescriptorProvider +{ + return react::concreteComponentDescriptorProvider(); +} + +@end + +#if RCT_NEW_ARCH_ENABLED +Class RNSContentScrollViewDetectorCls(void) +{ + return RNSContentScrollViewDetector.class; +} +#endif // RCT_NEW_ARCH_ENABLED + +#if !RCT_NEW_ARCH_ENABLED + +@implementation RNSContentScrollViewDetectorViewManager + +RCT_EXPORT_MODULE(RNSContentScrollViewDetector); + +- (UIView *)view +{ + return [[RNSContentScrollViewDetector alloc] init]; +} + +@end + +#endif // !RCT_NEW_ARCH_ENABLED diff --git a/ios/helpers/scroll-view/ContentScrollViewConsumer.h b/ios/helpers/scroll-view/ContentScrollViewConsumer.h new file mode 100644 index 0000000000..85a4c57f65 --- /dev/null +++ b/ios/helpers/scroll-view/ContentScrollViewConsumer.h @@ -0,0 +1,9 @@ +#pragma once + +@protocol ContentScrollViewConsumer + +- (void)registerContentScrollView:(UIScrollView *)scrollView; + +- (void)unregisterContentScrollView:(UIScrollView *)scrollView; + +@end diff --git a/package.json b/package.json index d5fd8b017b..0ab2b107fa 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,8 @@ "RNSSearchBar": "RNSSearchBar", "RNSSplitViewHost": "RNSSplitViewHostComponentView", "RNSSplitViewScreen": "RNSSplitViewScreenComponentView", - "RNSSafeAreaView": "RNSSafeAreaViewComponentView" + "RNSSafeAreaView": "RNSSafeAreaViewComponentView", + "RNSContentScrollViewDetector": "RNSContentScrollViewDetector" }, "components": { "RNSFullWindowOverlay": { @@ -245,6 +246,9 @@ }, "RNSSafeAreaView": { "className": "RNSSafeAreaViewComponentView" + }, + "RNSContentScrollViewDetector": { + "className": "RNSContentScrollViewDetector" } } } diff --git a/src/components/ContentScrollViewDetector.tsx b/src/components/ContentScrollViewDetector.tsx new file mode 100644 index 0000000000..3ababad25e --- /dev/null +++ b/src/components/ContentScrollViewDetector.tsx @@ -0,0 +1,18 @@ +'use client'; + +import React from 'react'; +import ContentScrollViewDetectorNativeComponent from '../fabric/ContentScrollViewDetectorNativeComponent'; + +function ContentScrollViewDetector({ + children, +}: { + children?: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +export default ContentScrollViewDetector; diff --git a/src/fabric/ContentScrollViewDetectorNativeComponent.ts b/src/fabric/ContentScrollViewDetectorNativeComponent.ts new file mode 100644 index 0000000000..95be9710d5 --- /dev/null +++ b/src/fabric/ContentScrollViewDetectorNativeComponent.ts @@ -0,0 +1,9 @@ +import { codegenNativeComponent } from 'react-native'; +import type { ViewProps } from 'react-native'; + +export interface NativeProps extends ViewProps {} + +export default codegenNativeComponent( + 'RNSContentScrollViewDetector', + {}, +);