diff --git a/android/src/main/jni/RNKC.h b/android/src/main/jni/RNKC.h index b8a0c49e99..9916c77dc1 100644 --- a/android/src/main/jni/RNKC.h +++ b/android/src/main/jni/RNKC.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h new file mode 100644 index 0000000000..3a3a23a4c7 --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewComponentDescriptor.h @@ -0,0 +1,27 @@ +// +// RNKCClippingScrollViewDecoratorViewComponentDescriptor.h +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#pragma once + +#include "RNKCClippingScrollViewDecoratorViewShadowNode.h" + +#include +#include +#include + +namespace facebook::react { +class ClippingScrollViewDecoratorViewComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + void adopt(ShadowNode &shadowNode) const override { + react_native_assert(dynamic_cast(&shadowNode)); + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +} // namespace facebook::react diff --git a/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp new file mode 100644 index 0000000000..2d87dd5740 --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.cpp @@ -0,0 +1,14 @@ +// +// RNKCClippingScrollViewDecoratorViewShadowNode.cpp +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#include "RNKCClippingScrollViewDecoratorViewShadowNode.h" + +namespace facebook::react { + +extern const char ClippingScrollViewDecoratorViewComponentName[] = "ClippingScrollViewDecoratorView"; + +} // namespace facebook::react diff --git a/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h new file mode 100644 index 0000000000..49a46387c4 --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewShadowNode.h @@ -0,0 +1,30 @@ +// +// RNKCClippingScrollViewDecoratorViewShadowNode.h +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#pragma once + +#include "RNKCClippingScrollViewDecoratorViewState.h" + +#include +#include +#include +#include + +namespace facebook::react { + +JSI_EXPORT extern const char ClippingScrollViewDecoratorViewComponentName[]; + +/* + * `ShadowNode` for component. + */ +using ClippingScrollViewDecoratorViewShadowNode = ConcreteViewShadowNode< + ClippingScrollViewDecoratorViewComponentName, + ClippingScrollViewDecoratorViewProps, + ClippingScrollViewDecoratorViewEventEmitter, + ClippingScrollViewDecoratorViewState>; + +} // namespace facebook::react diff --git a/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h new file mode 100644 index 0000000000..f940b5996b --- /dev/null +++ b/common/cpp/react/renderer/components/RNKC/RNKCClippingScrollViewDecoratorViewState.h @@ -0,0 +1,28 @@ +// +// RNKCClippingScrollViewDecoratorViewState.h +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +class ClippingScrollViewDecoratorViewState { + public: + ClippingScrollViewDecoratorViewState() = default; + +#ifdef ANDROID + ClippingScrollViewDecoratorViewState(ClippingScrollViewDecoratorViewState const &previousState, folly::dynamic data) {} + folly::dynamic getDynamic() const { + return {}; + } +#endif +}; + +} // namespace facebook::react diff --git a/ios/views/ClippingScrollViewDecoratorViewManager.h b/ios/views/ClippingScrollViewDecoratorViewManager.h new file mode 100644 index 0000000000..c35d65604a --- /dev/null +++ b/ios/views/ClippingScrollViewDecoratorViewManager.h @@ -0,0 +1,25 @@ +// +// ClippingScrollViewDecoratorViewManager.h +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#else +#import +#endif +#import +#import + +@interface ClippingScrollViewDecoratorViewManager : RCTViewManager +@end + +@interface ClippingScrollViewDecoratorView : +#ifdef RCT_NEW_ARCH_ENABLED + RCTViewComponentView +#else + UIView +#endif +@end diff --git a/ios/views/ClippingScrollViewDecoratorViewManager.mm b/ios/views/ClippingScrollViewDecoratorViewManager.mm new file mode 100644 index 0000000000..8ce311e96a --- /dev/null +++ b/ios/views/ClippingScrollViewDecoratorViewManager.mm @@ -0,0 +1,146 @@ +// +// ClippingScrollViewDecoratorViewManager.mm +// Pods +// +// Created by Kiryl Ziusko on 03/03/2025. +// + +#import "ClippingScrollViewDecoratorViewManager.h" + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import + +#import "RCTFabricComponentsPlugins.h" +#endif + +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +using namespace facebook::react; +#endif + +#pragma mark - Helpers + +static UIScrollView *KCFindFirstScrollView(UIView *view) +{ + for (UIView *subview in view.subviews) { + if ([subview isKindOfClass:[UIScrollView class]] && + ![subview isKindOfClass:[UITextView class]]) { + return (UIScrollView *)subview; + } + UIScrollView *found = KCFindFirstScrollView(subview); + if (found) { + return found; + } + } + return nil; +} + +static void KCApplyNoopScrollRectToVisible(UIScrollView *scrollView) +{ + if (!scrollView) { + return; + } + + Class originalClass = object_getClass(scrollView); + NSString *originalClassName = NSStringFromClass(originalClass); + + // Already patched — nothing to do + if ([originalClassName hasPrefix:@"KC_NoScrollRect_"]) { + return; + } + + NSString *subclassName = [@"KC_NoScrollRect_" stringByAppendingString:originalClassName]; + Class subclass = NSClassFromString(subclassName); + + if (!subclass) { + subclass = objc_allocateClassPair(originalClass, subclassName.UTF8String, 0); + if (!subclass) { + return; + } + + Method original = + class_getInstanceMethod(originalClass, @selector(scrollRectToVisible:animated:)); + if (original) { + IMP noopImp = imp_implementationWithBlock( + ^(__unused UIScrollView *self, __unused CGRect rect, __unused BOOL animated){ + // no-op + }); + class_addMethod( + subclass, + @selector(scrollRectToVisible:animated:), + noopImp, + method_getTypeEncoding(original)); + } + + objc_registerClassPair(subclass); + } + + object_setClass(scrollView, subclass); +} + +#pragma mark - Manager + +@implementation ClippingScrollViewDecoratorViewManager + +RCT_EXPORT_MODULE(ClippingScrollViewDecoratorViewManager) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +#ifndef RCT_NEW_ARCH_ENABLED +- (UIView *)view +{ + return [[ClippingScrollViewDecoratorView alloc] init]; +} +#endif + +@end + +#pragma mark - View + +#ifdef RCT_NEW_ARCH_ENABLED +@interface ClippingScrollViewDecoratorView () +#else +@interface ClippingScrollViewDecoratorView () +#endif +@end + +@implementation ClippingScrollViewDecoratorView + +#ifdef RCT_NEW_ARCH_ENABLED ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} +#endif + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load +{ + [super load]; +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + if (self.window) { + UIScrollView *scrollView = KCFindFirstScrollView(self); + KCApplyNoopScrollRectToVisible(scrollView); + } +} + +#ifdef RCT_NEW_ARCH_ENABLE +Class ClippingScrollViewDecoratorViewCls(void) +{ + return ClippingScrollViewDecoratorView.class; +} +#endif + +@end diff --git a/package.json b/package.json index 15c271bd02..e4e948eb98 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,8 @@ "KeyboardGestureArea": "KeyboardGestureArea", "OverKeyboardView": "OverKeyboardView", "KeyboardBackgroundView": "KeyboardBackgroundView", - "KeyboardExtender": "KeyboardExtender" + "KeyboardExtender": "KeyboardExtender", + "ClippingScrollViewDecoratorView": "ClippingScrollViewDecoratorView" } } }, diff --git a/react-native.config.js b/react-native.config.js index 2b17c7a2f8..fb3c79b6e3 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -7,6 +7,7 @@ module.exports = { "KeyboardGestureAreaComponentDescriptor", "OverKeyboardViewComponentDescriptor", "KeyboardBackgroundViewComponentDescriptor", + "ClippingScrollViewDecoratorViewComponentDescriptor", ], cmakeListsPath: "../android/src/main/jni/CMakeLists.txt", }, diff --git a/src/bindings.native.ts b/src/bindings.native.ts index 264bcf4a0c..9489aa9834 100644 --- a/src/bindings.native.ts +++ b/src/bindings.native.ts @@ -1,7 +1,6 @@ import { NativeEventEmitter, Platform } from "react-native"; import type { - ClippingScrollViewProps, FocusedInputEventsModule, KeyboardBackgroundViewProps, KeyboardControllerNativeModule, @@ -73,6 +72,4 @@ export const RCTKeyboardExtender: React.FC = ? require("./specs/KeyboardExtenderNativeComponent").default : ({ children }: KeyboardExtenderProps) => children; export const ClippingScrollView: React.FC = - Platform.OS === "android" - ? require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default - : ({ children }: ClippingScrollViewProps) => children; + require("./specs/ClippingScrollViewDecoratorViewNativeComponent").default; diff --git a/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts b/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts index 0a331cd553..e8999945cc 100644 --- a/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts +++ b/src/specs/ClippingScrollViewDecoratorViewNativeComponent.ts @@ -13,6 +13,5 @@ export default codegenNativeComponent( "ClippingScrollViewDecoratorView", { interfaceOnly: true, - excludedPlatforms: ["iOS"], }, ) as HostComponent;